Chapter 9

디플로이먼트: 선언적 애플리케이션 업데이트

  • 9.1 앱을 업데이트하는 원시적인 방법
  • 9.2 kubectl rolling-update — RC 시절의 업데이트
  • 9.3 Deployment로 선언적 업데이트
  • 9.4 롤백과 revision 이력
  • 9.5 롤아웃 속도 제어 — maxSurge/maxUnavailable
  • 9.6 나쁜 버전 막기 — minReadySeconds와 readiness probe

9장은 한 마디로 "앱을 무중단으로 업데이트하는 법"에 대한 장이야. 단순히 이미지 태그를 바꾸는 게 아니라, 구버전 Pod를 점진적으로 새 Pod로 교체하고, 문제가 생기면 되돌리고, 나쁜 버전은 아예 배포되지 못하게 막는 것 — 이 모든 걸 선언적으로 해. 이 장을 다 읽고 나면 Deployment가 왜 RC/RS의 진정한 후계자인지가 명확해져.

가장 원시적인 두 가지 방식부터 보자. 첫째 Recreate — 모든 구버전 Pod를 지우고 그 다음에 새 버전을 띄워. 업데이트 중 다운타임이 생기지. 구·신 버전이 동시에 돌면 안 되는 앱에서만 써야 해 (예를 들어 DB 스키마 변경 같은 거). 둘째 수동 Blue-Green — 새 버전 Pod를 N개 띄우고 준비되면 Service의 selector를 한 번에 바꿔. 다운타임은 없지만 리소스가 2배 필요하고, 조작을 사람이 해야 해서 실수 여지가 커. 두 방식의 중간 지점이 Rolling Update야 — 구버전을 하나씩 빼고 신버전을 하나씩 넣어. 다운타임도 없고 리소스 부담도 적어. 9장의 본론은 여기서 시작해.

쿠버네티스 초기에는 kubectl rolling-update 명령으로 RC 기반 롤링 업데이트를 했어. 지금은 obsolete지만, 왜 그런지 이해하는 게 Deployment의 필요성을 알게 해줘. 동작 방식은 이래. 구 RC(kubia-v1)를 놓고 새 RC(kubia-v2)를 옆에 만든 다음, 구 RC의 replica를 1 줄이고 새 RC의 replica를 1 늘리는 식으로 서서히 교체해. 두 RC 둘 다 같은 Service의 label selector에 잡히게 해서 트래픽은 섞여 흘러. 재밌는 디테일이 하나 있어 — 두 RC가 같은 라벨 기준으로 서로의 Pod를 자기 걸로 오해하면 안 되니까, kubectl이 두 RC의 selector와 기존 Pod의 라벨을 몰래 수정해서 구별이 가게 만들어. deployment=<hash> 같은 라벨을 추가하는 식이야. 이게 문제인 이유는 세 가지야. 첫째 kubectl이 클라이언트에서 오케스트레이션해. 명령이 중간에 끊기면 시스템이 어정쩡한 상태로 남아. 둘째 명령형이야. "구 RC 줄여, 새 RC 늘려" 하는 절차를 kubectl이 직접 때려박아. 쿠버네티스의 선언형 철학에 어긋나. 셋째 내 리소스의 라벨과 selector를 kubectl이 내 허락 없이 바꿔. 저자가 "누가 내 컨트롤러 건드렸어?!"라고 외치고 싶었다고 고백할 정도야. 그래서 "desired state만 바꾸면 시스템이 알아서 하는" 방식이 필요해졌고, 그게 Deployment야.

Deployment는 Pod를 직접 관리하지 않아. Deployment → ReplicaSet → Pod의 2단 구조야. Deployment는 버전 개념을 이해하고, 버전마다 ReplicaSet을 하나씩 만들어 관리해. 매니페스트 자체는 RC/RS와 형태가 거의 같지만 업데이트 전략을 가진다는 점이 달라. Deployment 이름에 -v1 같은 버전을 붙이지 않아. Deployment는 버전 위에 있는 개념이고, 그 아래 여러 버전의 Pod를 동시에 가질 수 있거든. 생성 후 Pod 이름을 보면 kubia-<RS해시>-<Pod해시> 형태인데, 가운데 해시가 Pod 템플릿의 해시야. 템플릿이 같으면 같은 ReplicaSet에, 다르면 새 ReplicaSet이 만들어져. 이 덕에 Deployment가 "이 템플릿에 해당하는 RS가 이미 있는가"를 찾아서 재사용할 수 있어.

업데이트 전략은 두 가지야 — Recreate(구 Pod 전부 삭제 후 신 Pod 생성, 다운타임 있음)와 RollingUpdate(기본값, 구 Pod를 하나씩 빼면서 신 Pod를 하나씩 넣음, 무중단). 업데이트를 트리거하려면 Deployment의 Pod 템플릿을 바꾸기만 하면 롤아웃이 시작돼. 바꾸는 방법은 여러 가지야 — kubectl edit deployment kubia로 에디터에서 직접 편집, kubectl patch로 특정 필드만, kubectl apply -f kubia-deployment-v2.yaml로 전체 파일로 덮어쓰기, 이미지만 쏙 바꾸려면 kubectl set image deployment kubia nodejs=luksa/kubia:v2. 템플릿이 아닌 필드(replicas, 전략 옵션 같은 거)를 바꾸는 건 롤아웃을 트리거하지 않아. 템플릿이 바뀌어야 새 ReplicaSet을 만들 일이 있으니까. 한 가지 주의 — Deployment가 참조하는 ConfigMap을 바꿔도 롤아웃은 안 일어나. 템플릿 자체는 그대로니까. ConfigMap으로 설정을 바꾸고 싶으면 ConfigMap을 새 이름으로 만들고 Deployment 템플릿에서 참조를 바꾸는 게 정석이야.

뒤에서 일어나는 일은, 롤아웃이 시작되면 Deployment가 새 ReplicaSet을 만들고 replicas를 서서히 올리면서 구 ReplicaSet의 replicas를 서서히 내려. 전부 컨트롤 플레인(Deployment Controller)에서 처리돼 — kubectl 클라이언트는 관여하지 않아. 이 점이 rolling-update와의 결정적 차이야. 선언적이고 서버 사이드. 롤아웃 진행 상황은 kubectl rollout status deployment kubia로 볼 수 있어.

v3을 올렸는데 버그가 있으면 kubectl rollout undo deployment kubia 한 줄로 직전 revision으로 되돌아가. 롤아웃이 진행 중이어도 쓸 수 있고, 그 경우 롤아웃을 중단하고 되돌려. 어떻게 가능하냐면, 구 ReplicaSet이 삭제되지 않고 replicas=0으로 남아있기 때문이야. 과거 버전마다 RS가 하나씩 남아서 revision 이력 역할을 해. kubectl rollout history deployment kubia로 목록을 보고 특정 revision으로 돌아갈 수도 있어 — kubectl rollout undo deployment kubia --to-revision=1. kubectl create--record 옵션을 주면 CHANGE-CAUSE 컬럼에 명령이 기록돼서 어떤 revision이 뭐였는지 보기 편해. 이력이 무한히 쌓이면 RS 목록이 어지러워지니까 **revisionHistoryLimit**으로 제한해 (apps/v1에서 기본 10). 오래된 RS는 자동 삭제돼. 한 가지 — 수동으로 구 RS를 지우는 건 하지 마. 그 revision으로 못 돌아가게 돼.

RollingUpdate 전략에는 두 파라미터가 있어. **maxSurge**는 desired replica count를 초과해서 만들 수 있는 Pod 수야. 기본 25%. 업데이트 중 리소스가 잠깐 얼마나 늘어도 되는지를 정하는 거지. **maxUnavailable**은 desired 대비 사용 불가능해도 되는 Pod 수야. 기본 25%. 업데이트 중 용량이 얼마나 줄어도 되는지를 정해. 두 값 모두 절대값이나 백분율로 설정 가능하고, 비율일 때 maxSurge는 올림, maxUnavailable은 내림이야. 예를 들어 replicas=3에 maxSurge=1, maxUnavailable=0이면 신 Pod 1개를 추가로 만들어서 총 4개가 되고, 새 Pod가 Ready 되면 구 Pod 1개를 삭제해서 다시 3개가 되고, 이걸 반복해. maxUnavailable=0이면 항상 3개가 가용이야. 용량이 절대 부족해지지 않아. 반대로 maxUnavailable=1, maxSurge=1이면 더 공격적으로 굴려. 프로덕션에서 뭐가 맞을지는 트래픽과 여유 리소스에 따라 다르고.

마지막으로 나쁜 버전을 막는 장치들. 롤아웃 중에 kubectl rollout pause deployment kubia로 일시정지할 수 있어. 그 시점에서 멈춰. 예를 들어 새 RS가 Pod 1개까지만 뜬 상태로 멈추면, 트래픽의 일부만 새 버전으로 가는 카나리 배포 흉내가 가능해. 확인 후 resume하거나 undo로 되돌리면 돼. 진짜 카나리는 보통 두 Deployment로 구성하는 게 더 깔끔하다고 책은 말해.

진짜 핵심은 **minReadySeconds**야. 새 Pod가 Ready로 인정받기 위해 얼마나 오래 Ready 상태여야 하는지를 정하는 거야. 기본은 0. 이걸 의미 있게 쓰려면 readiness probe가 함께 있어야 해. readiness probe가 지정 시간 내내 성공해야 Pod가 "available"로 카운트되고, 그제서야 롤아웃이 다음 단계로 넘어가거든. 왜 중요하냐면, Pod가 뜨자마자 readiness probe가 성공했다고 해도 몇 초 뒤에 실패하기 시작하면 그건 사실상 망가진 버전이잖아. minReadySeconds를 충분히 크게 두면 Pod가 그 시간 동안 건강을 유지해야 롤아웃이 진행되고, 안 그러면 롤아웃이 블로킹돼. maxUnavailable=0이랑 조합하면 구버전을 하나도 안 잃고 멈춰. 정리된 한 세트는 제대로 된 readiness probe 정의 + 충분한 minReadySeconds(10초 정도로는 실전 부족, 실제론 더 길게) + 보수적인 maxUnavailable=0이야. 이러면 "테스트에선 괜찮아 보였는데 프로덕션에서 터지는" v3 같은 버전이 롤아웃되다가 알아서 멈춰. readiness probe가 5번째 요청부터 500을 받기 시작하면 Pod가 not ready로 찍히고 Service 엔드포인트에서 빠지고 available count에 안 잡혀서 롤아웃이 진행이 안 돼. 유저는 거의 영향을 안 받고. 저자가 airbag에 비유한 장치야. 그리고 롤아웃이 진행을 못 하고 멈춰있으면 기본 10분 후에 ProgressDeadlineExceeded 조건으로 실패 처리돼. progressDeadlineSeconds로 조정 가능하고. 실패해도 자동 롤백은 안 돼서 kubectl rollout undo로 직접 되돌려야 해.


정리

9장 읽고 기억할 거 세 가지:

  1. Deployment는 Pod를 직접 관리하지 않고 ReplicaSet을 관리한다. 템플릿 해시로 버전마다 RS를 하나씩 만들고, 업데이트는 새 RS를 scale up, 구 RS를 scale down하는 방식. 과거 RS가 replicas=0으로 남아있어서 revision 이력이자 롤백 수단이 된다 — 그래서 구 RS를 수동으로 지우면 안 된다.
  2. 롤아웃은 Deployment 템플릿을 바꾸기만 하면 자동 시작된다. kubectl 클라이언트가 아니라 컨트롤 플레인의 Deployment Controller가 처리하므로 중간에 명령이 끊겨도 안전하다. 이게 kubectl rolling-update와의 결정적 차이 — 선언형 서버사이드 오케스트레이션.
  3. 나쁜 버전을 자동으로 막으려면 readiness probe + minReadySeconds + maxUnavailable=0 조합이 정석이다. Pod가 일정 시간 건강하게 Ready 상태를 유지해야 available로 인정되고, 아니면 롤아웃이 블로킹된다. 프로덕션 사고 시나리오에서 마지막 안전장치 역할.