Docker Compose에서 Kubernetes로 — 컨테이너 오케스트레이션 전환 가이드
단일 서버에서는 Compose로 충분했지만, 서버가 여러 대가 되자 배포·복구·스케일링이 모두 수동 작업이 됩니다. Docker Swarm으로 멀티노드를 시도해보면 선언적 배포까지는 해결되지만, 롤링 업데이트·자동 스케일링·에코시스템 도구 부재로 결국 한계에 부딪힙니다. 장애가 나면 어느 노드에서 무엇이 죽었는지 찾는 데만 시간이 소모되고, 롤백도 일관되지 않습니다. 이 시점부터는 "컨테이너 실행"보다 "원하는 상태를 자동 유지"하는 오케스트레이션 사고가 필요합니다. 이 모듈은 Docker 경험을 Kubernetes 운영 개념으로 자연스럽게 연결합니다.
이 모듈의 위치: Docker Compose까지 배웠다면 "서버가 여러 대면 어떻게 하나?"라는 질문이 생깁니다. 그 답이 오케스트레이션입니다. 현업 표준은 Kubernetes(K8s)입니다. 이 모듈은 Docker 지식을 K8s 개념에 연결하는 다리 역할을 합니다.
Docker를 알면 Kubernetes의 절반은 이미 이해한 겁니다. 컨테이너 이미지, 포트 바인딩, 볼륨, 환경변수 — 모두 K8s에서 같은 개념이 다른 문법으로 존재합니다. 낯선 용어들을 Docker 용어에 대응시키면서 배우면 훨씬 빠릅니다.
- 1Compose의 한계와 멀티 노드 현실을 통해 오케스트레이션의 필요성을 설명할 수 있다
- 2Docker 개념을 Kubernetes 개념(run→Pod, Compose→Deployment)의 유사점과 차이로 연결할 수 있다
- 3kubectl 기본 명령을 docker CLI와 비교하며 사용할 수 있다
- 4YAML 작성부터 외부 노출까지 첫 번째 Deployment를 배포할 수 있다
- 5Kubernetes가 무중단 배포를 처리하는 방식으로 롤링 업데이트와 롤백을 수행할 수 있다
minikube는 노트북에서 단일 노드 Kubernetes를 실행하는 도구입니다. 실제 클러스터 없이 이 모듈의 모든 실습을 진행할 수 있습니다.
minikube versionminikube startkubectl version --clientkubectl cluster-infoDocker Desktop의 Settings → Kubernetes → Enable Kubernetes로 로컬 클러스터 활성화 가능합니다
왜 Compose로는 부족한가 — 오케스트레이션의 필요성
Docker Compose로 운영 중인 서버가 다운됐습니다. 모든 컨테이너가 중단됩니다. 수동으로 서버를 복구하고 docker-compose up을 다시 실행해야 합니다. 트래픽이 갑자기 2배가 됐습니다. 컨테이너를 더 실행하려면 또 다른 서버에 SSH로 접속해서 수동으로 docker run을 해야 합니다. 배포할 때마다 잠깐 서비스가 중단됩니다. Compose는 단일 서버에서 컨테이너를 편하게 관리하는 도구지, 여러 서버를 자동으로 관리하는 도구가 아닙니다.
확대
Compose의 한계
Docker Compose가 해결하는 것:
✓ 로컬 개발 환경 멀티 컨테이너 관리
✓ 단일 서버에서 서비스 간 네트워크
✓ 볼륨, 환경변수, 의존성 관리
Docker Compose가 해결하지 못하는 것:
✗ 여러 서버(노드)에 컨테이너 분산
✗ 노드 장애 시 자동 복구
✗ 트래픽에 따른 자동 스케일링
✗ 무중단 롤링 업데이트
✗ 서비스 간 부하분산
Kubernetes가 해결하는 것
Kubernetes(K8s)는 컨테이너 오케스트레이션 플랫폼입니다. 여러 서버를 하나의 클러스터로 추상화하고, 컨테이너의 배포·스케일링·복구를 자동화합니다.
확대
Docker → Kubernetes 개념 매핑
K8s를 처음 배울 때 가장 힘든 것은 새로운 용어들입니다. Pod, Deployment, Service, ConfigMap, Ingress — 낯선 이름들이 쏟아집니다. Docker와 비슷한 역할을 하는 개념이 있지만 수명주기·네트워크·스케줄링 모델은 다릅니다. 매핑은 출발점으로만 쓰고 차이를 함께 확인해야 합니다.
확대
핵심 개념 매핑
| Docker / Compose | Kubernetes | 역할 |
|---|---|---|
docker run | Pod | 컨테이너 실행 단위 |
docker-compose.yml service | Deployment(가장 가까운 비교) | 컨테이너 수·이미지·설정 선언 (수명주기는 다름) |
--replicas: 3 | replicas: 3 | 동일 — 복제본 수 |
-p 8080:80 | Service (NodePort/LB) | 외부 포트 노출 |
docker network | Service (ClusterIP) | 컨테이너 간 통신 |
-e KEY=VALUE | env + ConfigMap | 환경변수 |
-e PASSWORD=... | Secret | 민감한 환경변수 |
-v /host:/container | PersistentVolumeClaim | 영구 볼륨 |
healthcheck: | livenessProbe / readinessProbe | 헬스체크 |
docker logs | kubectl logs | 컨테이너 로그 |
docker exec -it | kubectl exec -it | 컨테이너 접속 |
docker ps | kubectl get pods | 실행 중인 컨테이너 목록 |
확대
Pod란?
Pod는 K8s에서 가장 작은 배포 단위입니다. docker run nginx가 컨테이너 하나를 실행하는 것처럼, K8s에서는 Pod 안에 컨테이너가 실행됩니다. 대부분의 경우 Pod 하나 = 컨테이너 하나입니다.
# docker run nginx와 동일한 K8s Pod
apiVersion: v1
kind: Pod
metadata:
name: nginx-pod
spec:
containers:
- name: nginx
image: nginx:1.25-alpine
ports:
- containerPort: 80
Deployment란?
Pod를 직접 만들지 않고 Deployment를 사용합니다. Deployment는 "nginx를 3개 실행하고 항상 3개 상태를 유지하라"는 선언입니다. Pod가 죽으면 자동으로 새 Pod를 만듭니다.
# docker-compose.yml의 service와 대응
# services:
# web:
# image: nginx:1.25-alpine
# deploy:
# replicas: 3
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
spec:
replicas: 3 # Compose의 replicas: 3
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.25-alpine
ports:
- containerPort: 80
선언한 Deployment는 어떻게 '항상 3개'로 유지되나 — apply부터 롤링까지 5단계
kubectl apply -f로 replicas: 3을 선언하면 잠시 뒤 Pod 3개가 뜨고, 하나를 죽여도 곧 다시 3개가 됩니다. 이 "선언하면 알아서 유지된다"는 동작의 정체는 API 서버 저장 → 스케줄러 배정 → kubelet 실행 → 컨트롤러 조정(reconcile) → 롤링 업데이트로 이어지는 제어 루프입니다. Docker Swarm의 매니저·워커·태스크도 이름만 다를 뿐 같은 흐름이며, 이 단계를 알면 "Pod가 왜 안 뜨지", "왜 여기서 멈추지"를 단계로 좁혀 진단할 수 있습니다.
[사용자] kubectl apply -f deployment.yaml (replicas: 3)
│
① 선언 저장 — API 서버가 '원하는 상태'를 etcd에 기록 (desired = 3)
│
② 스케줄 — 스케줄러가 각 Pod를 배치할 노드를 고른다 (CPU·메모리·taint 고려)
│
③ 실행 — 대상 노드의 kubelet이 컨테이너(Pod)를 띄운다 (이미지 pull → start)
│
④ 상태 조정 — 컨트롤러가 desired(3) vs current를 계속 비교 (죽으면 새로 만들고 재배치)
│ → 이 reconcile 루프가 'self-healing'의 정체
│
⑤ 롤링 업데이트 — 이미지 바꾸면 새 ReplicaSet을 조금씩 교체 (old 축소·new 확대, 무중단)
▼
[클러스터] READY 3/3 유지 — 선언과 실제가 같아질 때까지 루프가 계속 돈다
각 단계에서 무슨 일이 일어나고, 막히면 어떤 증상인가:
| 단계 | 하는 일 | 여기서 막히면 |
|---|---|---|
| ① 선언 저장 | kubectl apply가 매니페스트를 API 서버로 보내고 desired state가 etcd에 저장된다 | YAML 문법·필드 오류면 apply 자체가 거부(error validating data) — 클러스터엔 아무 것도 안 뜸 |
| ② 스케줄 | 스케줄러가 requests·taint·노드 여유를 보고 Pod를 노드에 배정 | 배치할 노드가 없으면 Pod가 Pending — describe pod의 Events에 Insufficient cpu/memory |
| ③ 실행 | 배정된 노드의 kubelet이 이미지를 pull하고 컨테이너를 start | 이미지 오타·프라이빗 접근 실패는 ImagePullBackOff, 앱이 바로 죽으면 CrashLoopBackOff |
| ④ 상태 조정 | 컨트롤러가 desired와 current를 끊임없이 비교해 부족하면 새 Pod 생성·재배치 | replica가 1이면 그 Pod 재시작 동안 서비스 다운 — 가용성 원하면 최소 2 |
| ⑤ 롤링 업데이트 | 이미지 교체 시 새 ReplicaSet을 점진 확대·구버전 축소로 무중단 배포, 실패 시 rollout undo로 롤백 | 새 Pod가 readiness를 못 넘으면 롤아웃이 멈춤(rollout status가 진행 중에서 정체) |
즉 Kubernetes(와 Swarm)의 핵심은 "컨테이너를 실행한다"가 아니라 **"선언한 상태와 실제 상태의 차이를 컨트롤러가 계속 0으로 만든다"**는 것입니다 — Pod가 죽어도 다시 뜨는 이유가 ④의 reconcile 루프입니다. 접속·배포가 안 될 때 이 다섯 단계 중 어디서 끊겼는지를 kubectl get pods(상태)와 kubectl describe pod(Events)로 좁힙니다 — Pending이면 ②(배치할 곳 없음), CrashLoopBackOff면 ③(앱이 못 산다), 롤아웃 정체면 ⑤(새 버전이 준비 안 됨)입니다.
minikube 환경에서 nginx를 3개 복제본으로 배포하고, Service로 외부에 노출합니다.
# nginx-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
labels:
app: nginx
spec:
replicas: 3
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.25-alpine
ports:
- containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
name: nginx-service
spec:
type: NodePort
selector:
app: nginx
ports:
- port: 80
targetPort: 80
nodePort: 30080
# Deployment와 Service 동시 배포
kubectl apply -f nginx-deployment.yaml
# 배포 상태 확인
kubectl get pods
kubectl get deployment nginx-deployment
kubectl get service nginx-service
# minikube에서 외부 접근 URL 확인
minikube service nginx-service --url
kubectl apply -f nginx-deployment.yaml- REPLICAS 컬럼 먼저: kubectl get deployment nginx-deployment 에서 READY 컬럼이 3/3인지 확인. 0/3이나 1/3이면 아직 초기화 중 — 30초 후 재확인. 계속 0/3이면 kubectl describe pod 로 Events 섹션 확인
- Pod 수 기준: kubectl get pods 에서 nginx-deployment-* 이름의 Pod가 정확히 3개이고 모두 STATUS=Running 이어야 정상. 2개 이하면 스케줄러가 리소스 부족으로 일부 Pod 배치 실패한 것 (Pending 상태 Pod 확인)
- READY=3/3이고 curl 응답 200: Deployment + Service가 정상 동작. READY=3/3인데 curl 타임아웃이면 Service selector(app: nginx)와 Pod labels(app: nginx)가 불일치한 것 — kubectl get pods --show-labels 로 비교
트러블슈팅
증상
Deployment를 배포했는데 Pod가 Running이 되지 않고 계속 Pending 상태입니다.
kubectl get pods
# NAME READY STATUS RESTARTS AGE
# nginx-deployment-7d9c4bbfd-xk2p9 0/1 Pending 0 2m
kubectl describe pod nginx-deployment-7d9c4bbfd-xk2p9
# Events:
# Warning FailedScheduling 2m default-scheduler
# 0/1 nodes are available: 1 Insufficient memory.
원인 진단
# 노드 리소스 현황 확인
kubectl describe nodes | grep -A 5 "Allocated resources"
# Allocated resources:
# Resource Requests Limits
# cpu 950m (47%) 2 (100%)
# memory 1900Mi (95%) 2000Mi (99%)
# → 메모리가 거의 꽉 찬 상태
# Pod의 리소스 요청량 확인
kubectl get pod nginx-deployment-7d9c4bbfd-xk2p9 -o yaml | grep -A 10 resources
해결
# 방법 1: 다른 Pod 삭제하여 리소스 확보
kubectl get pods --all-namespaces | grep -v Running
# 불필요한 Pod 삭제
# 방법 2: Deployment에 resources.requests 조정
kubectl edit deployment nginx-deployment
# resources:
# requests:
# memory: "64Mi" ← 낮춤
# cpu: "100m"
# 방법 3: minikube 메모리 늘려서 재시작
minikube stop
minikube start --memory=4096
증상
kubectl apply 직후에는 Running이었다가 금방 Error → CrashLoopBackOff로 바뀝니다.
kubectl get pods
# NAME READY STATUS RESTARTS AGE
# myapp-deployment-abc123-x7k2p 0/1 CrashLoopBackOff 5 3m
# 재시작 횟수가 계속 늘어남
kubectl get pods -w
# myapp-.. 0/1 Error 3 90s
# myapp-.. 0/1 CrashLoopBackOff 3 95s
# myapp-.. 0/1 Running 4 110s
# myapp-.. 0/1 Error 4 115s
원인 진단
# 컨테이너 로그 확인 — docker logs와 동일한 역할
kubectl logs myapp-deployment-abc123-x7k2p
# Error: Cannot find module '/app/server.js' ← 이런 에러 메시지 확인
# 이전 충돌 컨테이너의 로그 확인
kubectl logs myapp-deployment-abc123-x7k2p --previous
# Pod 상세 이벤트 확인
kubectl describe pod myapp-deployment-abc123-x7k2p
# Events:
# Warning BackOff 2m kubelet Back-off restarting failed container
주요 원인별 해결
# 원인 1: 이미지 내 실행 파일 경로 오류 → Dockerfile ENTRYPOINT/CMD 확인
# 원인 2: 환경변수 누락 → kubectl get pod -o yaml 로 env 확인
# 원인 3: ConfigMap/Secret 마운트 실패 → kubectl describe pod Events 확인
# 임시 디버깅: 같은 이미지로 셸 접속
kubectl run debug --image=myapp:latest --restart=Never -it -- /bin/sh
증상
단일 노드에서는 Pod 간 통신이 잘 됐는데, 멀티 노드 클러스터(또는 Swarm) 환경에서 서로 다른 노드에 뜬 Pod끼리 통신이 안 됩니다.
# Pod A (Node 1)에서 Pod B (Node 2)로 ping/curl 실패
kubectl exec -it pod-a -- curl http://pod-b-service
# curl: (7) Failed to connect to pod-b-service port 80: Connection timed out
원인 진단
# 노드 간 오버레이 네트워크 포트 확인 (방화벽 문제)
# Kubernetes(Flannel/Calico): UDP 8472, UDP 4789 필요
# Kubernetes API: TCP 6443
# Swarm: TCP 2377, TCP/UDP 7946, UDP 4789
# 방화벽 상태 확인
sudo firewall-cmd --list-all # CentOS/RHEL
sudo ufw status # Ubuntu
해결
# Ubuntu (ufw) — 오버레이 네트워크 포트 허용
sudo ufw allow 6443/tcp # K8s API Server
sudo ufw allow 8472/udp # Flannel VXLAN
sudo ufw allow 4789/udp # Overlay network
sudo ufw allow 7946/tcp # Calico BGP / Swarm gossip
sudo ufw allow 7946/udp
# 설정 후 Pod 간 통신 재확인
kubectl exec -it pod-a -- curl http://pod-b-service
심화 — HEALTHCHECK 하나가 K8s에서 셋으로 갈라진다
심화: liveness·readiness·startup probe — HEALTHCHECK 하나가 셋으로 갈라지는 이유
Docker에는 HEALTHCHECK 하나뿐입니다. 그런데 K8s는 같은 "헬스체크"를 목적이 다른 세 probe로 쪼갭니다. 이 분리를 모르면 앞의 매핑 표에서 healthcheck → liveness/readiness를 봤어도, 정작 운영에서 엉뚱한 probe에 엉뚱한 검사를 넣어 사고를 냅니다.
- livenessProbe = "죽었나?": 실패하면 kubelet이 컨테이너를 kill 후 재시작합니다. 프로세스가 데드락에 빠져 스스로 못 살아나는 걸 복구하는 용도입니다. 여기에 DB 연결 같은 의존성 체크를 넣으면, DB가 잠깐 느릴 때 멀쩡한 앱까지 재시작됩니다.
- readinessProbe = "지금 트래픽 받아도 되나?": 실패해도 컨테이너는 살려두고 Service 엔드포인트에서만 빼 트래픽을 끊습니다. 워밍업 중이거나 의존성이 아직 안 붙었을 때 "재시작 말고 잠깐 트래픽만 보류"가 필요한 곳이 여기입니다.
- startupProbe = "다 떴나?": 초기화가 느린 앱(대형 JVM 등)을 위해, 이게 성공할 때까지 liveness·readiness를 미뤄줍니다. 없으면 느린 부팅을 liveness가 "죽었다"고 오판해 부팅 도중 계속 죽입니다.
내부 동작으로 보면 readiness는 엔드포인트 컨트롤러가 Service의 대상 목록을 갱신하는 신호이고, liveness는 kubelet이 컨테이너를 재시작하는 신호로, 완전히 다른 주체가 반응합니다. 그래서 원칙은 하나입니다 — liveness는 프로세스 생존만 가볍게, 의존성·준비 상태는 readiness로, 느린 초기화는 startupProbe로. Docker의 단일 HEALTHCHECK 감각으로 liveness에 무거운 검사를 몰아넣는 것이 K8s 전환 초기의 가장 흔한 자해입니다.
상황
새 서비스를 올린 뒤 응답도 정상이고 사용자 오류도 없는데, kubectl get pods의 RESTARTS 숫자가 시간마다 1씩 늘어납니다. 트래픽이 몰리는 시간대에 재시작이 더 잦습니다.
원인
앱이 죽은 게 아니라 livenessProbe가 멀쩡한 앱을 죽이고 있습니다. timeoutSeconds가 1초처럼 너무 짧거나 failureThreshold가 낮으면, GC 정지·순간 부하로 probe 응답이 몇백 ms만 늦어도 실패로 집계됩니다. 게다가 liveness 경로에 DB·캐시 같은 의존성 확인을 넣어뒀다면, 의존성이 잠깐 느릴 때 앱까지 재시작 대상이 됩니다.
진단
# Events에 Liveness probe failed / Killing 이 반복되는지
kubectl describe pod myapp-deployment-abc123-x7k2p | grep -A2 -i "liveness\|killing"
# 같은 Pod의 직전 로그에 앱 크래시가 없는지 (없으면 probe가 원인)
kubectl logs myapp-deployment-abc123-x7k2p --previous
# probe 정의와 임계값 확인
kubectl get deploy myapp-deployment -o yaml | grep -A8 livenessProbe
로그는 멀쩡한데 재시작만 반복되면 probe가 원인이라는 확정 신호입니다.
해결
liveness는 프로세스가 응답만 하는지 보는 가벼운 경로(예: /healthz가 즉시 200)로 바꾸고 timeoutSeconds·failureThreshold를 현실적으로 키웁니다. 느린 초기화는 startupProbe로 감싸 부팅 동안 liveness를 미룹니다. DB 등 의존성 준비 여부는 liveness가 아니라 readinessProbe로 옮겨, 준비가 안 됐을 땐 재시작 대신 트래픽만 잠깐 끊기게 합니다. Docker의 HEALTHCHECK 하나를 K8s의 세 probe로 올바르게 나누는 것이 핵심입니다.
"Compose로 충분한가" — 팀이 Kubernetes로 전환하는 실제 기준점
스타트업 초기에는 EC2 한 대에 docker-compose로 운영합니다. 빠르고 단순합니다. 그러다 어느 순간 이런 상황이 옵니다: 새벽 3시에 서버가 멈추고, 자동 복구가 없어서 수동으로 SSH 접속해서 docker-compose up을 칩니다. 다음 날 오전 트래픽 피크 때 컨테이너를 더 늘려야 하는데 새 서버에 일일이 접속해서 설정합니다. 이 지점이 오케스트레이션을 고민해야 할 때입니다.
팀 규모별 현실적 판단:
1~3명 팀, 단일 서버:
→ docker-compose + systemd restart=always 로 충분
→ K8s 도입하면 운영 오버헤드가 더 큼
4~10명 팀, 서버 3대 이상:
→ Kubernetes 도입 고려 시작
→ EKS/GKE 같은 매니지드 서비스가 현실적
10명+ 팀, MSA:
→ K8s는 선택이 아닌 필수
→ Helm, ArgoCD, Istio 등 생태계 전체 활용
실무에서 K8s를 배워야 하는 이유: 대부분의 중소기업 이상 회사의 인프라는 이미 K8s로 전환됐거나 전환 중입니다. 채용 공고에서 "K8s 운영 경험"이 필수 요건으로 등장하는 비율이 빠르게 늘고 있습니다. Docker를 잘 안다면 K8s 진입 장벽은 생각보다 낮습니다 — 이 모듈에서 배운 개념 매핑이 그 다리 역할을 합니다.
명령어·단축키 빠른 참조
이 모듈에서 Docker 개념과 대응시켜 다룬 kubectl·minikube 명령을 실전 예와 함께 모았습니다. "용도"의 괄호는 대응되는 Docker 명령입니다.
| 명령어/단축키 | 용도 | 자주 쓰는 예 |
|---|---|---|
kubectl apply -f | YAML로 Deployment·Service 배포(선언적) | kubectl apply -f nginx-deployment.yaml |
kubectl get pods | 실행 중 Pod 목록(= docker ps) | kubectl get pods -w (상태 변화 감시) · --show-labels |
kubectl get deployment | 복제본 READY 상태 확인 | kubectl get deployment nginx-deployment → READY 3/3 |
kubectl get service | 서비스·노출 포트 확인 | kubectl get service nginx-service |
kubectl logs | 컨테이너 로그(= docker logs) | kubectl logs <pod> --previous (직전 크래시 로그) |
kubectl describe pod | Pending·CrashLoop 원인 Events 확인 | kubectl describe pod <pod> → Events의 FailedScheduling·BackOff |
kubectl exec -it | 컨테이너 접속(= docker exec -it) | kubectl exec -it pod-a -- curl http://pod-b-service |
kubectl rollout undo | 이전 버전으로 즉시 롤백 | kubectl rollout undo deployment/<name> · --to-revision=2 |
kubectl edit deployment | 실행 중 Deployment 설정 수정 | kubectl edit deployment nginx-deployment (requests 조정) |
kubectl run | 임시 디버그 Pod 실행 | kubectl run debug --image=myapp:latest --restart=Never -it -- /bin/sh |
kubectl describe nodes | 노드 리소스 할당 현황 확인 | kubectl describe nodes | grep -A5 "Allocated resources" |
minikube service --url | 로컬 클러스터 외부 접근 URL | minikube service nginx-service --url |
관련 모듈로 더 깊이:
- 복잡한 멀티 서비스 환경의 유기적 연동과 배포 — 오케스트레이션으로 넘어가기 전 단일 서버 다중 컨테이너 선언의 출발점
- 무중단 배포와 스케일아웃을 위한 실전 컨테이너 아키텍처 — 단일 서버 운영의 한계를 메우는 프로덕션 운영 패턴과 안정화 기법
다음 모듈에서는 Kubernetes 트랙에서 실제 클러스터 운영, Helm 패키지 관리, 그리고 서비스 메시(Istio)를 심층적으로 다룹니다.