M11. 장애 디버깅 플레이북
1. 왜 필요한가
섹션 제목: “1. 왜 필요한가”NKS의 web-app, api-service, ai-service가 멈췄을 때 상태 문자열만 AI에 넘기면
image·설정·스케줄링·메모리 중 잘못된 층을 고칠 수 있습니다. 이 모듈에서는 관찰한 사실과
추론을 분리하고, 증거를 보존한 뒤 가장 작은 복구를 검증하는 순서를 손에 익힙니다.
개념 정의의 정본은 Kubernetes Basics,
Docker Basics, Linux Basics입니다. 원본은 각각
content/topics/L5/kubernetes-basics.mdx, content/topics/L5/docker-basics.mdx,
content/topics/L4/linux-basics.mdx입니다. M11은 container state를 다시 정의하지 않고 M2의
get·describe·logs, M5의 설정, M6의 scheduling, M9의 rollout, M10의 claim 진단을 하나의
실행 플레이북으로 묶습니다.
2. 핵심 개념
섹션 제목: “2. 핵심 개념”flowchart LR Scope["1. 범위 context·namespace·owner"] --> State["2. 현재 상태 Pod phase·container state"] State --> Evidence["3. 실패 증거 Events·logs --previous·lastState"] Evidence --> Layer["4. 원인 계층 image·config·scheduler·memory"] Layer --> Fix["5. 최소 복구 spec 한 축 수정"] Fix --> Verify["6. 종료 조건 Ready·restart·잔여 실패"]
STATUS는 원인이 아니라 현재 보이는 증상입니다.
| 먼저 읽을 값 | 답하는 질문 | 이 랩의 예시 |
|---|---|---|
Pod phase, .spec.nodeName | Pod가 Node에 배치됐는가? | Pending + node 없음 |
container state.waiting.reason | container 시작 전 어느 단계에서 막혔는가? | image pull, restart backoff |
lastState.terminated | 직전 실행은 왜, 어떤 exit code로 끝났는가? | Error/23, OOMKilled/137 |
kubectl logs --previous | 재시작 전에 종료된 container가 남긴 것은? | startup-config-missing |
| Pod Events | kubelet·scheduler가 기록한 실패는? | Failed, FailedScheduling, BackOff |
| owner의 desired spec | 관찰한 Pod를 만든 선언에서 무엇을 고칠 것인가? | image, env, request·limit, command |
| 증상 | 실패한 경계 | 결정적 증거 | 첫 복구 후보 |
|---|---|---|---|
CrashLoopBackOff | 시작한 process가 반복 종료 | lastState, exit code, logs --previous | command·config·dependency 확인 |
ErrImagePull/ImagePullBackOff | image를 받아 container 생성 전 | waiting reason, pull 실패 Event | image·registry auth·Node egress |
Pending + FailedScheduling | feasible Node를 찾지 못함 | PodScheduled=False, scheduler Event, requests | 제약·request·node pool capacity |
OOMKilled/137 | container process가 OOM kill됨 | lastState, limit·Node Event, 사용 패턴 | limit 초과·Node pressure를 구분 |
BackOff는 영구 원인명이 아니라 재시도 간격을 늘리는 상태입니다. OOMKilled의 137도 단독으로
“limit만 올리라”는 결론이 아닙니다.
flowchart TB Pod["Pod status"] --> Scheduler["scheduler Pending·FailedScheduling"] Pod --> Kubelet["kubelet/runtime image pull·restart·OOM"] Kubelet --> Current["logs 현재 container"] Kubelet --> Previous["logs --previous 직전 container"] Pod --> Owner["Deployment/Helm desired spec 지속 복구의 수정 대상"] Owner -. "Pod 직접 수정은 재생성 때 소실" .-> Pod
예시 3서비스 시스템 장애 메모의 최소 형식
섹션 제목: “예시 3서비스 시스템 장애 메모의 최소 형식”| 필드 | 기록 예시 | 피할 표현 |
|---|---|---|
| 범위 | ai-service, 실습용 fixture namespace, Pod 1개 | “NKS 전체 장애” |
| 관찰 | PodScheduled=False, Event Insufficient cpu | “아마 autoscaler 버그” |
| 최근 변경 | CPU request만 이전 revision 대비 증가 | 근거 없는 원인 단정 |
| 조치 | request를 fixture 정상값으로 복구 | 여러 축 동시 변경 |
| 종료 조건 | Ready=True, 새 container restart 0, 실패 fixture 제거 | apply exit 0만 확인 |
시점 의존 설명은 2026-07-16에 다음 공식 1차 자료로 확인했습니다.
- Debug Pods: Pod 상태와 recent
Events를 먼저 확인하고
Pending의 scheduler message를 좁히는 순서 - Pod lifecycle: Pod phase,
container state, restart backoff와
CrashLoopBackOff - Images:
ImagePullBackOff와 증가하는 재시도 지연 - Assign Memory Resources:
memory limit 초과,
OOMKilled, exit 137 - Kubernetes Scheduler: filtering 뒤 feasible Node가 없으면 unscheduled로 남는 경계
- NKS 문제 해결: NKS의 worker Node,
ImagePullBackOff, DNS, LoadBalancer, autoscaling 후속 진단 경계
실제 NKS registry 인증, worker Node 교체, control plane·Cilium·LoadBalancer 장애와 console 조작은
승인된 non-production 환경이 없어 official-doc-only입니다. production 변경은 excluded입니다.
etcd, CNI 내부 구현, kubelet 직접 운영, Terraform 작성은 범위 밖입니다. k3d의 Event 문구와 재시도
시간은 NKS 실측값이 아닙니다.
3. 직관 비유
섹션 제목: “3. 직관 비유”| 개념 | 비유 | 비유의 경계 |
|---|---|---|
| 상태 문자열 | 계기판의 경고등 | 경고등 하나가 고장 부품을 확정하지는 않습니다. |
| Event | 현장 담당자가 남긴 최근 작업 기록 | 영구 감사 로그가 아닙니다. |
logs --previous | 교대 직전 작업자가 남긴 마지막 메모 | stdout·stderr 밖의 상태는 보이지 않습니다. |
| 최소 복구 | 차단기 하나를 고친 뒤 정상 전압을 다시 재는 과정 | apply 성공만으로 사용자 경로를 증명하지 않습니다. |
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; k8s-wb-m11-<UTC run ID> |
모든 shell lab은 executed-local입니다. NKS 장애 조작은 official-doc-only, production 변경은
excluded이며 예상 출력을 쓰지 않습니다.
사전 조건
섹션 제목: “사전 조건”- 저장소 root에서 실행하며 M0~M10을 완료한 상태여야 합니다.
- Docker Desktop과 M2의
cs-study-workbookcluster가 실행 중이어야 합니다. - 모든 API 명령은 고정 context와 고유 namespace를 명시합니다.
- 각 fence는 별도 Bash process에서 블록 단위로 실행합니다. Lab 1~4의 exit 1은 의도한 장애를 관찰하고 같은 블록에서 복구까지 검증했다는 결과입니다.
- 예상 소요 시간은 100분입니다.
긴 block을 외우지 말고 아래의 진단 명령 → raw evidence → 판단 → 복구만 먼저 따라가세요. loop와 assertion은 변동하는 상태가 수렴할 때까지 자동으로 확인하는 안전장치입니다.
| Lab | 사람이 먼저 실행할 진단 | raw evidence에서 찾을 값 | 이 랩의 최소 복구 |
|---|---|---|---|
| 1 | get pod → describe pod → logs --previous | restart, BackOff, exit 23, 직전 log | owner Deployment의 command 한 축 patch |
| 2 | get pod → describe pod | waiting reason, pull Failed Event | image reference만 교체 |
| 3 | get pod → describe pod | node 없음, Unschedulable, CPU message | CPU request만 fixture 정상값으로 교체 |
| 4 | get pod → describe pod → logs --previous | OOMKilled, 137, limit, 직전 log | workload 크기와 limit을 근거 값으로 교체 |
Setup
섹션 제목: “Setup”고유 namespace 만들기
섹션 제목: “고유 namespace 만들기”실패 시 이 실행이 만든 namespace만 rollback합니다. 삭제도 실패하면 /tmp 소유권 표식을 보존하고
exit 70으로 중단합니다.
set -euo pipefailrm -rf /tmp/k8s-wb-m11mkdir -p /tmp/k8s-wb-m11chmod 700 /tmp/k8s-wb-m11node automation/k8s-workbook/preflight.mjs --require-clustertest "$(kubectl config current-context)" = "k3d-cs-study-workbook"NS="k8s-wb-m11-$(date -u +%Y%m%d%H%M%S)"printf '%s' "$NS" >/tmp/k8s-wb-m11/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-m11 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-m11 and inspect namespace' >&2 exit 70 fi rm -rf /tmp/k8s-wb-m11 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-m11-<UTC run ID> context_guard=passLab 1 — CrashLoopBackOff: 직전 process 증거 보존
섹션 제목: “Lab 1 — CrashLoopBackOff: 직전 process 증거 보존”set -euo pipefailtest "$(kubectl config current-context)" = "k3d-cs-study-workbook"NS="$(cat /tmp/k8s-wb-m11/namespace)"kubectl --context k3d-cs-study-workbook -n "$NS" apply -f - <<'YAML' >/dev/nullapiVersion: apps/v1kind: Deploymentmetadata: name: crash-loopspec: replicas: 1 selector: matchLabels: {app: api-service, fixture: m11} template: metadata: labels: {app: api-service, fixture: m11} spec: containers: - name: app image: busybox:1.37.0@sha256:9532d8c39891ca2ecde4d30d7710e01fb739c87a8b9299685c63704296b16028 command: ["sh", "-c", "echo startup-config-missing >&2; sleep 20; exit 23"]YAMLRESTARTS="0"BACKOFF=""RUNNING=""POD=""for _ in $(seq 1 120); do POD="$(kubectl --context k3d-cs-study-workbook -n "$NS" get pod \ -l app=api-service,fixture=m11 -o jsonpath='{.items[0].metadata.name}' 2>/dev/null || true)" test -n "$POD" || { sleep 1; continue; } RESTARTS="$(kubectl --context k3d-cs-study-workbook -n "$NS" get pod "$POD" \ -o jsonpath='{.status.containerStatuses[0].restartCount}' 2>/dev/null || true)" BACKOFF="$(kubectl --context k3d-cs-study-workbook -n "$NS" get events \ --field-selector involvedObject.kind=Pod,involvedObject.name="$POD" \ -o jsonpath='{range .items[*]}{.reason}{"\n"}{end}' 2>/dev/null \ | awk '$1 == "BackOff" {print; exit}')" RUNNING="$(kubectl --context k3d-cs-study-workbook -n "$NS" get pod "$POD" \ -o jsonpath='{.status.containerStatuses[0].state.running.startedAt}' 2>/dev/null || true)" test "${RESTARTS:-0}" -ge 1 && test "$BACKOFF" = "BackOff" && test -n "$RUNNING" && break sleep 1doneLAST_REASON="$(kubectl --context k3d-cs-study-workbook -n "$NS" get pod "$POD" \ -o jsonpath='{.status.containerStatuses[0].lastState.terminated.reason}')"LAST_EXIT="$(kubectl --context k3d-cs-study-workbook -n "$NS" get pod "$POD" \ -o jsonpath='{.status.containerStatuses[0].lastState.terminated.exitCode}')"PREVIOUS="$(kubectl --context k3d-cs-study-workbook -n "$NS" logs "$POD" --previous)"kubectl --context k3d-cs-study-workbook -n "$NS" describe pod "$POD" \ >/tmp/k8s-wb-m11/crash.describegrep -q 'BackOff' /tmp/k8s-wb-m11/crash.describetest "$BACKOFF" = "BackOff"test -n "$RUNNING"test "$LAST_REASON" = "Error"test "$LAST_EXIT" = "23"test "$RESTARTS" -ge 1test "$PREVIOUS" = "startup-config-missing"OLD_UID="$(kubectl --context k3d-cs-study-workbook -n "$NS" get pod "$POD" -o jsonpath='{.metadata.uid}')"OWNER_KIND="$(kubectl --context k3d-cs-study-workbook -n "$NS" get pod "$POD" \ -o jsonpath='{.metadata.ownerReferences[0].kind}')"test "$OWNER_KIND" = "ReplicaSet"kubectl --context k3d-cs-study-workbook -n "$NS" patch deployment crash-loop --type=json \ -p='[{"op":"replace","path":"/spec/template/spec/containers/0/command","value":["sh","-c","mkdir -p /www; echo ok >/www/health; httpd -f -p 8080 -h /www"]}]' >/dev/nullkubectl --context k3d-cs-study-workbook -n "$NS" rollout status deployment/crash-loop \ --timeout=90s >/dev/nullNEW_RS="$(kubectl --context k3d-cs-study-workbook -n "$NS" get rs \ -l app=api-service,fixture=m11 --sort-by=.metadata.creationTimestamp \ -o jsonpath='{.items[-1:].metadata.name}')"NEW_POD="$(kubectl --context k3d-cs-study-workbook -n "$NS" get pod \ -l app=api-service,fixture=m11 \ -o jsonpath='{range .items[*]}{.metadata.name}{"="}{.metadata.ownerReferences[0].name}{"\n"}{end}' \ | awk -F= -v rs="$NEW_RS" '$2 == rs {print $1; exit}')"kubectl --context k3d-cs-study-workbook -n "$NS" wait \ --for=condition=Ready pod/"$NEW_POD" --timeout=60s >/dev/nullNEW_UID="$(kubectl --context k3d-cs-study-workbook -n "$NS" get pod "$NEW_POD" -o jsonpath='{.metadata.uid}')"REQUEST="$(kubectl --context k3d-cs-study-workbook -n "$NS" exec "$NEW_POD" -- \ wget -qO- http://127.0.0.1:8080/health)"test "$OLD_UID" != "$NEW_UID"test "$REQUEST" = "ok"printf 'crash pattern=repeated-exit event=%s last_exit=%s previous_log=%s owner=Deployment desired_patch=command pod_replaced=true request=%s recovery=Ready\n' \ "$BACKOFF" "$LAST_EXIT" "$PREVIOUS" "$REQUEST"exit 1예상 출력(exit 1):
crash pattern=repeated-exit event=BackOff last_exit=23 previous_log=startup-config-missing owner=Deployment desired_patch=command pod_replaced=true request=ok recovery=Readyrestart count, 화면에 순간적으로 보이는 Running·Error·CrashLoopBackOff, backoff 수렴 시간은
변동 필드입니다. 이 랩은 반복 exit와 BackOff Event를 함께 확인하고, 두 번째 실행이 살아 있는 동안
--previous를 보존합니다.
Lab 2 — ImagePullBackOff: process 시작 전 경계
섹션 제목: “Lab 2 — ImagePullBackOff: process 시작 전 경계”set -euo pipefailtest "$(kubectl config current-context)" = "k3d-cs-study-workbook"NS="$(cat /tmp/k8s-wb-m11/namespace)"kubectl --context k3d-cs-study-workbook -n "$NS" run image-pull \ --image=busybox:missing-m11 --image-pull-policy=Always \ --labels=app=web-app,fixture=m11 --command -- sh -c 'sleep 3600' >/dev/nullPULL_REASON=""for _ in $(seq 1 60); do PULL_REASON="$(kubectl --context k3d-cs-study-workbook -n "$NS" get pod image-pull \ -o jsonpath='{.status.containerStatuses[0].state.waiting.reason}' 2>/dev/null || true)" case "$PULL_REASON" in ErrImagePull|ImagePullBackOff) break ;; esac sleep 1doneEVENT_REASON="$(kubectl --context k3d-cs-study-workbook -n "$NS" get events \ --field-selector involvedObject.kind=Pod,involvedObject.name=image-pull \ -o jsonpath='{range .items[*]}{.reason}{"\n"}{end}' | awk '$1 == "Failed" {print; exit}')"case "$PULL_REASON" in ErrImagePull|ImagePullBackOff) ;; *) exit 2 ;; esactest "$EVENT_REASON" = "Failed"kubectl --context k3d-cs-study-workbook -n "$NS" describe pod image-pull \ >/tmp/k8s-wb-m11/image.describegrep -Eq 'ErrImagePull|ImagePullBackOff|Failed to pull image' /tmp/k8s-wb-m11/image.describekubectl --context k3d-cs-study-workbook -n "$NS" delete pod image-pull --wait=true >/dev/nullkubectl --context k3d-cs-study-workbook -n "$NS" run image-pull \ --image=busybox:1.37.0@sha256:9532d8c39891ca2ecde4d30d7710e01fb739c87a8b9299685c63704296b16028 \ --labels=app=web-app,fixture=m11 --command -- sh -c 'sleep 3600' >/dev/nullkubectl --context k3d-cs-study-workbook -n "$NS" wait \ --for=condition=Ready pod/image-pull --timeout=60s >/dev/nullprintf 'image symptom=%s event=%s app_log=not-started recovery=Ready\n' \ "$PULL_REASON" "$EVENT_REASON"exit 1예상 출력(exit 1):
image symptom=<ErrImagePull|ImagePullBackOff> event=Failed app_log=not-started recovery=Readyregistry 상세 문구와 reason 전환 시점은 변동 필드입니다. NKS에서는 tag 외에 registry 인증·Node egress도 확인해야 합니다.
Lab 3 — Pending: scheduler 제약 읽기
섹션 제목: “Lab 3 — Pending: scheduler 제약 읽기”set -euo pipefailtest "$(kubectl config current-context)" = "k3d-cs-study-workbook"NS="$(cat /tmp/k8s-wb-m11/namespace)"kubectl --context k3d-cs-study-workbook -n "$NS" apply -f - <<'YAML' >/dev/nullapiVersion: v1kind: Podmetadata: name: pending-cpu labels: {app: ai-service, fixture: m11}spec: containers: - name: app image: busybox:1.37.0@sha256:9532d8c39891ca2ecde4d30d7710e01fb739c87a8b9299685c63704296b16028 command: ["sh", "-c", "sleep 3600"] resources: requests: {cpu: "100000"}YAMLREASON=""for _ in $(seq 1 60); do REASON="$(kubectl --context k3d-cs-study-workbook -n "$NS" get pod pending-cpu \ -o jsonpath='{.status.conditions[?(@.type=="PodScheduled")].reason}' 2>/dev/null || true)" test "$REASON" = "Unschedulable" && break sleep 1donePHASE="$(kubectl --context k3d-cs-study-workbook -n "$NS" get pod pending-cpu -o jsonpath='{.status.phase}')"NODE="$(kubectl --context k3d-cs-study-workbook -n "$NS" get pod pending-cpu -o jsonpath='{.spec.nodeName}')"MESSAGE="$(kubectl --context k3d-cs-study-workbook -n "$NS" get pod pending-cpu \ -o jsonpath='{.status.conditions[?(@.type=="PodScheduled")].message}')"test "$PHASE" = "Pending"test "$REASON" = "Unschedulable"test -z "$NODE"case "$MESSAGE" in *"Insufficient cpu"*) ;; *) exit 2 ;; esackubectl --context k3d-cs-study-workbook -n "$NS" describe pod pending-cpu \ >/tmp/k8s-wb-m11/pending.describegrep -q 'Insufficient cpu' /tmp/k8s-wb-m11/pending.describekubectl --context k3d-cs-study-workbook -n "$NS" delete pod pending-cpu --wait=true >/dev/nullkubectl --context k3d-cs-study-workbook -n "$NS" apply -f - <<'YAML' >/dev/nullapiVersion: v1kind: Podmetadata: name: pending-cpu labels: {app: ai-service, fixture: m11}spec: containers: - name: app image: busybox:1.37.0@sha256:9532d8c39891ca2ecde4d30d7710e01fb739c87a8b9299685c63704296b16028 command: ["sh", "-c", "sleep 3600"] resources: requests: {cpu: 10m}YAMLkubectl --context k3d-cs-study-workbook -n "$NS" wait \ --for=condition=Ready pod/pending-cpu --timeout=60s >/dev/nullprintf 'pending phase=%s scheduled_reason=%s node=none cause=Insufficient-cpu recovery=Ready\n' \ "$PHASE" "$REASON"exit 1예상 출력(exit 1):
pending phase=Pending scheduled_reason=Unschedulable node=none cause=Insufficient-cpu recovery=ReadyNode 개수와 scheduler 상세 message는 변동 필드입니다. production request 축소 전에는 실제 peak와 SLO를 검토해야 합니다.
Lab 4 — OOMKilled: 종료 이유와 limit 함께 읽기
섹션 제목: “Lab 4 — OOMKilled: 종료 이유와 limit 함께 읽기”첫 container process가 heap을 빠르게 늘려 cgroup OOM을 재현합니다. process가 종료되면 그
메모리는 해제되고, disk-backed marker를 본 두 번째 container는 안정적으로 대기하므로 직전
상태와 log를 함께 보존할 수 있습니다. memory-backed volume에 OOM fixture를 남기면 다음
container init도 OOM되어 StartError가 원래 증거를 덮을 수 있으므로 사용하지 않습니다.
set -euo pipefailtest "$(kubectl config current-context)" = "k3d-cs-study-workbook"NS="$(cat /tmp/k8s-wb-m11/namespace)"kubectl --context k3d-cs-study-workbook -n "$NS" apply -f - <<'YAML' >/dev/nullapiVersion: v1kind: Podmetadata: name: oom-memory labels: {app: ai-service, fixture: m11}spec: containers: - name: app image: busybox:1.37.0@sha256:9532d8c39891ca2ecde4d30d7710e01fb739c87a8b9299685c63704296b16028 command: - sh - -c - | if test -e /state/after-oom; then echo oom-restart-stable sleep 3600 else touch /state/after-oom echo oom-fixture-start awk 'BEGIN { s="0123456789abcdef"; while (1) s = s s }' fi resources: requests: {memory: 4Mi} limits: {memory: 16Mi} volumeMounts: - {name: state, mountPath: /state} volumes: - name: state emptyDir: {}YAMLLAST_REASON=""LAST_EXIT=""RESTARTS="0"PREVIOUS=""RUNNING=""for _ in $(seq 1 120); do LAST_REASON="$(kubectl --context k3d-cs-study-workbook -n "$NS" get pod oom-memory \ -o jsonpath='{.status.containerStatuses[0].lastState.terminated.reason}' 2>/dev/null || true)" LAST_EXIT="$(kubectl --context k3d-cs-study-workbook -n "$NS" get pod oom-memory \ -o jsonpath='{.status.containerStatuses[0].lastState.terminated.exitCode}' 2>/dev/null || true)" RESTARTS="$(kubectl --context k3d-cs-study-workbook -n "$NS" get pod oom-memory \ -o jsonpath='{.status.containerStatuses[0].restartCount}' 2>/dev/null || true)" RUNNING="$(kubectl --context k3d-cs-study-workbook -n "$NS" get pod oom-memory \ -o jsonpath='{.status.containerStatuses[0].state.running.startedAt}' 2>/dev/null || true)" PREVIOUS="$(kubectl --context k3d-cs-study-workbook -n "$NS" logs oom-memory --previous \ 2>/dev/null | sed -n '1p' || true)" if test "$LAST_REASON" = "OOMKilled" && test "$LAST_EXIT" = "137" \ && test "${RESTARTS:-0}" -ge 1 && test -n "$RUNNING" \ && test "$PREVIOUS" = "oom-fixture-start"; then break fi sleep 1donetest "$LAST_REASON" = "OOMKilled"test "$LAST_EXIT" = "137"test "$RESTARTS" -ge 1test -n "$RUNNING"test "$PREVIOUS" = "oom-fixture-start"kubectl --context k3d-cs-study-workbook -n "$NS" describe pod oom-memory \ >/tmp/k8s-wb-m11/oom.describegrep -q 'OOMKilled' /tmp/k8s-wb-m11/oom.describekubectl --context k3d-cs-study-workbook -n "$NS" delete pod oom-memory --wait=true >/dev/nullkubectl --context k3d-cs-study-workbook -n "$NS" apply -f - <<'YAML' >/dev/nullapiVersion: v1kind: Podmetadata: name: oom-memory labels: {app: ai-service, fixture: m11}spec: containers: - name: app image: busybox:1.37.0@sha256:9532d8c39891ca2ecde4d30d7710e01fb739c87a8b9299685c63704296b16028 command: ["sh", "-c", "dd if=/dev/zero of=/memory/blob bs=1M count=4; sleep 3600"] resources: requests: {memory: 4Mi} limits: {memory: 64Mi} volumeMounts: - {name: memory, mountPath: /memory} volumes: - name: memory emptyDir: {medium: Memory, sizeLimit: 8Mi}YAMLkubectl --context k3d-cs-study-workbook -n "$NS" wait \ --for=condition=Ready pod/oom-memory --timeout=60s >/dev/nullprintf 'oom last_reason=%s last_exit=%s restarts=>=1 previous_log=%s recovery=Ready\n' \ "$LAST_REASON" "$LAST_EXIT" "$PREVIOUS"exit 1예상 출력(exit 1):
oom last_reason=OOMKilled last_exit=137 restarts=>=1 previous_log=oom-fixture-start recovery=Readyrestart count와 OOM 수렴 시간은 변동 필드입니다. 실제 서비스에서는 kubectl top 한 시점만으로
누수·peak·working set을 구분할 수 없습니다.
Lab 5 — 복구 종료 조건 확인
섹션 제목: “Lab 5 — 복구 종료 조건 확인”set -euo pipefailtest "$(kubectl config current-context)" = "k3d-cs-study-workbook"NS="$(cat /tmp/k8s-wb-m11/namespace)"READY="$(kubectl --context k3d-cs-study-workbook -n "$NS" get pod -l fixture=m11 \ -o jsonpath='{range .items[*]}{range .status.conditions[?(@.type=="Ready")]}{.status}{"\n"}{end}{end}' \ | awk '$1 == "True" {count++} END {print count+0}')"TOTAL="$(kubectl --context k3d-cs-study-workbook -n "$NS" get pod -l fixture=m11 \ -o jsonpath='{.items[*].metadata.name}' | wc -w | tr -d ' ')"RESTART_SUM="$(kubectl --context k3d-cs-study-workbook -n "$NS" get pod -l fixture=m11 \ -o jsonpath='{range .items[*]}{.status.containerStatuses[0].restartCount}{"\n"}{end}' \ | awk '{sum += $1} END {print sum+0}')"test "$TOTAL" = "4"test "$READY" = "4"test "$RESTART_SUM" = "0"printf 'recovery pods=%s ready=%s current_restarts=%s evidence_before_delete=preserved\n' \ "$TOTAL" "$READY" "$RESTART_SUM"예상 출력(exit 0):
recovery pods=4 ready=4 current_restarts=0 evidence_before_delete=preservedCleanup
섹션 제목: “Cleanup”자원 제거와 검증
섹션 제목: “자원 제거와 검증”set -euo pipefailtest "$(kubectl config current-context)" = "k3d-cs-study-workbook"NS="$(cat /tmp/k8s-wb-m11/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-m11test ! -e /tmp/k8s-wb-m11MODULE_PODS="$(kubectl --context k3d-cs-study-workbook get pods -A -l fixture=m11 \ --no-headers 2>/dev/null | wc -l | tr -d ' ')"NODE_READY="$(kubectl --context k3d-cs-study-workbook get nodes --no-headers \ | awk '$2 == "Ready" {count++} END {print count+0}')"test "$MODULE_PODS" = "0"test "$NODE_READY" = "2"printf 'cleanup namespace=0 pods=0 tmp=0 nodes_ready=%s cluster=retained\n' "$NODE_READY"예상 출력(exit 0):
cleanup namespace=0 pods=0 tmp=0 nodes_ready=2 cluster=retained5. 관찰 포인트
섹션 제목: “5. 관찰 포인트”| 관찰 | 운영 판단 |
|---|---|
logs --previous에 시작 실패 메시지 | 재시작·삭제 전에 직전 container 증거를 보존합니다. |
| pull Event는 있지만 app log는 없음 | app code보다 image 경계를 먼저 봅니다. |
Pending, nodeName 없음 | 사용률보다 requests·제약·Node capacity를 비교합니다. |
OOMKilled, 137, memory limit | limit 상향 전에 peak·누수·batch를 구분합니다. |
| 새 Pod 4개 Ready, restart 합 0 | 조치 적용과 runtime 회복을 분리해 종료 조건을 봅니다. |
6. 셀프체크 Q&A
섹션 제목: “6. 셀프체크 Q&A”Q1. api-service가 CrashLoopBackOff이고 AI가 바로 Pod 삭제를 제안했습니다. 승인하시겠습니까?
섹션 제목: “Q1. api-service가 CrashLoopBackOff이고 AI가 바로 Pod 삭제를 제안했습니다. 승인하시겠습니까?”바로 승인하지 않습니다. context·namespace·owner를 확인하고 lastState, exit code,
logs --previous, Events를 먼저 보존합니다. owner의 command·설정 reference를 한 축만 수정하고
Ready·restart·사용자 경로를 종료 조건으로 봅니다.
Q2. ai-service Pod가 Pending이고 Node CPU 사용률은 25%입니다. node pool을 늘릴까요?
섹션 제목: “Q2. ai-service Pod가 Pending이고 Node CPU 사용률은 25%입니다. node pool을 늘릴까요?”사용률만으로 결정하지 않습니다. .spec.nodeName, PodScheduled, scheduler Event에서 requests,
taint, affinity, storage topology 중 어떤 filtering 조건이 feasible Node를 0으로 만들었는지 봅니다.
실제 capacity 부족일 때만 NKS node pool/Cluster Autoscaler를 검토합니다.
Q3. web-app이 OOMKilled/137이고 AI가 memory limit을 4배로 올렸습니다. 무엇을 요구하시겠습니까?
섹션 제목: “Q3. web-app이 OOMKilled/137이고 AI가 memory limit을 4배로 올렸습니다. 무엇을 요구하시겠습니까?”직전 lastState, requests·limits, 정상 p95·peak, 시작 시점인지 특정 요청 뒤인지, 누수 추세와 Node
pressure를 요구합니다. limit 오류면 근거 있는 값으로 조정하고, buffering·batch·누수면 code를
고칩니다. 새 rollout의 Ready, restart 0, memory 안정과 실제 요청 성공을 확인합니다.
7. 흔한 함정
섹션 제목: “7. 흔한 함정”함정 1 — 상태 문자열을 원인으로 번역하기
섹션 제목: “함정 1 — 상태 문자열을 원인으로 번역하기”BackOff는 재시도 상태입니다. 상세 원인은 직전 상태·log·Event·spec에서 확인합니다.
함정 2 — 증거를 지우고 나서 디버깅하기
섹션 제목: “함정 2 — 증거를 지우고 나서 디버깅하기”Pod 삭제·rollout 전에 필요한 상태·reason·exit·revision을 보존하되 실제 Secret 값은 복사하지 않습니다.
함정 3 — 여러 축을 동시에 고치기
섹션 제목: “함정 3 — 여러 축을 동시에 고치기”image, env, probe, resources를 한 번에 바꾸지 않습니다. 가설 하나, 최소 변경 하나, 종료 조건 하나로 진행합니다.
8. 다음 모듈 연결 고리
섹션 제목: “8. 다음 모듈 연결 고리”다음 M12에서는 이 플레이북을 관리형 NKS 경계와 예시 3서비스 topology에 적용해 migration 전 검토, 배포, 장애 판단, rollback까지 하나의 capstone으로 통합합니다.