콘텐츠로 이동

M9. Probe·rollout·rollback과 무중단 경계

NKS의 web-app, api-service, ai-serviceRunning이라는 사실만으로는 실제 요청을 받을 준비가 되었는지 알 수 없습니다. 잘못된 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와 요청 흐름에 연결합니다.

probe 실패가 바꾸는 상태
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도 별도로 검토해야 합니다.

RollingUpdate에서 Ready가 만드는 교체 문
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 복원"]
설정·상태이 랩의 값의미
replicas2정상 serving replica 목표
maxUnavailable0rollout 중 unavailable 허용량을 0으로 제한
maxSurge1새 Pod를 목표보다 최대 1개 더 생성
minReadySeconds2Ready가 된 뒤 2초 유지해야 Available로 계산
progressDeadlineSeconds15진전이 없으면 ProgressDeadlineExceeded 상태를 보고
revisionHistoryLimit4이전 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차 자료로 확인했습니다.

실제 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 작성은 심화 또는 범위 밖입니다.

개념비유비유의 경계
readiness식당이 문은 열었지만 주문을 받을 수 있을 때만 대기표에 올림process 생존이나 재시작 필요성을 판단하지 않습니다.
liveness멈춘 조리 기계를 껐다 켜야 하는지 보는 안전장치재료 배송 지연 같은 외부 문제까지 재시작하면 악화됩니다.
startup개점 준비 시간 동안 영업 점검을 잠시 유예하는 표지끝없이 실패하는 초기화를 무제한 기다리지는 않습니다.
rollback잘못 놓은 새 메뉴판을 보관한 이전 판으로 되돌림이미 판매된 주문이나 DB 변경까지 취소하지 않습니다.
항목검증값
기준일2026-07-16
hostmacOS arm64
Docker client / engine29.6.1 / 29.6.1
k3d5.9.0
K3s image / API serverrancher/k3s:v1.36.2-k3s1 / v1.36.2+k3s1
kubectl1.36.1
Helm4.2.3 — 이 모듈에서는 사용하지 않음
workload imagebusybox: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-workbook cluster가 실행 중이어야 합니다.
  • 모든 API 명령은 --context k3d-cs-study-workbook과 고유 namespace를 명시합니다.
  • fixture의 1초 probe 주기와 짧은 termination grace는 학습 시간을 줄이기 위한 값입니다.
  • 예상 소요 시간은 105분이며 probe·rollout 수렴 대기를 포함합니다.

Setup 실패 시 자신이 만든 namespace만 rollback합니다. namespace 삭제까지 실패하면 /tmp의 소유권 표식을 보존하고 exit 70으로 중단하므로 수동 확인 없이 다음 실행을 시작하지 않습니다.

Terminal window
set -euo pipefail
rm -rf /tmp/k8s-wb-m9
mkdir -p /tmp/k8s-wb-m9
chmod 700 /tmp/k8s-wb-m9
node automation/k8s-workbook/preflight.mjs --require-cluster
test "$(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/namespace
if 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 1
fi
rollback() {
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 EXIT
kubectl --context k3d-cs-study-workbook create namespace "$NS" >/dev/null
trap - EXIT
printf 'setup namespace=%s context_guard=pass\n' "$NS"

예상 출력(exit 0):

PASS platform: darwin/arm64
PASS architecture: arm64
PASS docker-cli: v29.6.1
PASS docker-engine: v29.6.1
PASS k3d: v5.9.0
PASS kubectl-skew: v1.36.1
PASS helm: v4.2.3
PASS context: k3d-cs-study-workbook
PASS kubernetes-server: v1.36.2+k3s1
setup namespace=k8s-wb-m9-<UTC run ID> context_guard=pass

Lab 1 — 세 probe와 Service 기준선 만들기

섹션 제목: “Lab 1 — 세 probe와 Service 기준선 만들기”

같은 HTTP server에 /ready/live를 분리합니다. preStop과 5초 grace는 동작 경계를 보기 위한 local fixture이지 production 권장값이 아닙니다.

Terminal window
set -euo pipefail
test "$(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: v1
kind: Service
metadata:
name: api-service
spec:
selector:
app: api-service
ports:
- name: http
port: 80
targetPort: http
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: api-service
spec:
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: 32Mi
YAML
kubectl --context k3d-cs-study-workbook -n "$NS" \
rollout status deployment/api-service --timeout=90s >/dev/null
READY="$(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 created
deployment.apps/api-service created
baseline version=v1 ready=2 available=2 endpoints=2

Lab 2 — readiness 실패는 endpoint만 제외하기

섹션 제목: “Lab 2 — readiness 실패는 endpoint만 제외하기”

한 Pod의 /ready 파일을 지운 뒤 container restart 없이 Ready endpoint만 2→1로 줄어드는지 확인합니다. 복구 후에는 같은 Pod가 다시 endpoint에 들어옵니다.

Terminal window
set -euo pipefail
test "$(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/ready
READY=""
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 1
done
RESTARTS_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/null
for _ 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 1
done
test "$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=2

Lab 3 — liveness 실패는 container를 재시작하기

섹션 제목: “Lab 3 — liveness 실패는 container를 재시작하기”

이번에는 /live를 지웁니다. readiness 실패와 달리 kubelet이 같은 Pod 안의 container를 재시작하고, 시작 script가 health 파일을 다시 만들면 Ready로 복구됩니다.

Terminal window
set -euo pipefail
test "$(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/live
RESTARTS_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 1
done
test "$RESTARTS_AFTER" -gt "$RESTARTS_BEFORE"
kubectl --context k3d-cs-study-workbook -n "$NS" wait \
--for=condition=Ready "pod/$POD" --timeout=45s >/dev/null
VERSION="$(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=v1

Lab 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가 적용됩니다.

Terminal window
set -euo pipefail
test "$(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/v1
kind: Deployment
metadata:
name: slow-start
spec:
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: 1
YAML
PHASE=""
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 1
done
test "$PHASE" = "Running"
sleep 2
EARLY_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/null
FINAL_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 created
startup pod=slow-start-<template hash>-<suffix> early_ready=False restarts=0 final_ready=true final_restarts=0

Lab 5 — 성공 rollout 동안 Service 요청 연속성 측정하기

섹션 제목: “Lab 5 — 성공 rollout 동안 Service 요청 연속성 측정하기”

cluster 내부 client가 160회 요청하는 동안 v1→v2 Pod template 변경을 수행합니다. 두 version을 모두 관찰하고 오류가 0인지 확인합니다. 이는 짧은 fixture의 관측값이지 보편적인 무중단 보장이 아닙니다.

Terminal window
set -euo pipefail
test "$(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/null
kubectl --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/null
kubectl --context k3d-cs-study-workbook -n "$NS" wait \
--for=condition=Ready pod/rollout-client --timeout=30s >/dev/null
for _ 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 1
done
test "$BEFORE_LINES" -ge 3
kubectl --context k3d-cs-study-workbook -n "$NS" set env deployment/api-service \
APP_VERSION=v2 FAIL_READY=false >/dev/null
kubectl --context k3d-cs-study-workbook -n "$NS" \
rollout status deployment/api-service --timeout=90s >/dev/null
for _ 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 1
done
test "$PHASE" = "Succeeded"
kubectl --context k3d-cs-study-workbook -n "$NS" logs rollout-client \
>/tmp/k8s-wb-m9/success-client.log
read 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)
EOF
READY="$(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 0
test "$V2" -gt 0
test "$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/2

Lab 6 — readiness 불량 revision을 멈추고 rollback하기

섹션 제목: “Lab 6 — readiness 불량 revision을 멈추고 rollback하기”

v3는 의도적으로 /ready를 만들지 않습니다. maxUnavailable: 0이므로 v2 endpoint 2개를 유지한 채 rollout이 progress deadline을 넘겨 실패해야 합니다. 요청 기록에서 v3가 0건인지 확인한 뒤 이전 Pod template으로 undo합니다.

#하는 일중단되면 먼저 볼 대상
1260회 Service 요청 client 시작failure-client phase와 logs
2v3 실패 주입과 Deployment 상태 판독failed-rollout.log, Progressing condition
3응답 수·version·endpoint 검산failure-client.log, EndpointSlice
4undo 경고 캡처와 v2 수렴 확인undo.log, Pod template, Ready·updated replica
Terminal window
set -euo pipefail
test "$(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/null
kubectl --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/null
kubectl --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/null
set +e
kubectl --context k3d-cs-study-workbook -n "$NS" rollout status \
deployment/api-service --timeout=30s >/tmp/k8s-wb-m9/failed-rollout.log 2>&1
ROLLOUT_EXIT=$?
set -e
test "$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 1
test "$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 1
done
test "$PHASE" = "Succeeded"
kubectl --context k3d-cs-study-workbook -n "$NS" logs failure-client \
>/tmp/k8s-wb-m9/failure-client.log
read 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)
EOF
test "$TOTAL" = "260"
test "$ERRORS" = "0"
test "$V2" = "260"
test "$V3" = "0"
test "$OTHER" = "0"
# 4) live Pod template을 undo하고 v2 revision의 완전한 수렴을 확인한다.
set +e
kubectl --context k3d-cs-study-workbook -n "$NS" rollout undo \
deployment/api-service >/tmp/k8s-wb-m9/undo.log 2>&1
UNDO_EXIT=$?
set -e
test "$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 ;;
esac
FINAL_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 1
done
test "$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=0
rollback version=v2 fail_ready=false ready=2/2 undo_warning=last-applied-stale

kubectl rollout undo는 live Pod template을 복원했지만, 이 fixture를 처음 만든 client-side kubectl applylast-applied-configuration annotation은 갱신하지 않는다는 경고를 냅니다. 따라서 이후 이 fixture의 v1 선언을 다시 apply하면 선언 파일의 Pod template 값이 live v2에 다시 반영될 수 있습니다. 실무 GitOps에서는 undo만 실행하고 끝내지 않고, 선언 원본을 정상 revision으로 되돌린 PR과 live diff 수렴까지 함께 확인합니다.

공유 k3d cluster는 유지하고 모듈 namespace와 /tmp fixture만 제거합니다.

Terminal window
set -euo pipefail
test "$(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/null
test -z "$(kubectl --context k3d-cs-study-workbook get namespace "$NS" \
--ignore-not-found -o name)"
rm -rf /tmp/k8s-wb-m9
test ! -e /tmp/k8s-wb-m9
NODE_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=retained
  1. readiness 실패 후 restart count는 그대로지만 Ready endpoint는 2→1로 줄어듭니다.
  2. readiness가 회복되면 같은 Pod가 다시 endpoint에 들어옵니다.
  3. liveness 실패는 container restart count를 올리고 시작 script가 다시 실행됩니다.
  4. startup 성공 전에는 공격적인 liveness가 실행되지 않아 느린 시작이 restart 없이 끝납니다.
  5. 성공 rollout에서 v1과 v2 응답을 모두 받지만 이 fixture의 요청 오류는 0입니다.
  6. readiness가 실패하는 v3는 Service 응답에 한 번도 나타나지 않고 old v2 endpoint 2개가 남습니다.
  7. rollout undo 뒤 Deployment Pod template과 Ready replica가 v2·2/2로 복원됩니다.
  8. cleanup 뒤 module namespace, Deployment, Service, client Pod와 /tmp가 모두 0입니다.

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 호환성, 외부 부작용의 보상·중단 절차를 별도로 승인해야 합니다.

함정 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은 호환성 설계와 관측 기준을 보완하는 수단이지 대체물이 아닙니다.

다음 M10에서는 Pod template을 되돌려도 남아야 하는 데이터와 일회용 Pod filesystem을 구분하고, PV·PVC와 stateless 서비스 경계를 판단합니다.