새 버전을 띄우는 동안 옛 버전이 트래픽을 받는 찰나를 누가 만드는가

한 팀이 금요일 오후에 새 버전을 배포했다. 컨테이너를 5개 돌리던 서비스였다. 가장 단순한 방법은 옛 컨테이너 5개를 전부 죽이고 새 5개를 띄우는 것이다 — 하지만 그러면 배포 중 몇 분간 서비스가 완전히 죽는다. 그래서 그들은 컨테이너를 하나씩만 바꿨다: 옛 것 1개를 죽이고 새 것 1개를 띄우길 5번 반복. 이 사이에 "옛 버전과 새 버전이 동시에 돌면서 각자 트래픽을 받는" 찰나가 생긴다. 문제는 — 이 찰나를 손으로 관리하면 어느 순간 꼬인다는 것이다.

이 글이 푸는 것은: Kubernetes의 Deployment가 이 "점진적 교체"를 어떻게 자동화하고, 왜 그것이 replicas 선언 하나로 가능한가다.

Deployment는 Pod를 직접 만들지 않는다 — ReplicaSet을 거친다

가장 먼저 정리할 오해: Deployment는 Pod를 직접 관리하지 않는다. 사이에 ReplicaSet이라는 한 겹이 있다. Deployment가 하는 일은 ReplicaSet을 만드는 것이고, ReplicaSet이 Pod를 만든다.

flowchart TD
    D["Deployment<br/>(배포 전략을 안다)"] -->|"관리"| RS1["ReplicaSet v1<br/>(nginx:1.26)"]
    D -->|"관리"| RS2["ReplicaSet v2<br/>(nginx:1.27)"]
    RS1 --> P1["Pod × N"]
    RS2 --> P2["Pod × M"]

왜 이렇게 두 겹으로? 롤아웃(rollout) 때문이다. 새 버전으로 갈아탈 때, Deployment는 옛 ReplicaSet을 바로 지우지 않고 새 ReplicaSet을 하나 더 만든다. 그리고 옛 RS의 replica를 줄이고 새 RS의 replica를 늘리는 식으로 Pod들을 점진적으로 교체한다. 옛 ReplicaSet이 살아 있기에 롤백이 빠르다 — replica만 다시 늘리면 옛 버전이 즉시 돌아온다.

# Kubernetes 1.36 — Deployment가 만든 ReplicaSet 보기
kubectl get deploy,rs -l app=nginx

확인할 것: Deployment 하나에 ReplicaSet이 여러 개(현재 + 과거 버전) 있을 수 있다. DESIRED가 0인 건 과거 버전(롤백 대비용).

NAME                          DESIRED   CURRENT   READY   AGE
replicaset.apps/nginx-xxxx1   0         0         0       30m   # 옛 버전
replicaset.apps/nginx-xxxx2   3         3         3       2m    # 현재 버전

선언 하나가 점진적 교체를 만드는 과정

# Kubernetes 1.36
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx
spec:
  replicas: 3
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxUnavailable: 1       # 동시에 사용 불가능해도 되는 최대 수
      maxSurge: 1             # 의도한 수를 초과해 만들어도 되는 최대 수
  selector:
    matchLabels: {app: nginx}
  template:
    metadata:
      labels: {app: nginx}
    spec:
      containers:
      - name: nginx
        image: nginx:1.27

이미지를 nginx:1.26에서 nginx:1.27로 바꾸고 kubectl apply하면 무슨 일이 일어나는가? Pod 템플릿이 바뀌었으므로 Deployment는 새 ReplicaSet(1.27)을 만들고, 두 개의 제약 안에서 Pod들을 옮긴다:

  • maxSurge: 1 → 의도한 3개보다 최대 1개 더(즉 최대 4개)까지 만들어도 된다.
  • maxUnavailable: 1 → 동시에 최대 1개까지는 사용 불가능해도 된다.
flowchart LR
    S0["시작: RS-v1 에 3개<br/>(1.26)"] --> S1["새 RS-v2 1개 추가<br/>(1.27) → 총 4개 (surge)"]
    S1 --> S2["RS-v1 1개 감소 → 총 3개<br/>(2 옛 + 1 새)"]
    S2 --> S3["RS-v2 1개 추가 → 총 4개"]
    S3 --> S4["RS-v1 1개 감소 → 3개<br/>(1 옛 + 2 새)"]
    S4 --> S5["... 반복 ..."]
    S5 --> S6["종료: RS-v2 에 3개<br/>RS-v1 은 0 (삭제 아님)"]

핵심 통찰: Deployment는 직접 "이 Pod를 죽여라" 명령을 내리지 않는다. 단지 "이 ReplicaSet은 replica 0, 저 ReplicaSet은 replica 3"이라는 원하는 상태를 설정할 뿐이다. 각 ReplicaSet이 자기 조정 루프(03장)로 Pod를 만들고 지운다. 그래서 배포 과정 전체가 선언적 모델의 연장이다.

maxUnavailable과 maxSurge — 속도와 가용성의 트레이드오프

이 두 값이 롤아웃의 성격을 결정한다. 같은 replicas: 3에서:

설정 의미 효과
maxUnavailable: 0, maxSurge: 1 먼저 새 것 1개 띄우고(총4), 그 후 옛 것 1개 죽임 항상 3개 이상 가용. 가장 안전하지만 새 자원을 잠깐 더 씀
maxUnavailable: 1, maxSurge: 0 먼저 옛 것 1개 죽이고(총2), 그 후 새 것 1개 띄움 일시적으로 2개만 가용. 자원은 안 늘지만 가용성 일시 하락
maxUnavailable: 1, maxSurge: 1 (기본) 새 것 늘리기/옛 것 줄이기를 겹침 속도와 자원의 절충
maxUnavailable: 100%, maxSurge: 100% 한 번에 전부 교체 빠르지만 다운타임/과부하 위험

실무에서 가장 흔한 선택은 "가용성 우선" — maxUnavailable: 0. 단 자원이 충분할 때. 메모리 빠듯한 클러스터에선 surge가 스케줄 실패로 이어지니 maxUnavailable > 0을 택하기도 한다. 이 선택은 비용(자원)과 안전(가용성)의 트레이드오프이지 정답이 없다.

readinessProbe가 롤아웃의 브레이크다

점진적 교체가 "안전한" 이유는 새 Pod가 준비됐는지 확인하는 절차가 있기 때문이다. 그것이 readinessProbe다.

readinessProbe:
  httpGet: {path: /healthz, port: 8080}
  initialDelaySeconds: 2
  periodSeconds: 3

롤아웃 중 새 Pod가 뜨면, Deployment(정확히는 ReplicaSet 컨트롤러가 보는 ready 조건)은 readinessProbe가 통과하기를 기다린다. 통과해야 READY가 되고, 그래야 다음 Pod 교체 단계로 넘어간다. readinessProbe가 없으면 "컨테이너가 시작됐다"는 것만으로 ready로 간주돼, 아직 요청을 받을 준비가 안 된 Pod로 트래픽이 가는 사태가 벌어진다.

# 롤아웃 진행 상황 실시간 추적
kubectl rollout status deployment/nginx
Waiting for rollout to finish: 2 out of 3 new replicas have been updated...
deployment "nginx" successfully rolled out

readinessProbe를 빼먹으면 "배포 직후 502 폭발"이라는 고전적 장애가 생긴다 — 컨테이너는 떴지만 앱이 아직 준비 안 됐는데 Service(네트워킹 영역)가 트래픽을 보내기 때문. readinessProbe는 부가 기능이 아니라 무중단 배포의 필수 부품이다.

롤아웃 실패와 롤백 — 그래서 옛 ReplicaSet을 지우지 않는다

롤아웃이 실패하는 경우: 새 이미지가 크래시 루프에 빠지거나, readinessProbe가 영영 통과하지 않으면, Deployment는 ProgressDeadlineSeconds(기본 600초)를 넘겨 Progressing=False로 표시한다. 이때 롤백은 옛 ReplicaSet의 replica를 다시 3으로 올리는 것이다. 옛 RS가 살아 있기에 몇 초 만에 옛 버전으로 돌아간다.

# 롤백 — 옛 ReplicaSet으로 되돌림
kubectl rollout undo deployment/nginx
# 특정 리비전으로
kubectl rollout undo deployment/nginx --to-revision=2
# 롤아웃 이력
kubectl rollout history deployment/nginx

확인할 것: undo 직후 새 Pod(옛 이미지)들이 다시 생겨난다.

Deployment는 revision 히스토리를 revisionHistoryLimit(기본 10)만큼 보존한다. 너무 크면 etcd에 쌓이고, 너무 작으면 옛 버전으로 못 돌아간다. 상황에 맞게 조정.

Recreate 전략 — 점진적 교체가 싫을 때

RollingUpdate 외에 Recreate 전략이 있다. 이건 옛 Pod를 전부 먼저 죽인 뒤 새 Pod를 띄운다 — 즉 다운타임이 생긴다. 언제 쓰나? 단일 인스턴스로만 돌아가는 앱(예: 데이터 파일을 단독 잠그는 레거시), 또는 새/옛 버전이 동시에 돌면 안 되는(예: incompatible DB 스키마) 경우. 보통은 RollingUpdate가 기본이고 Recreate는 의도적 선택이다.

Deployment vs 직접 Pod — 왜 Pod를 직접 만들면 안 되나

처음엔 kind: Pod로 직접 Pod를 만들어 실험하곤 한다. 문제는 — Pod가 죽었을 때 아무도 다시 안 만든다는 것이다. 직접 만든 Pod는 자가 복구가 없다. Deployment(를 거쳐 ReplicaSet)가 만든 Pod만이, 죽으면 새 Pod로 교체된다.

그래서 실무에서는 거의 항상 Deployment로 배포한다. Pod를 직접 만드는 건 일회성 디버깅(kubectl run)이나 튜토리얼 한정이다. 이것도 "Pod는 재스케줄되지 않는다"(04장)는 사실의 연장선이다.

직접 확인하기

# Kubernetes 1.36 — Deployment 배포 후 이미지 변경으로 롤아웃 유발
kubectl create deploy nginx --image=nginx:1.26 --replicas=3
kubectl set image deploy/nginx nginx=nginx:1.27
kubectl rollout status deploy/nginx
Waiting for rollout to finish: 1 out of 3...
deployment "nginx" successfully rolled out
# 옛 ReplicaSet이 살아있는지 (replica 0)
kubectl get rs -l app=nginx

확인할 것: 두 ReplicaSet이 보인다. 하나는 DESIRED 0(옛), 하나는 3(현재).

# 의도적 롤백
kubectl rollout undo deploy/nginx
kubectl get rs -l app=nginx

확인할 것: undo 후 replica가 뒤바뀐다 — 옛 RS가 다시 3, 새 RS가 0으로.

흔히 묻는 것, 흔히 틀리는 것

오해 정정
"Deployment가 Pod를 만든다" ReplicaSet이 Pod를 만든다. Deployment는 ReplicaSet을 만든다
"롤아웃은 항상 무중단이다" readinessProbe가 있어야. 없으면 준비 안 된 Pod로 트래픽
"maxSurge, maxUnavailable은 Pod 수다" 비율로도 가능(25%). 의도한 replica 대비 비율
"옛 ReplicaSet은 배포 후 삭제된다" replica 0으로 남는다(롤백용). revisionHistoryLimit까지만
"롤백은 옛 이미지를 다시 빌드한다" 아니다. 옛 ReplicaSet(옛 이미지 템플릿)의 replica를 올리는 것뿐
"Recreate가 빠르니까 더 낫다" 다운타임이 생긴다. 가용성이 중요하면 RollingUpdate

요약 — 이 글의 결론

  • Deployment는 Pod를 직접 만들지 않는다. ReplicaSet이라는 한 겹을 두며, 이 한 겹이 롤아웃·롤백을 가능하게 한다 — 새 ReplicaSet을 만들고 옛 것의 replica를 줄이는 방식.
  • 점진적 교체는 선언 하나로 일어난다. maxUnavailable/maxSurge 두 제약 안에서 새 Pod를 늘리고 옛 Pod를 줄인다. 가용성 vs 자원의 트레이드오프를 이 두 값이 조율한다.
  • readinessProbe가 무중단의 진짜 브레이크다. 새 Pod가 준비됐는지 확인해야 다음 단계로. 빠뜨리면 "배포 직후 502" 장애.
  • 롤백이 빠른 이유는 옛 ReplicaSet을 지우지 않기 때문. replica만 다시 올리면 된다. revisionHistoryLimit까지만 보존.
  • 직접 Pod를 만들면 자가 복구가 없다. 실무 배포는 거의 항상 Deployment. "Pod는 재스케줄되지 않는다"(04장)와 같은 맥락.
  • Recreate는 의도적 다운타임 허용 전략 — 단일 인스턴스 전용 앱, 호환 안 되는 스키마 교체 등에만.

생각해 볼 문제

  1. maxUnavailable: 0이면 surge 없이는 롤아웃이 가능한가? 왜?
  2. readinessProbe 없는 Deployment에서 배포 직후 502가 발생하는 경로를 추적하라. (Service도 함께 생각)
  3. 옛 ReplicaSet을 즉시 지우게 만들면 어떤 운영 능력을 잃는가?
  4. replicas: 3인 Deployment의 롤아웃 중 한 노드가 죽었다. Deployment는 이걸 어떻게 처리하나?
  5. Recreate 전략을 선택해야 하는 구체적 시나리오를 하나 설계해 보라.
  6. Deployment가 ReplicaSet을 거친다는 설계에서, 왜 사용자는 ReplicaSet을 직접 만들지 않는가? (관리 책임의 소재)

참고

'Tech Artifacts > Kubernetes' 카테고리의 다른 글

Kubernetes - 07. daemonset job cronjob  (0) 2026.07.13
Kubernetes - 06. statefulset  (0) 2026.07.13
Kubernetes - 04. pod  (0) 2026.07.13
Kubernetes - 03. api machinery  (0) 2026.07.13
Kubernetes - 02. architecture  (1) 2026.07.13