← all posts
DEV 2026.05.02 · 12 min read Intermediate

Docker에서 Kubernetes로 — 무엇이 달라지는가

docker-compose.yml 한 파일로 충분했던 세계에서, 왜 Deployment·Service·PVC·ConfigMap이 필요해지는가. 개념 매핑부터 마이그레이션 트레이드오프까지 추적한다.


docker-compose up -d 한 줄로 전체 스택이 올라오는 세계에서, Kubernetes는 같은 일을 하기 위해 Deployment·Service·PVC·ConfigMap·Secret을 따로 정의하도록 요구한다. 왜 이렇게 복잡해졌는가? 그리고 그 복잡함은 무엇을 얻기 위한 대가인가?

단일 호스트에서 클러스터로

Docker는 한 대의 서버 위에서 컨테이너를 실행한다. 스케일링이 필요하면 더 큰 서버로 올리거나, 수동으로 여러 서버를 구성해야 한다. 장애가 나면 사람이 재시작한다. 로드 밸런싱은 별도 구성이다.

Kubernetes는 이 세 가지를 클러스터 수준의 자동화로 해결한다.

Docker (단일 호스트):         Kubernetes (클러스터):
┌────────────────────┐        ┌───────────────────────────────┐
│  Container1        │        │  Control Plane                │
│  Container2        │        │  ┌─────────────────────────┐  │
│  Container3        │        │  │ API Server / Scheduler  │  │
│  Docker Engine     │        │  └────────────┬────────────┘  │
└────────────────────┘        │               ↓               │
                              │  Worker1  Worker2  Worker3    │
                              │  [Pod][Pod] [Pod][Pod] [Pod]  │
                              └───────────────────────────────┘

Pod가 죽으면 Kubernetes가 다시 만든다. 트래픽이 늘면 HPA가 Pod를 추가한다. 노드가 죽으면 Scheduler가 다른 노드에 Pod를 재배치한다. Docker에서는 이 모두를 운영자가 직접 했다.

개념 매핑 — Docker의 어휘가 K8s에서 어떻게 쪼개지는가

Docker Compose의 service 하나는 Kubernetes에서 최소 두 개의 리소스로 분리된다.

docker-compose service  →  Deployment (실행 방법)
                        +  Service    (네트워크 접근)

Volume                  →  PersistentVolumeClaim
Environment Variables   →  ConfigMap (공개) + Secret (민감)
Networks                →  Service (ClusterIP / LoadBalancer)
healthcheck             →  livenessProbe + readinessProbe

이 분리는 단순한 명명 규칙 변화가 아니다. Docker에서 ports: "8080:80"은 “이 컨테이너의 80 포트를 호스트 8080에 바인딩하라”는 명령이다. Kubernetes의 Service는 “이 레이블을 가진 Pod들에게 트래픽을 분산하라”는 선언이다. 컨테이너가 어느 노드에 있든 상관없이.

# docker-compose.yml — 명령적
services:
  web:
    image: nginx
    ports:
      - "8080:80"

# kubernetes — 선언적
apiVersion: apps/v1
kind: Deployment
metadata:
  name: web
spec:
  replicas: 3
  selector:
    matchLabels:
      app: web
  template:
    spec:
      containers:
      - name: nginx
        image: nginx
        ports:
        - containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
  name: web
spec:
  selector:
    app: web
  type: LoadBalancer
  ports:
  - port: 80
    targetPort: 80

Pod — Kubernetes의 최소 단위가 컨테이너가 아닌 이유

Docker에서 배포 단위는 컨테이너다. Kubernetes에서 배포 단위는 Pod다. Pod는 하나 이상의 컨테이너를 묶어 같은 네트워크 네임스페이스와 볼륨을 공유하게 한다.

이 설계는 Sidecar 패턴을 자연스럽게 만든다. 애플리케이션 컨테이너 옆에 로그 수집기나 메트릭 익스포터를 붙이면, 두 컨테이너는 localhost로 통신하고 같은 파일 시스템을 본다.

spec:
  volumes:
  - name: shared-logs
    emptyDir: {}
  containers:
  - name: web
    image: nginx
    volumeMounts:
    - name: shared-logs
      mountPath: /var/log/nginx
  - name: log-collector
    image: busybox
    command: ["tail", "-f", "/logs/access.log"]
    volumeMounts:
    - name: shared-logs
      mountPath: /logs

Init Container는 이 패턴의 변형이다. Main 컨테이너 전에 순차적으로 실행되고 완료 후 종료된다. “DB가 준비될 때까지 대기”, “마이그레이션 실행”, “설정 파일 다운로드” 같은 초기화 작업에 쓰인다.

Deployment vs StatefulSet — 상태가 있는가 없는가

Pod를 직접 생성하지 않는다. Deployment나 StatefulSet이 Pod를 관리한다.

Deployment는 Stateless 워크로드를 위한 컨트롤러다. 세 개의 Pod는 서로 교체 가능하다. 어느 Pod가 죽어도 새 Pod가 같은 역할을 이어받는다. 이름은 랜덤 해시로 붙는다.

StatefulSet은 Stateful 워크로드를 위한 컨트롤러다. 각 Pod는 고유한 ID(postgres-0, postgres-1, postgres-2)를 가지고, 각자 독립적인 PVC를 할당받는다. 스케일 업은 순서대로(0 → 1 → 2), 스케일 다운은 역순으로(2 → 1)일어난다.

Deployment:                    StatefulSet:
[Pod-abc] [Pod-def] [Pod-ghi]  [postgres-0] [postgres-1] [postgres-2]
     ↓                               ↓            ↓            ↓
 공유 PVC 또는 없음               PVC-0        PVC-1        PVC-2

데이터베이스에 StatefulSet을 쓰는 것이 옳지만, 실무에서는 RDS·Cloud SQL 같은 매니지드 서비스에 위임하고 K8s에서는 Stateless 워크로드만 운영하는 패턴이 더 흔하다.

트레이드오프

Kubernetes는 복잡도를 자동화로 교환한다. docker-compose up 한 줄이 kubectl apply -f k8s/ 에 해당하지만, 그 뒤에는 자동 복구·롤링 업데이트·HPA가 붙어온다. 반대로 소규모 팀·단순 앱·낮은 트래픽 환경에서 K8s의 운영 오버헤드는 이득보다 크다. “클러스터가 필요한 문제가 있는가”가 먼저다.

마이그레이션 — 변환이 아니라 아키텍처 전환

docker-compose.yml을 K8s YAML로 바꾸는 것은 기계적 작업이다. Kompose 같은 도구가 자동으로 변환해주기도 한다. 그러나 진짜 마이그레이션은 다음 세 가지를 요구한다.

첫째, 환경변수 분리. docker-compose의 environment 블록을 그대로 Deployment에 옮기는 것은 작동하지만 잘못된 패턴이다. 공개 설정은 ConfigMap으로, 패스워드·API 키는 Secret으로 분리한다.

둘째, Health Probe 추가. Docker의 healthcheck는 단일 상태를 확인한다. Kubernetes는 세 가지를 구분한다 — startupProbe(시작 완료 여부), livenessProbe(재시작 필요 여부), readinessProbe(트래픽 수신 가능 여부). 셋 다 설정해야 롤링 업데이트가 안전하게 동작한다.

셋째, 전환 전략 선택. 전면 전환(Big Bang)은 빠르지만 위험하다. 서비스 단위로 하나씩 옮기는 점진적 전환(Strangler Fig)은 느리지만 롤백이 쉽다. Canary 배포는 새 버전에 10%의 트래픽만 먼저 보내며 검증한다.

정리

  • Docker의 service 하나는 K8s에서 Deployment + Service로 분리된다. 분리는 복잡함이 아니라 클러스터 수준 자동화를 위한 구조다.
  • Pod는 컨테이너의 묶음이다. Sidecar·Init Container 패턴은 이 구조에서 자연스럽게 나온다.
  • Stateless → Deployment, Stateful → StatefulSet. 데이터베이스는 가능하면 매니지드 서비스에 위임하라.
  • 마이그레이션은 YAML 변환이 아니다. ConfigMap/Secret 분리, Health Probe 추가, 전환 전략 선택이 실질적인 작업이다.

다음 글에서는 K8s 네트워킹의 핵심 — Service·Ingress·DNS가 클러스터 안팎의 트래픽을 어떻게 라우팅하는지 추적한다.