Certified Kubernetes Administrator (CKA) #22 Troubleshooting 1: Pod와 앱 (Pending, CrashLoop, ImagePull, OOM)
#21 Helm과 Kustomize까지 매니페스트를 만들고 배포하는 도메인을 모두 끝냈습니다. 이제부터 네 편은 이미 망가진 것을 고치는 트러블슈팅입니다. CKA 시험에서 Troubleshooting은 30%로 가장 비중이 큰 도메인입니다. 다섯 도메인 가운데 무언가를 새로 만드는 작업보다, 누군가 망가뜨려 둔 클러스터를 추적해 고치는 작업의 점수가 가장 큽니다. 합격선이 66%이므로 이 30%를 놓치면 거의 떨어집니다.
트러블슈팅의 핵심은 추측하지 않는 것입니다. 증상을 보고 원인을 머릿속으로 짐작하는 대신, 클러스터가 이미 기록해 둔 사실(describe의 Events, 컨테이너 로그, 종료 코드)을 순서대로 읽어 내려가면 원인은 거의 스스로 드러납니다. 이번 글에서는 Pod 레벨 장애 네 가지를 그 순서대로 진단하겠습니다.
Troubleshooting이 왜 30%인가 #
CKA의 다섯 도메인 중 Troubleshooting이 30%로 단일 최대입니다. 두 번째인 Cluster Architecture(25%)와 합치면 절반을 넘습니다. 이 비중은 우연이 아닙니다. 클러스터 관리자의 실제 업무가 새 리소스를 만드는 일보다 돌던 것이 멈췄을 때 원인을 찾아 복구하는 일에 더 가깝기 때문입니다.
그래서 시험에서도 트러블슈팅 문제는 “이 Pod가 왜 안 뜨는지 고쳐라” 식으로, 이미 깨진 상태를 던져 줍니다. 매니페스트를 처음부터 쓰는 문제와 달리, 깨진 곳을 빠르게 짚어 내는 진단 속도가 점수를 가릅니다. 이번 편은 그중 가장 자주 나오는 Pod 레벨 장애만 다루고, 노드(#23),control plane(#24),네트워킹(#25)은 다음 편들로 이어집니다.
진단 도구: 읽는 순서가 전부다 #
트러블슈팅에서 명령 자체는 몇 개 안 됩니다. 중요한 것은 어떤 순서로 읽느냐입니다. 다음 순서를 몸에 붙여 두면 대부분의 Pod 장애는 1〜2분 안에 원인이 드러납니다.
# 1) 전체 상태와 STATUS, RESTARTS, AGE를 먼저 본다
k get pod -o wide
# 2) describe의 Events를 가장 먼저 읽는다 (진단의 90%가 여기)
k describe pod <name>
# 3) 컨테이너가 떴다가 죽은 경우, 이전 컨테이너의 로그를 본다
k logs <name>
k logs <name> --previous
# 4) 컨테이너가 여러 개면 -c로 지정
k logs <name> -c <container>
# 5) 클러스터 전체 이벤트를 시간순으로
k get events --sort-by=.metadata.creationTimestamp각 도구가 답하는 질문이 다릅니다.
| 도구 | 답해 주는 질문 |
|---|---|
k get pod -o wide | 지금 STATUS는 무엇이고, 어느 노드에 떴고, RESTARTS는 몇 번인가 |
k describe pod | scheduler,kubelet이 이 Pod에 무슨 일을 했는가 (Events) |
k logs | 앱이 죽기 직전 무슨 메시지를 남겼는가 |
k logs --previous | 재시작 직전, 즉 죽은 그 컨테이너가 무슨 말을 남겼는가 |
k get events | 클러스터 차원에서 최근 무슨 일이 일어났는가 |
시험 포인트: describe의 Events를 가장 먼저 #
가장 흔한 실수가 k logs부터 보는 것입니다. Pending 상태면 컨테이너가 아예 안 떠서 로그가 없습니다. ImagePull 실패도 컨테이너 시작 전이라 로그가 없습니다. 이런 경우 답은 전부 describe의 Events 섹션에 적혀 있습니다. 그래서 순서는 항상 describe(events) 먼저, logs는 그다음입니다. CrashLoop처럼 컨테이너가 떴다 죽은 경우에만 logs --previous가 결정적인 단서가 됩니다.
증상별 원인과 1차 진단 #
네 가지 증상을 한 표로 먼저 잡고, 각각을 재현,해결로 내려가겠습니다.
| 증상(STATUS) | 컨테이너가 떴나 | 1차로 볼 곳 | 대표 원인 |
|---|---|---|---|
| Pending | 안 뜸 | describe Events | 자원 부족, nodeSelector 불일치, taint, PVC 미바인딩 |
| CrashLoopBackOff | 떴다 죽음 | logs --previous | 앱 오류, 잘못된 command, probe 실패 |
| ImagePullBackOff / ErrImagePull | 안 뜸 | describe Events | 이미지 태그 오타, 레지스트리 인증 실패 |
| OOMKilled | 떴다 죽음 | describe의 Last State | 메모리 limit 초과 (exit code 137) |
이 표의 두 번째 열이 진단의 분기점입니다. 컨테이너가 아직 안 떴다면 describe Events(scheduler/kubelet 관점), 떴다 죽었다면 logs –previous(앱 관점)를 봅니다.
1) Pending: 스케줄이 안 된다 #
Pending은 kube-scheduler가 이 Pod를 올릴 노드를 찾지 못한 상태입니다. 컨테이너는 아직 시작도 안 했으므로 로그가 없습니다. 답은 describe의 Events에 있습니다.
재현 #
자원을 과도하게 요구해 어느 노드에도 안 맞게 만듭니다.
k run hungry --image=nginx \
--overrides='{"spec":{"containers":[{"name":"hungry","image":"nginx","resources":{"requests":{"cpu":"100"}}}]}}'진단 #
k get pod hungry
# NAME READY STATUS RESTARTS AGE
# hungry 0/1 Pending 0 20s
k describe pod hungryEvents 섹션에 다음 같은 줄이 핵심입니다.
Events:
Warning FailedScheduling ... 0/3 nodes are available:
3 Insufficient cpu. preemption: 0/3 nodes are available ...FailedScheduling 메시지가 이유를 그대로 말해 줍니다. Pending의 원인은 거의 이 한 줄에서 갈립니다.
| Events에 보이는 문구 | 원인 | 해결 |
|---|---|---|
Insufficient cpu / Insufficient memory | requests가 노드 여유보다 큼 | requests를 낮추거나, 노드 증설/여유 확보 |
node(s) didn't match node selector | nodeSelector 라벨이 어느 노드에도 없음 | 라벨을 노드에 추가하거나 selector 수정 |
node(s) had untolerated taint | 노드 taint에 toleration이 없음 | Pod에 toleration 추가 |
pod has unbound immediate PersistentVolumeClaims | PVC가 PV에 바인딩되지 않음 | PV/StorageClass 확인, PVC 바인딩 해결 |
해결 #
자원 부족이면 requests를 현실적인 값으로 낮춥니다.
k delete pod hungry
k run hungry --image=nginxnodeSelector 불일치면 노드 라벨을 확인하고 맞춥니다.
# 어떤 라벨을 요구하는지
k get pod <name> -o jsonpath='{.spec.nodeSelector}'
# 노드에 그 라벨이 있는지
k get nodes --show-labels
# 노드에 라벨을 붙여 해결하는 경우
k label node node01 disktype=ssdtaint가 원인이면 toleration을 추가하거나(아래), 의도된 taint가 아니라면 노드에서 taint를 제거합니다.
tolerations:
- key: "key1"
operator: "Exists"
effect: "NoSchedule"PVC 미바인딩은 k get pvc와 k get pv로 상태를 확인합니다. StorageClass 동적 프로비저닝이 있으면 자동 바인딩되어야 하고, 정적이면 맞는 PV가 있어야 합니다. 이 부분의 깊은 진단은 스토리지 편(#16,#17)의 내용을 그대로 씁니다.
2) CrashLoopBackOff: 떴다가 계속 죽는다 #
CrashLoopBackOff는 컨테이너가 시작은 했지만 곧 종료되고, kubelet이 재시작을 반복하며 점점 backoff(대기 시간)를 늘리는 상태입니다. RESTARTS 숫자가 계속 올라갑니다. 여기서는 죽은 그 컨테이너의 로그, 즉 logs --previous가 결정적입니다.
재현 #
존재하지 않는 명령을 실행해 즉시 종료시킵니다.
k run crasher --image=busybox --restart=Always -- /bin/sh -c "exit 1"진단 #
k get pod crasher
# NAME READY STATUS RESTARTS AGE
# crasher 0/1 CrashLoopBackOff 3 (20s ago) 60s
# 지금 컨테이너는 backoff 중이라 비어 있을 수 있다
k logs crasher
# 죽은 그 컨테이너의 마지막 로그
k logs crasher --previous--previous가 없으면 backoff 대기 중인 빈 컨테이너를 보게 되어 단서를 놓칩니다. CrashLoop 진단은 거의 항상 --previous로 봅니다.
원인과 해결 #
| 원인 | describe / logs에서의 단서 | 해결 |
|---|---|---|
| 앱 자체 오류로 종료 | logs에 스택트레이스,에러 메시지 | 앱 설정(환경 변수,ConfigMap) 수정 |
| 잘못된 command/args | exec: "..." : not found, exit 127 | command/args를 이미지에 맞게 수정 |
| 필수 설정 누락 | logs에 missing env, 연결 실패 | ConfigMap/Secret 마운트,키 확인 |
| liveness probe 실패 | describe Events에 Liveness probe failed | probe 경로,포트,initialDelaySeconds 조정 |
probe 실패가 원인인 경우는 특히 헷갈립니다. 앱은 멀쩡한데 liveness probe가 너무 빨리, 또는 틀린 경로로 검사해서 kubelet이 정상 컨테이너를 계속 죽이는 경우입니다. describe Events에 Liveness probe failed: ...가 보이면 probe 설정을 의심합니다.
# probe 설정 확인
k get pod crasher -o jsonpath='{.spec.containers[0].livenessProbe}'command 오타로 인한 종료라면 매니페스트의 command/args를 고칩니다. 시험에서는 Deployment를 직접 edit하거나 매니페스트를 수정해 재적용합니다.
k edit deploy <name>
# 또는 매니페스트 수정 후
k apply -f deploy.yaml3) ImagePullBackOff / ErrImagePull: 이미지를 못 가져온다 #
이 둘은 kubelet이 컨테이너 이미지를 끌어오지 못한 상태입니다. ErrImagePull이 먼저 뜨고, 재시도가 backoff에 들어가면 ImagePullBackOff가 됩니다. 컨테이너는 시작조차 못 했으므로 로그는 없고, 원인은 describe의 Events에 있습니다.
재현 #
존재하지 않는 태그를 지정합니다.
k run badimg --image=nginx:doesnotexist진단 #
k get pod badimg
# NAME READY STATUS RESTARTS AGE
# badimg 0/1 ImagePullBackOff 0 30s
k describe pod badimgEvents에서 다음 줄을 봅니다.
Events:
Warning Failed ... Failed to pull image "nginx:doesnotexist":
... manifest for nginx:doesnotexist not found원인과 해결 #
| Events 단서 | 원인 | 해결 |
|---|---|---|
manifest for ... not found | 이미지 이름,태그 오타 | 이미지/태그를 올바른 값으로 수정 |
repository does not exist | 레지스트리 경로 오타, private 저장소 | 전체 경로(registry/repo:tag) 확인 |
pull access denied / unauthorized | 레지스트리 인증 실패 | imagePullSecrets 설정,연결 |
no such host / 타임아웃 | 노드에서 레지스트리 도달 불가 | 노드 네트워크,DNS 확인 |
태그 오타가 가장 흔합니다. 올바른 태그로 바로잡습니다.
k set image pod/badimg badimg=nginx:1.27
# Deployment라면
k set image deploy/<name> <container>=nginx:1.27private 레지스트리 인증 실패라면 imagePullSecret을 만들어 ServiceAccount나 Pod 스펙에 연결합니다.
k create secret docker-registry regcred \
--docker-server=<registry> \
--docker-username=<user> \
--docker-password=<pass>spec:
imagePullSecrets:
- name: regcred4) OOMKilled: 메모리 limit을 넘었다 #
OOMKilled는 컨테이너가 자신의 메모리 limit을 초과해 커널 OOM killer에 의해 강제 종료된 상태입니다. 특징적인 신호는 종료 코드 137(128 + SIGKILL 9)입니다. 컨테이너는 떴다 죽었지만, 죽인 주체가 앱이 아니라 커널이라 로그에는 흔적이 없을 수 있습니다. 단서는 describe의 Last State에 있습니다.
재현 #
작은 limit을 걸고 그보다 많은 메모리를 쓰게 합니다.
k run oom --image=polinux/stress \
--overrides='{"spec":{"containers":[{"name":"oom","image":"polinux/stress","resources":{"limits":{"memory":"20Mi"}},"command":["stress"],"args":["--vm","1","--vm-bytes","250M"]}]}}'진단 #
k get pod oom
# NAME READY STATUS RESTARTS AGE
# oom 0/1 OOMKilled 2 (10s ago) 40s # 또는 CrashLoopBackOff로 반복
k describe pod oomdescribe에서 다음 부분이 결정적입니다.
Last State: Terminated
Reason: OOMKilled
Exit Code: 137Reason: OOMKilled와 Exit Code: 137이 함께 보이면 메모리 부족이 확정입니다. RESTARTS가 함께 늘면 STATUS는 CrashLoopBackOff로 보일 수 있으니, 종료 코드 137을 단서로 메모리 문제를 분리해 냅니다.
해결 #
원인은 둘 중 하나입니다. limit이 앱의 실제 사용량보다 비현실적으로 낮거나(설정 문제), 앱이 실제로 메모리를 너무 많이 쓰거나(앱 문제)입니다.
# 평소 사용량을 본다 (metrics-server 필요)
k top pod oomlimit이 너무 낮은 것이 원인이면 현실적인 값으로 올립니다.
resources:
requests:
memory: "128Mi"
limits:
memory: "256Mi"requests와 limits의 관계, QoS 클래스(BestEffort/Burstable/Guaranteed)가 OOM 시 어떤 Pod부터 죽는지에 영향을 준다는 점은 리소스 관리 편(#15)에서 다룬 내용을 그대로 적용합니다. 운영 환경에서 메모리,CPU를 어떻게 관측하고 알람을 거는지는 옵저버빌리티 편에서 메트릭 축으로 정리했습니다.
한 장으로 보는 진단 흐름 #
시험장에서 Pod 장애를 만나면 다음 순서로 내려갑니다.
k get pod -o wide로 STATUS와 RESTARTS를 본다- 무조건
k describe pod의 Events를 먼저 읽는다 - STATUS가 Pending이면 → Events의
FailedScheduling문구로 자원/selector/taint/PVC 분기 - ImagePull 계열이면 → Events의
Failed to pull image문구로 태그/인증/네트워크 분기 - 컨테이너가 떴다 죽었으면 →
k logs --previous로 앱 메시지 확인 - describe의 Last State에
OOMKilled/Exit Code: 137이면 → 메모리 limit 문제로 처리
이 흐름의 출발점은 언제나 같습니다. 추측하지 말고 describe의 Events부터 읽는다. 이 한 습관이 트러블슈팅 30%의 절반을 가져갑니다.
정리 #
이번 글에서 잡은 것:
- Troubleshooting은 CKA의 최대 도메인(30%). 망가진 것을 빠르게 고치는 진단 속도가 점수를 가른다
- 진단 도구는
k describe(Events),k logs --previous,k get events,k get pod -o wide. describe의 Events를 항상 먼저 읽는다 - Pending. scheduler가 노드를 못 찾음.
FailedScheduling문구로 자원 부족,nodeSelector,taint,PVC 미바인딩을 분기 - CrashLoopBackOff. 떴다 죽음.
logs --previous로 앱 오류,잘못된 command,probe 실패를 확인 - ImagePullBackOff / ErrImagePull. 이미지를 못 가져옴. Events로 태그 오타,레지스트리 인증,네트워크를 분기
- OOMKilled. 메모리 limit 초과. describe의 Last State에
Reason: OOMKilled와 exit code 137
다음: Troubleshooting 2 #
Pod 레벨은 잡았습니다. 그런데 Pod가 멀쩡한 매니페스트인데도 안 뜨고, 심지어 노드 전체가 NotReady로 빠지는 경우가 있습니다. 그러면 한 단계 아래, 노드와 kubelet 으로 내려가야 합니다.
#23 Troubleshooting 2: 노드와 kubelet에서는 노드가 NotReady가 되는 원인을 추적하겠습니다. kubelet 서비스가 죽었거나, 인증서,kubeconfig가 어긋났거나, 노드에 disk pressure,memory pressure가 걸린 경우를 systemctl status kubelet과 journalctl -u kubelet으로 내려가며 진단하고 복구하겠습니다.