Docker와 Kubernetes 세팅에서 20년차가 먼저 고정하는 것
Docker와 Kubernetes를 처음 붙일 때 가장 많이 하는 실수는 도구를 아키텍처처럼 대하는 것입니다. 컨테이너로 감싸면 구조가 좋아질 것 같고, Kubernetes에 올리면 운영이 자동화될 것 같지만 실제로는 반대인 경우가 많습니다. 애플리케이션 경계가 흐린 상태로 컨테이너를 만들면 배포 단위만 복잡해집니다.
제가 먼저 보는 것은 명령어가 아닙니다. 이 서비스가 어떤 파일로 빌드되고, 어떤 환경 변수를 필요로 하며, 어떤 포트로 살아 있고, 죽었을 때 어떻게 감지할 수 있는지입니다. 이 네 가지가 정리되지 않으면 Dockerfile이나 Deployment YAML은 금방 임시 스크립트가 됩니다.
Dockerfile에서 먼저 고정할 것
Dockerfile은 실행 환경을 문서화하는 파일입니다. 단순히 컨테이너가 뜨게 만드는 파일이 아닙니다. 좋은 Dockerfile은 누가 빌드해도 같은 결과가 나와야 하고, 런타임 이미지에 빌드 도구가 남지 않아야 하며, 애플리케이션이 root 권한으로 돌지 않아야 합니다.
Node 서비스를 예로 들면 기본 골격은 대략 이렇게 잡습니다.
FROM node:22-bookworm-slim AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci
FROM node:22-bookworm-slim AS build
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build
FROM node:22-bookworm-slim AS runtime
WORKDIR /app
ENV NODE_ENV=production
RUN useradd -r -u 10001 appuser
COPY --from=build /app/dist ./dist
COPY --from=deps /app/node_modules ./node_modules
USER appuser
EXPOSE 3000
CMD ["node", "dist/server.js"]
여기서 중요한 것은 멀티 스테이지 자체가 아닙니다. 빌드 단계와 실행 단계를 분리한다는 결정입니다. 실행 이미지에는 빌드 캐시, 테스트 도구, 소스 전체가 필요하지 않습니다. 이미지가 작아지는 것도 장점이지만 더 중요한 것은 런타임 공격면이 줄어드는 것입니다.
Compose는 로컬 운영 계약이다
Docker Compose는 운영용 오케스트레이터가 아니라 로컬 개발 환경의 계약으로 쓰는 편이 좋습니다. 애플리케이션, DB, Redis, Mock 서버를 어떻게 묶어 실행하는지 팀원 모두가 같은 방식으로 재현할 수 있게 만드는 파일입니다.
services:
app:
build: .
ports:
- "3000:3000"
environment:
DATABASE_URL: "postgres://app:app@db:5432/app"
depends_on:
db:
condition: service_healthy
db:
image: postgres:16
environment:
POSTGRES_USER: "app"
POSTGRES_PASSWORD: "app"
POSTGRES_DB: "app"
healthcheck:
test: ["CMD-SHELL", "pg_isready -U app"]
interval: 5s
timeout: 3s
retries: 10
여기서 depends_on만 믿으면 안 됩니다. 컨테이너가 시작된 것과 DB가 쿼리를 받을 준비가 된 것은 다릅니다. 그래서 healthcheck가 필요합니다. 실무에서 자주 보는 장애는 애플리케이션이 DB보다 먼저 뜨고, 첫 연결 실패 후 그대로 죽거나 반쯤 뜬 상태로 남는 경우입니다.
Kubernetes는 배포 플랫폼이지 마법이 아니다
Kubernetes에 올릴 때는 Deployment, Service, Ingress를 복사해서 붙이기 전에 먼저 애플리케이션의 생존 조건을 정해야 합니다. 어떤 요청을 받으면 살아 있다고 볼 것인지, 언제 준비 완료로 볼 것인지, CPU와 메모리를 어디까지 허용할 것인지가 없으면 클러스터는 제대로 판단할 수 없습니다.
apiVersion: apps/v1
kind: Deployment
metadata:
name: app
spec:
replicas: 2
selector:
matchLabels:
app: app
template:
metadata:
labels:
app: app
spec:
containers:
- name: app
image: registry.example.com/app:2026-05-31
ports:
- containerPort: 3000
readinessProbe:
httpGet:
path: /ready
port: 3000
periodSeconds: 5
livenessProbe:
httpGet:
path: /health
port: 3000
periodSeconds: 10
resources:
requests:
cpu: "100m"
memory: "128Mi"
limits:
cpu: "500m"
memory: "512Mi"
readiness와 liveness는 같은 것이 아닙니다. readiness는 트래픽을 받아도 되는지 묻는 것이고, liveness는 프로세스를 재시작해야 하는지 묻는 것입니다. 둘을 같은 엔드포인트로 대충 묶으면 장애 때 이상한 일이 생깁니다. DB가 잠깐 느린데 liveness가 실패해서 Pod를 계속 재시작하는 식입니다.
처음부터 꼭 넣는 기본값
Kubernetes 매니페스트에서 제가 거의 항상 먼저 확인하는 항목은 다음입니다.
- 이미지 태그가
latest가 아닌가 - readinessProbe와 livenessProbe가 분리되어 있는가
- requests와 limits가 있는가
- ConfigMap과 Secret이 코드와 분리되어 있는가
- Deployment rollout이 실패했을 때 되돌릴 수 있는가
- 로그가 stdout/stderr로 나오고 있는가
- graceful shutdown을 위한 terminationGracePeriodSeconds가 있는가
이 중 하나라도 빠지면 “일단 뜨는 배포”는 될 수 있어도 “운영 가능한 배포”라고 보기는 어렵습니다.
Kubernetes가 필요한 시점
작은 서비스 하나를 운영하는데 Kubernetes부터 도입하는 것은 대개 과합니다. Compose와 단일 VM, 또는 서버리스로 충분한 경우가 많습니다. Kubernetes가 필요한 시점은 보통 아래 조건이 겹칠 때입니다.
- 서비스가 여러 개이고 배포 주기가 다르다.
- 무중단 배포와 롤백이 자주 필요하다.
- 트래픽이 늘었다 줄었다 하며 수평 확장이 필요하다.
- 팀이 운영 표준을 공유해야 한다.
- Secret, Config, Network Policy, 관측성을 플랫폼 차원에서 관리해야 한다.
반대로 팀이 작고 서비스도 하나라면 Kubernetes는 문제보다 도구를 먼저 들여오는 선택이 될 수 있습니다. Docker는 배포 단위를 만들고, Compose는 로컬 실행 계약을 만들고, Kubernetes는 여러 배포 단위를 운영하는 플랫폼입니다. 이 셋을 같은 층위로 보면 안 됩니다.
결론
Docker를 잘 쓴다는 것은 이미지를 만들 줄 안다는 뜻이 아닙니다. 실행 환경을 재현 가능하게 고정한다는 뜻입니다. Kubernetes를 잘 쓴다는 것은 YAML을 많이 안다는 뜻이 아닙니다. 애플리케이션의 생존 조건과 운영 경계를 클러스터가 이해할 수 있게 표현한다는 뜻입니다.
컨테이너는 구조를 대신 설계해주지 않습니다. 좋은 경계를 가진 서비스를 컨테이너에 담으면 운영이 단순해지고, 경계가 없는 서비스를 컨테이너에 담으면 복잡한 배포물이 됩니다.
아직 댓글이 없습니다.