M9. Probe·rollout·rollback과 무중단 경계
1. 왜 필요한가
섹션 제목: “1. 왜 필요한가”NKS의 web-app, api-service, ai-service가 Running이라는 사실만으로는 실제
요청을 받을 준비가 되었는지 알 수 없습니다. 잘못된 probe나 rollout 설정은 정상 Pod를
재시작하거나 준비되지 않은 revision에 트래픽을 보내므로, AI가 만든 배포 diff를 승인하려면
트래픽 제외·컨테이너 재시작·시작 보호·revision 되돌림의 경계를 구분해야 합니다.
개념 정의의 정본은 Kubernetes Basics입니다. 원본 경로는
content/topics/L5/kubernetes-basics.mdx이며, 이 모듈은 정의를 반복하지 않고 M3의
Deployment·ReplicaSet 관계, M4의 Service·EndpointSlice, M8의 Ready replica 판단을 실제
probe와 요청 흐름에 연결합니다.
2. 핵심 개념
섹션 제목: “2. 핵심 개념”flowchart LR
Kubelet["kubelet probe"] --> Startup{"startup 성공?"}
Startup -- "아니요: 임계 전" --> Gate["readiness·liveness 실행 보류"]
Startup -- "아니요: 임계 초과" --> StartupRestart["container kill
restartPolicy 적용"]
Startup -- "예" --> Readiness{"readiness 성공?"}
Readiness -- "아니요" --> NoTraffic["Pod Ready=False
Service endpoint 제외"]
Readiness -- "예" --> Traffic["Service traffic 후보"]
Startup -- "예" --> Liveness{"liveness 성공?"}
Liveness -- "아니요" --> Restart["container restart"]
Liveness -- "예" --> Keep["현재 container 유지"] | 신호 | 답하려는 질문 | 실패했을 때 실제 반응 | 외부 dependency 포함 기준 |
|---|---|---|---|
| readiness | 지금 새 요청을 받아도 되는가? | Pod Ready=False, Service endpoint에서 제외 | 필수 dependency의 일시 상태를 검토 가능 |
| liveness | 재시작만이 회복 수단인 내부 고장인가? | kubelet이 해당 container를 재시작 | DB·외부 API의 일시 지연은 넣지 않음 |
| startup | 초기화가 끝나 주기 probe를 시작할까? | 임계 전에는 주기 probe 보류, 임계 초과 시 kill·restartPolicy 적용 | 긴 bootstrap 완료 조건만 확인 |
periodSeconds × failureThreshold는 대략적인 실패 감지 구간을 이해하는 출발점일 뿐입니다.
probe 실행 시간, 첫 실행 시점과 controller·EndpointSlice 전파 지연이 있으므로 정확한 SLA 공식으로
쓰지 않습니다. timeoutSeconds도 별도로 검토해야 합니다.
flowchart LR
Old["old ReplicaSet
Ready 2"] --> New["new Pod 1개 생성
maxSurge: 1"]
New --> Probe{"new Pod Ready?"}
Probe -- "예" --> Replace["old Pod 1개 종료
다음 교체 진행"]
Probe -- "아니요" --> Hold["old Ready 2 유지
rollout 정지"]
Hold --> Inspect["Events·probe·revision 확인"]
Inspect --> Undo["rollout undo
이전 Pod template 복원"] | 설정·상태 | 이 랩의 값 | 의미 |
|---|---|---|
replicas | 2 | 정상 serving replica 목표 |
maxUnavailable | 0 | rollout 중 unavailable 허용량을 0으로 제한 |
maxSurge | 1 | 새 Pod를 목표보다 최대 1개 더 생성 |
minReadySeconds | 2 | Ready가 된 뒤 2초 유지해야 Available로 계산 |
progressDeadlineSeconds | 15 | 진전이 없으면 ProgressDeadlineExceeded 상태를 보고 |
revisionHistoryLimit | 4 | 이전 Pod template을 rollback할 수 있도록 history 보존 |
maxUnavailable: 0과 readiness는 이 fixture의 serving replica를 지키지만, 애플리케이션의
SIGTERM 처리, 긴 요청 drain, DB schema의 양방향 호환성, 외부 Load Balancer 전파까지 자동으로
증명하지 않습니다. kubectl rollout undo도 Deployment의 Pod template만 되돌리며 이미 실행한
DB migration이나 외부 부작용을 취소하지 않습니다.
시점 의존 설명은 2026-07-16에 다음 공식 1차 자료로 확인했습니다.
- Liveness, Readiness, and Startup Probes: startup 성공 전 주기 probe의 gate, readiness 실패의 EndpointSlice 제외, liveness 실패의 restart
- Deployments:
maxUnavailable,maxSurge, revision, progress deadline과 rollback - EndpointSlices:
ready,serving,terminating조건과 Service traffic 대상 - Pod Lifecycle: termination grace period, endpoint termination 상태와 종료 흐름
- Declarative Management with Configuration Files:
kubectl apply의 local file·live object·last-applied-configuration기반 patch 계산
실제 NKS rollout, NCP Load Balancer drain, production registry·DB migration은 승인된
non-production 환경이 없어 official-doc-only 또는 excluded입니다. 이 랩의 연속 요청 성공은
단일 k3d cluster·짧은 HTTP fixture에서 얻은 executed-local 증거이며 NKS 무중단 SLA가 아닙니다.
Argo Rollouts·service mesh canary, PDB·node drain, control plane·CNI 내부 구현과 Terraform 작성은
심화 또는 범위 밖입니다.
3. 직관 비유
섹션 제목: “3. 직관 비유”| 개념 | 비유 | 비유의 경계 |
|---|---|---|
| readiness | 식당이 문은 열었지만 주문을 받을 수 있을 때만 대기표에 올림 | process 생존이나 재시작 필요성을 판단하지 않습니다. |
| liveness | 멈춘 조리 기계를 껐다 켜야 하는지 보는 안전장치 | 재료 배송 지연 같은 외부 문제까지 재시작하면 악화됩니다. |
| startup | 개점 준비 시간 동안 영업 점검을 잠시 유예하는 표지 | 끝없이 실패하는 초기화를 무제한 기다리지는 않습니다. |
| rollback | 잘못 놓은 새 메뉴판을 보관한 이전 판으로 되돌림 | 이미 판매된 주문이나 DB 변경까지 취소하지 않습니다. |
4. 핸즈온 랩
섹션 제목: “4. 핸즈온 랩”검증 환경
섹션 제목: “검증 환경”| 항목 | 검증값 |
|---|---|
| 기준일 | 2026-07-16 |
| host | macOS arm64 |
| Docker client / engine | 29.6.1 / 29.6.1 |
| k3d | 5.9.0 |
| K3s image / API server | rancher/k3s:v1.36.2-k3s1 / v1.36.2+k3s1 |
| kubectl | 1.36.1 |
| Helm | 4.2.3 — 이 모듈에서는 사용하지 않음 |
| workload image | busybox:1.37.0 + 고정 multi-platform digest |
| evidence 환경 | local-cluster; 고정 context + k8s-wb-m9-<UTC run ID> |
모든 shell lab은 executed-local입니다. 실제 NKS workload와 Load Balancer 변경은
official-doc-only, production 변경은 excluded이며 예상 출력을 작성하지 않습니다.
사전 조건
섹션 제목: “사전 조건”- 저장소 root에서 실행하며 M0~M8을 완료한 상태여야 합니다.
- Docker Desktop과 M2의
cs-study-workbookcluster가 실행 중이어야 합니다. - 모든 API 명령은
--context k3d-cs-study-workbook과 고유 namespace를 명시합니다. - fixture의 1초 probe 주기와 짧은 termination grace는 학습 시간을 줄이기 위한 값입니다.
- 예상 소요 시간은 105분이며 probe·rollout 수렴 대기를 포함합니다.
Setup
섹션 제목: “Setup”고유 namespace 만들기
섹션 제목: “고유 namespace 만들기”Setup 실패 시 자신이 만든 namespace만 rollback합니다. namespace 삭제까지 실패하면 /tmp의
소유권 표식을 보존하고 exit 70으로 중단하므로 수동 확인 없이 다음 실행을 시작하지 않습니다.
set -euo pipefailrm -rf /tmp/k8s-wb-m9mkdir -p /tmp/k8s-wb-m9chmod 700 /tmp/k8s-wb-m9node automation/k8s-workbook/preflight.mjs --require-clustertest "$(kubectl config current-context)" = "k3d-cs-study-workbook"NS="k8s-wb-m9-$(date -u +%Y%m%d%H%M%S)"printf '%s' "$NS" >/tmp/k8s-wb-m9/namespaceif kubectl --context k3d-cs-study-workbook get namespace "$NS" >/dev/null 2>&1; then echo "namespace collision: $NS" >&2 rm -rf /tmp/k8s-wb-m9 exit 1firollback() { STATUS=$? trap - EXIT set +e kubectl --context k3d-cs-study-workbook delete namespace "$NS" \ --ignore-not-found --wait=true >/dev/null 2>&1 REMAINING="$(kubectl --context k3d-cs-study-workbook get namespace "$NS" \ --ignore-not-found -o name 2>/dev/null)" if test $? != 0 || test -n "$REMAINING"; then echo 'rollback incomplete: preserve /tmp/k8s-wb-m9 and inspect namespace' >&2 exit 70 fi rm -rf /tmp/k8s-wb-m9 exit "$STATUS"}trap rollback EXITkubectl --context k3d-cs-study-workbook create namespace "$NS" >/dev/nulltrap - EXITprintf 'setup namespace=%s context_guard=pass\n' "$NS"예상 출력(exit 0):
PASS platform: darwin/arm64PASS architecture: arm64PASS docker-cli: v29.6.1PASS docker-engine: v29.6.1PASS k3d: v5.9.0PASS kubectl-skew: v1.36.1PASS helm: v4.2.3PASS context: k3d-cs-study-workbookPASS kubernetes-server: v1.36.2+k3s1setup namespace=k8s-wb-m9-<UTC run ID> context_guard=passLab 1 — 세 probe와 Service 기준선 만들기
섹션 제목: “Lab 1 — 세 probe와 Service 기준선 만들기”같은 HTTP server에 /ready와 /live를 분리합니다. preStop과 5초 grace는 동작 경계를 보기
위한 local fixture이지 production 권장값이 아닙니다.
set -euo pipefailtest "$(kubectl config current-context)" = "k3d-cs-study-workbook"NS="$(cat /tmp/k8s-wb-m9/namespace)"kubectl --context k3d-cs-study-workbook -n "$NS" apply -f - <<'YAML'apiVersion: v1kind: Servicemetadata: name: api-servicespec: selector: app: api-service ports: - name: http port: 80 targetPort: http---apiVersion: apps/v1kind: Deploymentmetadata: name: api-servicespec: replicas: 2 minReadySeconds: 2 progressDeadlineSeconds: 15 revisionHistoryLimit: 4 strategy: type: RollingUpdate rollingUpdate: maxUnavailable: 0 maxSurge: 1 selector: matchLabels: app: api-service template: metadata: labels: app: api-service spec: terminationGracePeriodSeconds: 5 containers: - name: app image: busybox:1.37.0@sha256:9532d8c39891ca2ecde4d30d7710e01fb739c87a8b9299685c63704296b16028 imagePullPolicy: IfNotPresent env: - name: APP_VERSION value: v1 - name: FAIL_READY value: "false" command: ["sh", "-c"] args: - | mkdir -p /www printf '%s\n' "$APP_VERSION" >/www/index.html printf 'live\n' >/www/live if test "$FAIL_READY" = "true"; then rm -f /www/ready else printf 'ready\n' >/www/ready fi exec httpd -f -p 8080 -h /www ports: - name: http containerPort: 8080 startupProbe: httpGet: path: /live port: http periodSeconds: 1 failureThreshold: 10 readinessProbe: httpGet: path: /ready port: http periodSeconds: 1 failureThreshold: 1 livenessProbe: httpGet: path: /live port: http periodSeconds: 1 failureThreshold: 2 lifecycle: preStop: exec: command: ["sh", "-c", "sleep 2"] resources: requests: cpu: 10m memory: 8Mi limits: cpu: 100m memory: 32MiYAMLkubectl --context k3d-cs-study-workbook -n "$NS" \ rollout status deployment/api-service --timeout=90s >/dev/nullREADY="$(kubectl --context k3d-cs-study-workbook -n "$NS" get deployment api-service \ -o jsonpath='{.status.readyReplicas}')"AVAILABLE="$(kubectl --context k3d-cs-study-workbook -n "$NS" get deployment api-service \ -o jsonpath='{.status.availableReplicas}')"ENDPOINTS="$(kubectl --context k3d-cs-study-workbook -n "$NS" get endpointslice \ -l kubernetes.io/service-name=api-service \ -o jsonpath='{range .items[*].endpoints[?(@.conditions.ready==true)]}{.addresses[0]}{"\n"}{end}' \ | awk 'NF {count++} END {print count+0}')"test "$READY" = "2"test "$AVAILABLE" = "2"test "$ENDPOINTS" = "2"printf 'baseline version=v1 ready=%s available=%s endpoints=%s\n' \ "$READY" "$AVAILABLE" "$ENDPOINTS"예상 출력(exit 0):
service/api-service createddeployment.apps/api-service createdbaseline version=v1 ready=2 available=2 endpoints=2Lab 2 — readiness 실패는 endpoint만 제외하기
섹션 제목: “Lab 2 — readiness 실패는 endpoint만 제외하기”한 Pod의 /ready 파일을 지운 뒤 container restart 없이 Ready endpoint만 2→1로 줄어드는지
확인합니다. 복구 후에는 같은 Pod가 다시 endpoint에 들어옵니다.
set -euo pipefailtest "$(kubectl config current-context)" = "k3d-cs-study-workbook"NS="$(cat /tmp/k8s-wb-m9/namespace)"POD="$(kubectl --context k3d-cs-study-workbook -n "$NS" get pod \ -l app=api-service -o jsonpath='{.items[0].metadata.name}')"RESTARTS_BEFORE="$(kubectl --context k3d-cs-study-workbook -n "$NS" get pod "$POD" \ -o jsonpath='{.status.containerStatuses[0].restartCount}')"kubectl --context k3d-cs-study-workbook -n "$NS" exec "$POD" -- rm /www/readyREADY=""ENDPOINTS=""for _ in $(seq 1 30); do READY="$(kubectl --context k3d-cs-study-workbook -n "$NS" get pod "$POD" \ -o jsonpath='{.status.conditions[?(@.type=="Ready")].status}')" ENDPOINTS="$(kubectl --context k3d-cs-study-workbook -n "$NS" get endpointslice \ -l kubernetes.io/service-name=api-service \ -o jsonpath='{range .items[*].endpoints[?(@.conditions.ready==true)]}{.addresses[0]}{"\n"}{end}' \ | awk 'NF {count++} END {print count+0}')" test "$READY" = "False" && test "$ENDPOINTS" = "1" && break sleep 1doneRESTARTS_FAILED="$(kubectl --context k3d-cs-study-workbook -n "$NS" get pod "$POD" \ -o jsonpath='{.status.containerStatuses[0].restartCount}')"test "$READY" = "False"test "$ENDPOINTS" = "1"test "$RESTARTS_FAILED" = "$RESTARTS_BEFORE"kubectl --context k3d-cs-study-workbook -n "$NS" exec "$POD" -- sh -c \ "printf 'ready\\n' >/www/ready"kubectl --context k3d-cs-study-workbook -n "$NS" wait \ --for=condition=Ready "pod/$POD" --timeout=30s >/dev/nullfor _ in $(seq 1 30); do ENDPOINTS="$(kubectl --context k3d-cs-study-workbook -n "$NS" get endpointslice \ -l kubernetes.io/service-name=api-service \ -o jsonpath='{range .items[*].endpoints[?(@.conditions.ready==true)]}{.addresses[0]}{"\n"}{end}' \ | awk 'NF {count++} END {print count+0}')" test "$ENDPOINTS" = "2" && break sleep 1donetest "$ENDPOINTS" = "2"printf 'readiness pod=%s ready=false endpoints=2->1 restarts=%s->%s recovered_endpoints=%s\n' \ "$POD" "$RESTARTS_BEFORE" "$RESTARTS_FAILED" "$ENDPOINTS"예상 출력(exit 0):
readiness pod=api-service-<template hash>-<suffix> ready=false endpoints=2->1 restarts=0->0 recovered_endpoints=2Lab 3 — liveness 실패는 container를 재시작하기
섹션 제목: “Lab 3 — liveness 실패는 container를 재시작하기”이번에는 /live를 지웁니다. readiness 실패와 달리 kubelet이 같은 Pod 안의 container를
재시작하고, 시작 script가 health 파일을 다시 만들면 Ready로 복구됩니다.
set -euo pipefailtest "$(kubectl config current-context)" = "k3d-cs-study-workbook"NS="$(cat /tmp/k8s-wb-m9/namespace)"POD="$(kubectl --context k3d-cs-study-workbook -n "$NS" get pod \ -l app=api-service -o jsonpath='{.items[0].metadata.name}')"RESTARTS_BEFORE="$(kubectl --context k3d-cs-study-workbook -n "$NS" get pod "$POD" \ -o jsonpath='{.status.containerStatuses[0].restartCount}')"kubectl --context k3d-cs-study-workbook -n "$NS" exec "$POD" -- rm /www/liveRESTARTS_AFTER="$RESTARTS_BEFORE"for _ in $(seq 1 45); do RESTARTS_AFTER="$(kubectl --context k3d-cs-study-workbook -n "$NS" get pod "$POD" \ -o jsonpath='{.status.containerStatuses[0].restartCount}')" test "$RESTARTS_AFTER" -gt "$RESTARTS_BEFORE" && break sleep 1donetest "$RESTARTS_AFTER" -gt "$RESTARTS_BEFORE"kubectl --context k3d-cs-study-workbook -n "$NS" wait \ --for=condition=Ready "pod/$POD" --timeout=45s >/dev/nullVERSION="$(kubectl --context k3d-cs-study-workbook -n "$NS" exec "$POD" -- \ wget -qO- http://127.0.0.1:8080/)"test "$VERSION" = "v1"printf 'liveness pod=%s restarts=%s->%s recovered_ready=true version=%s\n' \ "$POD" "$RESTARTS_BEFORE" "$RESTARTS_AFTER" "$VERSION"예상 출력(exit 0):
liveness pod=api-service-<template hash>-<suffix> restarts=0->1 recovered_ready=true version=v1Lab 4 — startup probe가 느린 시작을 보호하기
섹션 제목: “Lab 4 — startup probe가 느린 시작을 보호하기”slow-start는 HTTP server를 먼저 띄우지만 6초 뒤에야 startup marker와 /live를 만듭니다.
startup probe가 성공하기 전에는 더 공격적인 liveness가 실행되지 않으므로 restart 없이 Ready에
도달해야 합니다. 이 fixture는 12회 실패 임계 전에 startup이 성공해 restart 0이 되는 경로만
검증합니다. 12회를 모두 실패하면 무기한 보류되는 것이 아니라 container가 kill되고
restartPolicy가 적용됩니다.
set -euo pipefailtest "$(kubectl config current-context)" = "k3d-cs-study-workbook"NS="$(cat /tmp/k8s-wb-m9/namespace)"kubectl --context k3d-cs-study-workbook -n "$NS" apply -f - <<'YAML'apiVersion: apps/v1kind: Deploymentmetadata: name: slow-startspec: replicas: 1 selector: matchLabels: app: slow-start template: metadata: labels: app: slow-start spec: containers: - name: app image: busybox:1.37.0@sha256:9532d8c39891ca2ecde4d30d7710e01fb739c87a8b9299685c63704296b16028 imagePullPolicy: IfNotPresent command: ["sh", "-c"] args: - | mkdir -p /www printf 'slow\n' >/www/index.html printf 'ready\n' >/www/ready rm -f /www/startup /www/live (sleep 6; touch /www/startup /www/live) & exec httpd -f -p 8080 -h /www startupProbe: exec: command: ["sh", "-c", "test -f /www/startup"] periodSeconds: 1 failureThreshold: 12 readinessProbe: httpGet: path: /ready port: 8080 periodSeconds: 1 failureThreshold: 1 livenessProbe: httpGet: path: /live port: 8080 periodSeconds: 1 failureThreshold: 1YAMLPHASE=""POD=""for _ in $(seq 1 30); do POD="$(kubectl --context k3d-cs-study-workbook -n "$NS" get pod \ -l app=slow-start -o jsonpath='{.items[0].metadata.name}' 2>/dev/null || true)" test -n "$POD" || { sleep 1; continue; } PHASE="$(kubectl --context k3d-cs-study-workbook -n "$NS" get pod "$POD" \ -o jsonpath='{.status.phase}')" test "$PHASE" = "Running" && break sleep 1donetest "$PHASE" = "Running"sleep 2EARLY_READY="$(kubectl --context k3d-cs-study-workbook -n "$NS" get pod "$POD" \ -o jsonpath='{.status.conditions[?(@.type=="Ready")].status}')"EARLY_RESTARTS="$(kubectl --context k3d-cs-study-workbook -n "$NS" get pod "$POD" \ -o jsonpath='{.status.containerStatuses[0].restartCount}')"test "$EARLY_READY" = "False"test "$EARLY_RESTARTS" = "0"kubectl --context k3d-cs-study-workbook -n "$NS" wait \ --for=condition=Ready "pod/$POD" --timeout=30s >/dev/nullFINAL_RESTARTS="$(kubectl --context k3d-cs-study-workbook -n "$NS" get pod "$POD" \ -o jsonpath='{.status.containerStatuses[0].restartCount}')"test "$FINAL_RESTARTS" = "0"printf 'startup pod=%s early_ready=%s restarts=%s final_ready=true final_restarts=%s\n' \ "$POD" "$EARLY_READY" "$EARLY_RESTARTS" "$FINAL_RESTARTS"예상 출력(exit 0):
deployment.apps/slow-start createdstartup pod=slow-start-<template hash>-<suffix> early_ready=False restarts=0 final_ready=true final_restarts=0Lab 5 — 성공 rollout 동안 Service 요청 연속성 측정하기
섹션 제목: “Lab 5 — 성공 rollout 동안 Service 요청 연속성 측정하기”cluster 내부 client가 160회 요청하는 동안 v1→v2 Pod template 변경을 수행합니다. 두 version을
모두 관찰하고 오류가 0인지 확인합니다. 이는 짧은 fixture의 관측값이지 보편적인 무중단 보장이
아닙니다.
set -euo pipefailtest "$(kubectl config current-context)" = "k3d-cs-study-workbook"NS="$(cat /tmp/k8s-wb-m9/namespace)"kubectl --context k3d-cs-study-workbook -n "$NS" delete pod rollout-client \ --ignore-not-found --wait=true >/dev/nullkubectl --context k3d-cs-study-workbook -n "$NS" run rollout-client \ --restart=Never \ --image=busybox:1.37.0@sha256:9532d8c39891ca2ecde4d30d7710e01fb739c87a8b9299685c63704296b16028 \ --image-pull-policy=IfNotPresent \ --command -- sh -c \ 'i=0; while test "$i" -lt 160; do wget -T 1 -qO- http://api-service || echo ERR; i=$((i+1)); sleep 0.1; done' \ >/dev/nullkubectl --context k3d-cs-study-workbook -n "$NS" wait \ --for=condition=Ready pod/rollout-client --timeout=30s >/dev/nullfor _ in $(seq 1 30); do BEFORE_LINES="$(kubectl --context k3d-cs-study-workbook -n "$NS" logs rollout-client \ 2>/dev/null | awk 'END {print NR+0}')" test "$BEFORE_LINES" -ge 3 && break sleep 1donetest "$BEFORE_LINES" -ge 3kubectl --context k3d-cs-study-workbook -n "$NS" set env deployment/api-service \ APP_VERSION=v2 FAIL_READY=false >/dev/nullkubectl --context k3d-cs-study-workbook -n "$NS" \ rollout status deployment/api-service --timeout=90s >/dev/nullfor _ in $(seq 1 90); do PHASE="$(kubectl --context k3d-cs-study-workbook -n "$NS" get pod rollout-client \ -o jsonpath='{.status.phase}')" test "$PHASE" = "Succeeded" && break sleep 1donetest "$PHASE" = "Succeeded"kubectl --context k3d-cs-study-workbook -n "$NS" logs rollout-client \ >/tmp/k8s-wb-m9/success-client.logread TOTAL ERRORS V1 V2 OTHER <<EOF$(awk '{total++; if ($0=="ERR") err++; else if ($0=="v1") v1++; else if ($0=="v2") v2++; else other++} END {print total+0,err+0,v1+0,v2+0,other+0}' \ /tmp/k8s-wb-m9/success-client.log)EOFREADY="$(kubectl --context k3d-cs-study-workbook -n "$NS" get deployment api-service \ -o jsonpath='{.status.readyReplicas}')"test "$TOTAL" = "160"test "$ERRORS" = "0"test "$OTHER" = "0"test "$V1" -gt 0test "$V2" -gt 0test "$READY" = "2"printf 'continuity rollout=v1->v2 requests=%s errors=%s v1=%s v2=%s ready=%s/2\n' \ "$TOTAL" "$ERRORS" "$V1" "$V2" "$READY"예상 출력(exit 0):
continuity rollout=v1->v2 requests=160 errors=0 v1=<1 이상> v2=<1 이상> ready=2/2Lab 6 — readiness 불량 revision을 멈추고 rollback하기
섹션 제목: “Lab 6 — readiness 불량 revision을 멈추고 rollback하기”v3는 의도적으로 /ready를 만들지 않습니다. maxUnavailable: 0이므로 v2 endpoint 2개를
유지한 채 rollout이 progress deadline을 넘겨 실패해야 합니다. 요청 기록에서 v3가 0건인지
확인한 뒤 이전 Pod template으로 undo합니다.
| # | 하는 일 | 중단되면 먼저 볼 대상 |
|---|---|---|
| 1 | 260회 Service 요청 client 시작 | failure-client phase와 logs |
| 2 | v3 실패 주입과 Deployment 상태 판독 | failed-rollout.log, Progressing condition |
| 3 | 응답 수·version·endpoint 검산 | failure-client.log, EndpointSlice |
| 4 | undo 경고 캡처와 v2 수렴 확인 | undo.log, Pod template, Ready·updated replica |
set -euo pipefailtest "$(kubectl config current-context)" = "k3d-cs-study-workbook"NS="$(cat /tmp/k8s-wb-m9/namespace)"# 1) 요청을 시작한다.kubectl --context k3d-cs-study-workbook -n "$NS" delete pod failure-client \ --ignore-not-found --wait=true >/dev/nullkubectl --context k3d-cs-study-workbook -n "$NS" run failure-client \ --restart=Never \ --image=busybox:1.37.0@sha256:9532d8c39891ca2ecde4d30d7710e01fb739c87a8b9299685c63704296b16028 \ --image-pull-policy=IfNotPresent \ --command -- sh -c \ 'i=0; while test "$i" -lt 260; do wget -T 1 -qO- http://api-service || echo ERR; i=$((i+1)); sleep 0.1; done' \ >/dev/nullkubectl --context k3d-cs-study-workbook -n "$NS" wait \ --for=condition=Ready pod/failure-client --timeout=30s >/dev/null# 2) readiness가 실패하는 v3를 주입하고 rollout 실패 상태를 판독한다.kubectl --context k3d-cs-study-workbook -n "$NS" set env deployment/api-service \ APP_VERSION=v3 FAIL_READY=true >/dev/nullset +ekubectl --context k3d-cs-study-workbook -n "$NS" rollout status \ deployment/api-service --timeout=30s >/tmp/k8s-wb-m9/failed-rollout.log 2>&1ROLLOUT_EXIT=$?set -etest "$ROLLOUT_EXIT" = "1"REASON="$(kubectl --context k3d-cs-study-workbook -n "$NS" get deployment api-service \ -o jsonpath='{.status.conditions[?(@.type=="Progressing")].reason}')"AVAILABLE="$(kubectl --context k3d-cs-study-workbook -n "$NS" get deployment api-service \ -o jsonpath='{.status.availableReplicas}')"NEW_READY="$(kubectl --context k3d-cs-study-workbook -n "$NS" get pod \ -l app=api-service -o jsonpath='{range .items[?(@.metadata.labels.pod-template-hash)]}{.status.conditions[?(@.type=="Ready")].status}{"\n"}{end}' \ | awk '$1=="False" {count++} END {print count+0}')"ENDPOINTS="$(kubectl --context k3d-cs-study-workbook -n "$NS" get endpointslice \ -l kubernetes.io/service-name=api-service \ -o jsonpath='{range .items[*].endpoints[?(@.conditions.ready==true)]}{.addresses[0]}{"\n"}{end}' \ | awk 'NF {count++} END {print count+0}')"test "$REASON" = "ProgressDeadlineExceeded"test "$AVAILABLE" = "2"test "$NEW_READY" -ge 1test "$ENDPOINTS" = "2"# 3) client 완료 뒤 응답 수와 version을 검산한다.for _ in $(seq 1 90); do PHASE="$(kubectl --context k3d-cs-study-workbook -n "$NS" get pod failure-client \ -o jsonpath='{.status.phase}')" test "$PHASE" = "Succeeded" && break sleep 1donetest "$PHASE" = "Succeeded"kubectl --context k3d-cs-study-workbook -n "$NS" logs failure-client \ >/tmp/k8s-wb-m9/failure-client.logread TOTAL ERRORS V2 V3 OTHER <<EOF$(awk '{total++; if ($0=="ERR") err++; else if ($0=="v2") v2++; else if ($0=="v3") v3++; else other++} END {print total+0,err+0,v2+0,v3+0,other+0}' \ /tmp/k8s-wb-m9/failure-client.log)EOFtest "$TOTAL" = "260"test "$ERRORS" = "0"test "$V2" = "260"test "$V3" = "0"test "$OTHER" = "0"# 4) live Pod template을 undo하고 v2 revision의 완전한 수렴을 확인한다.set +ekubectl --context k3d-cs-study-workbook -n "$NS" rollout undo \ deployment/api-service >/tmp/k8s-wb-m9/undo.log 2>&1UNDO_EXIT=$?set -etest "$UNDO_EXIT" = "0"UNDO_OUTPUT="$(cat /tmp/k8s-wb-m9/undo.log)"case "$UNDO_OUTPUT" in *"was previously managed with 'kubectl apply'"*) UNDO_WARNING="last-applied-stale" ;; *) printf 'unexpected undo output: %s\n' "$UNDO_OUTPUT" >&2; exit 2 ;;esacFINAL_READY=""FINAL_UPDATED=""FINAL_VERSION=""FINAL_FAIL_READY=""FINAL_REASON=""for _ in $(seq 1 90); do FINAL_READY="$(kubectl --context k3d-cs-study-workbook -n "$NS" get deployment api-service \ -o jsonpath='{.status.readyReplicas}')" FINAL_UPDATED="$(kubectl --context k3d-cs-study-workbook -n "$NS" get deployment api-service \ -o jsonpath='{.status.updatedReplicas}')" FINAL_VERSION="$(kubectl --context k3d-cs-study-workbook -n "$NS" get deployment api-service \ -o jsonpath='{.spec.template.spec.containers[0].env[?(@.name=="APP_VERSION")].value}')" FINAL_FAIL_READY="$(kubectl --context k3d-cs-study-workbook -n "$NS" get deployment api-service \ -o jsonpath='{.spec.template.spec.containers[0].env[?(@.name=="FAIL_READY")].value}')" FINAL_REASON="$(kubectl --context k3d-cs-study-workbook -n "$NS" get deployment api-service \ -o jsonpath='{.status.conditions[?(@.type=="Progressing")].reason}')" test "$FINAL_READY" = "2" && test "$FINAL_UPDATED" = "2" && \ test "$FINAL_VERSION" = "v2" && test "$FINAL_FAIL_READY" = "false" && \ test "$FINAL_REASON" = "NewReplicaSetAvailable" && break sleep 1donetest "$FINAL_READY" = "2"test "$FINAL_UPDATED" = "2"test "$FINAL_VERSION" = "v2"test "$FINAL_FAIL_READY" = "false"test "$FINAL_REASON" = "NewReplicaSetAvailable"printf 'failed_rollout exit=%s reason=%s available=%s endpoints=%s requests=%s errors=%s v3=%s\n' \ "$ROLLOUT_EXIT" "$REASON" "$AVAILABLE" "$ENDPOINTS" "$TOTAL" "$ERRORS" "$V3"printf 'rollback version=%s fail_ready=%s ready=%s/2 undo_warning=%s\n' \ "$FINAL_VERSION" "$FINAL_FAIL_READY" "$FINAL_READY" "$UNDO_WARNING"exit 1예상 출력(exit 1, 의도한 실패를 관찰하고 복구 완료):
failed_rollout exit=1 reason=ProgressDeadlineExceeded available=2 endpoints=2 requests=260 errors=0 v3=0rollback version=v2 fail_ready=false ready=2/2 undo_warning=last-applied-stalekubectl rollout undo는 live Pod template을 복원했지만, 이 fixture를 처음 만든 client-side
kubectl apply의 last-applied-configuration annotation은 갱신하지 않는다는 경고를 냅니다.
따라서 이후 이 fixture의 v1 선언을 다시 apply하면 선언 파일의 Pod template 값이 live v2에
다시 반영될 수 있습니다. 실무 GitOps에서는 undo만 실행하고 끝내지 않고, 선언 원본을 정상
revision으로 되돌린 PR과 live diff 수렴까지 함께 확인합니다.
Cleanup
섹션 제목: “Cleanup”공유 k3d cluster는 유지하고 모듈 namespace와 /tmp fixture만 제거합니다.
set -euo pipefailtest "$(kubectl config current-context)" = "k3d-cs-study-workbook"NS="$(cat /tmp/k8s-wb-m9/namespace)"kubectl --context k3d-cs-study-workbook delete namespace "$NS" --wait=true >/dev/nulltest -z "$(kubectl --context k3d-cs-study-workbook get namespace "$NS" \ --ignore-not-found -o name)"rm -rf /tmp/k8s-wb-m9test ! -e /tmp/k8s-wb-m9NODE_READY="$(kubectl --context k3d-cs-study-workbook get nodes --no-headers \ | awk '$2 == "Ready" {count++} END {print count+0}')"test "$NODE_READY" = "2"printf 'cleanup namespace=0 deployment=0 service=0 client_pods=0 tmp=0 nodes_ready=%s cluster=retained\n' \ "$NODE_READY"예상 출력(exit 0):
cleanup namespace=0 deployment=0 service=0 client_pods=0 tmp=0 nodes_ready=2 cluster=retained5. 관찰 포인트
섹션 제목: “5. 관찰 포인트”- readiness 실패 후 restart count는 그대로지만 Ready endpoint는 2→1로 줄어듭니다.
- readiness가 회복되면 같은 Pod가 다시 endpoint에 들어옵니다.
- liveness 실패는 container restart count를 올리고 시작 script가 다시 실행됩니다.
- startup 성공 전에는 공격적인 liveness가 실행되지 않아 느린 시작이 restart 없이 끝납니다.
- 성공 rollout에서 v1과 v2 응답을 모두 받지만 이 fixture의 요청 오류는 0입니다.
- readiness가 실패하는 v3는 Service 응답에 한 번도 나타나지 않고 old v2 endpoint 2개가 남습니다.
rollout undo뒤 Deployment Pod template과 Ready replica가 v2·2/2로 복원됩니다.- cleanup 뒤 module namespace, Deployment, Service, client Pod와
/tmp가 모두 0입니다.
6. 셀프체크 Q&A
섹션 제목: “6. 셀프체크 Q&A”Q1. api-service readiness와 liveness가 모두 DB 연결을 검사합니다. DB가 30초 느려졌을 때 어떤 위험이 있습니까?
섹션 제목: “Q1. api-service readiness와 liveness가 모두 DB 연결을 검사합니다. DB가 30초 느려졌을 때 어떤 위험이 있습니까?”답: readiness 실패로 Pod를 endpoint에서 빼는 것은 새 요청 보호에 도움이 될 수 있지만, liveness까지 실패하면 모든 container가 재시작되어 DB에 재연결 폭풍을 만들 수 있습니다. 앱 자체의 deadlock·치명 상태만 liveness에 두고, 외부 dependency의 일시 상태는 readiness와 별도 timeout· circuit breaker 관점에서 검토합니다.
Q2. 새 revision Pod가 Running 1/1이고 rollout status도 성공했습니다. NKS 무중단 배포를 승인할 수 있습니까?
섹션 제목: “Q2. 새 revision Pod가 Running 1/1이고 rollout status도 성공했습니다. NKS 무중단 배포를 승인할 수 있습니까?”답: 이 두 신호만으로는 승인하지 않습니다. Service/Ingress 또는 Load Balancer를 지나는 실제 요청 오류율과 latency, termination 중 in-flight request drain, SIGTERM 처리, DB schema의 이전·새 version 동시 호환, rollback trigger를 확인해야 합니다. 이 랩의 0 errors도 짧은 k3d fixture 범위의 증거일 뿐입니다.
Q3. readiness 불량 v3를 rollout undo해 v2 Pod가 복구됐습니다. 함께 되돌아가지 않는 것은 무엇입니까?
섹션 제목: “Q3. readiness 불량 v3를 rollout undo해 v2 Pod가 복구됐습니다. 함께 되돌아가지 않는 것은 무엇입니까?”답: Deployment history가 복원하는 것은 Pod template입니다. v3가 이미 수행한 DB migration, queue message, 외부 API 호출, feature flag와 Secret 변경은 자동 rollback되지 않습니다. 배포 전에 expand/contract migration과 version 호환성, 외부 부작용의 보상·중단 절차를 별도로 승인해야 합니다.
7. 흔한 함정
섹션 제목: “7. 흔한 함정”함정 1 — readiness와 liveness에 같은 무거운 dependency check 넣기
섹션 제목: “함정 1 — readiness와 liveness에 같은 무거운 dependency check 넣기”일시적인 DB/API 장애가 전체 Pod restart storm으로 확대될 수 있습니다. 두 probe가 실패했을 때 Kubernetes가 취하는 행동부터 비교하고 endpoint를 분리합니다.
함정 2 — maxUnavailable: 0을 무중단 보증서로 보기
섹션 제목: “함정 2 — maxUnavailable: 0을 무중단 보증서로 보기”Ready replica 하한만 다룰 뿐 long-lived connection, in-flight drain, 애플리케이션 오류, Node 장애, 외부 Load Balancer 전파와 schema 비호환은 보장하지 않습니다. 실제 요청과 종료 경로를 따로 측정합니다.
함정 3 — rollback 가능성을 forward/backward compatibility 대신 쓰기
섹션 제목: “함정 3 — rollback 가능성을 forward/backward compatibility 대신 쓰기”이전 Pod template으로 돌아가도 새 schema나 외부 부작용이 남으면 이전 앱이 더 이상 동작하지 않을 수 있습니다. rollback은 호환성 설계와 관측 기준을 보완하는 수단이지 대체물이 아닙니다.
8. 다음 모듈 연결 고리
섹션 제목: “8. 다음 모듈 연결 고리”다음 M10에서는 Pod template을 되돌려도 남아야 하는 데이터와 일회용 Pod filesystem을 구분하고, PV·PVC와 stateless 서비스 경계를 판단합니다.