운영 트러블슈팅 패턴
장애는 패턴이 있다
쿠버네티스 운영 중 마주치는 대부분의 문제는 몇 가지 패턴으로 수렴한다. 패턴을 알면 수십 분 걸릴 진단을 몇 분 안에 끝낼 수 있다. 이 글에서는 현장에서 가장 자주 보이는 Pod 실패 상태를 체계적으로 진단하고 해결하는 방법을 다룬다.
진단의 첫 번째 도구 — describe와 events
kubectl get pod <pod-name> -n <namespace>
kubectl describe pod <pod-name> -n <namespace>
kubectl get events --sort-by='.lastTimestamp' -n <namespace>describe 출력의 Events: 섹션이 핵심이다. Scheduler가 왜 Pod를 배치하지 못했는지, kubelet이 어떤 이유로 컨테이너를 종료했는지가 여기에 기록된다. Events는 기본 1시간 후 etcd에서 사라지므로 장애 직후 빠르게 확인해야 한다.
Pod 상태별 트러블슈팅
Pending — 스케줄러가 노드를 찾지 못했다
kubectl describe pod <pod-name> | grep -A 10 "Events:"describe 이벤트에 0/3 nodes are available 같은 메시지가 뜬다.
| 원인 | 확인 명령 | 해결 방향 |
|---|---|---|
| 클러스터 리소스 부족 | kubectl top nodes | 노드 추가 또는 리소스 request 낮추기 |
| PVC Pending | kubectl get pvc | StorageClass 확인, PV 생성 여부 확인 |
| nodeSelector/affinity 불일치 | kubectl describe pod Events | 레이블 확인, affinity 규칙 재검토 |
| Taint/Toleration 미설정 | kubectl describe node Taints | Toleration 추가 |
nodeSelector vs affinity: nodeSelector는 단순한 레이블 매칭이고, affinity는
preferredDuringScheduling(소프트 규칙)과requiredDuringScheduling(하드 규칙)을 구분한다. requiredDuringScheduling은 조건을 만족하는 노드가 없으면 Pod를 영구 Pending 상태로 만든다.
CrashLoopBackOff — 컨테이너가 계속 죽는다
쿠버네티스가 컨테이너를 재시작하되 지수 백오프(10s → 20s → 40s → ... → 최대 5분)를 적용하는 상태다.
# 현재 로그
kubectl logs <pod-name> -n <namespace>
# 이전(죽기 전) 인스턴스의 로그 — 핵심
kubectl logs <pod-name> -n <namespace> --previous
# 재시작 횟수 확인
kubectl get pod <pod-name> -o jsonpath='{.status.containerStatuses[0].restartCount}'--previous 플래그 없이는 방금 시작된 컨테이너의 로그만 보이므로 실제 크래시 원인을 놓치기 쉽다.
흔한 원인:
- 앱 코드 예외 또는 설정 파일 오류
- 없는 ConfigMap/Secret을 마운트 시도
- 커맨드/인수 오류 (명령어가 즉시 exit 0 반환)
- Liveness Probe가 너무 엄격해 초기화 중 Pod를 재시작
OOMKilled — 메모리 제한을 초과했다
컨테이너가 limits.memory를 초과하면 커널이 SIGKILL(종료 코드 137)을 보낸다.
kubectl describe pod <pod-name> | grep -A 5 "Last State"
# 출력: Reason: OOMKilled, Exit Code: 137| 대응 | 언제 |
|---|---|
limits.memory 값 상향 | 실제로 더 많은 메모리가 필요한 경우 |
| VPA Off 모드로 권장값 수집 | 적정값을 모르는 경우 |
| 메모리 누수 조사 | request/limit을 올려도 계속 증가하는 경우 |
container_memory_working_set_bytes 메트릭이 limits 값에 근접하는 트렌드를 Grafana에서 미리 확인하면 OOMKill 전에 예방할 수 있다.
ImagePullBackOff / ErrImagePull — 이미지를 내려받지 못했다
kubectl describe pod <pod-name> | grep -A 5 "Events:"
# Failed to pull image "...", repository does not exist or may require 'docker login'| 원인 | 해결 |
|---|---|
| 이미지 이름 / 태그 오타 | image: 필드 재확인 |
| 프라이빗 레지스트리 인증 없음 | imagePullSecrets 추가 |
| 레지스트리 네트워크 단절 | 노드에서 curl <registry> 테스트 |
latest 태그 캐시 불일치 | 구체적인 다이제스트 또는 버전 태그 사용 |
# imagePullSecrets 예시
spec:
imagePullSecrets:
- name: regcred
containers:
- image: private.registry.io/myapp:1.2.3Terminating — Pod가 삭제되지 않는다
kubectl delete pod <pod-name> --grace-period=0 --forcePod가 Terminating 상태에서 멈추는 경우는 주로 두 가지다. Finalizer가 설정돼 있어 컨트롤러가 응답하지 않거나, 컨테이너가 SIGTERM을 무시하고 grace period(기본 30초)가 만료되기를 기다리는 경우다. --force --grace-period=0은 etcd 레코드를 강제 삭제하므로 신중하게 사용한다.
심층 진단 — exec와 debug
컨테이너가 실행 중이라면 직접 들어갈 수 있다.
# 실행 중인 컨테이너 내부 쉘
kubectl exec -it <pod-name> -n <namespace> -- /bin/sh
# 멀티컨테이너 Pod의 특정 컨테이너
kubectl exec -it <pod-name> -c <container-name> -- /bin/bash컨테이너에 쉘이 없거나 시작 즉시 크래시되는 경우엔 kubectl debug를 사용한다.
# 동일 Pod의 복사본에 디버그 컨테이너 추가 (원본 유지)
kubectl debug -it <pod-name> --image=busybox --copy-to=debug-pod -- sh
# 노드에 직접 접근 (노드 파일시스템, 프로세스 확인)
kubectl debug node/<node-name> -it --image=ubuntu서비스 연결 트러블슈팅
Pod 자체는 정상인데 요청이 도달하지 않는다면 네트워킹 레이어를 확인한다.
# Service의 Endpoints가 채워져 있는지 확인 (비어 있으면 selector 불일치)
kubectl get endpoints <service-name>
# DNS 해석 테스트 (임시 Pod 활용)
kubectl run tmp --image=busybox --rm -it -- nslookup <service-name>.<namespace>.svc.cluster.local
# Service 포트 → Pod 포트 포워딩으로 앱 직접 확인
kubectl port-forward pod/<pod-name> 8080:8080가장 흔한 원인은 Service의 selector와 Pod의 labels가 일치하지 않는 것이다. kubectl get endpoints가 비어 있다면 레이블을 먼저 확인한다.
트러블슈팅 결정 트리
노드 레벨 트러블슈팅
Pod가 아닌 노드 자체 문제일 수 있다.
# 노드 상태 확인
kubectl get nodes
kubectl describe node <node-name>
# NotReady 노드의 원인 (kubelet, 네트워크 플러그인, 디스크 압박 등)
kubectl get node <node-name> -o jsonpath='{.status.conditions}' | jq .
# 노드의 이벤트
kubectl get events --field-selector involvedObject.kind=Node노드가 NotReady가 되면 해당 노드의 Pod는 Unknown 상태로 전환되고, 기본 5분 후 다른 노드로 재스케줄링된다. 디스크 압박(DiskPressure), 메모리 압박(MemoryPressure), PID 부족(PIDPressure) Condition을 확인해야 한다.
안티패턴 정리
| 실수 | 결과 | 올바른 접근 |
|---|---|---|
--force --grace-period=0을 습관적으로 사용 | 데이터 손실, Finalizer 꼬임 | 원인 파악 후 정상 삭제 우선 |
kubectl logs 만 보고 --previous 안 씀 | 크래시 직전 로그를 놓침 | 반드시 --previous 병행 확인 |
| resource request 없이 배포 | 노드 Pending, OOMKill 예측 불가 | 모든 컨테이너에 request 설정 |
| Liveness Probe에 DB 연결 확인 포함 | 외부 장애 시 Pod 재시작 폭풍 | Liveness는 앱 자체만 확인 |
latest 태그 사용 | 의도치 않은 버전 배포, 캐시 불일치 | 구체적인 버전 태그 또는 digest 사용 |
| Events 만료 후 조사 시작 | 이벤트 소멸(기본 1시간)로 컨텍스트 손실 | 장애 즉시 이벤트 스냅샷 저장 |
References
- https://kubernetes.io/docs/tasks/debug/debug-application/debug-pods/
- https://kubernetes.io/docs/tasks/debug/debug-application/debug-running-pod/
- https://kx.cloudingenium.com/kubernetes-pod-troubleshooting-guide/
- https://www.netdata.cloud/academy/kubernetes-crash-loop-backoff/
- https://www.sysdig.com/blog/debug-kubernetes-crashloopbackoff
- https://komodor.com/learn/how-to-fix-crashloopbackoff-kubernetes-error/
- https://www.groundcover.com/kubernetes-troubleshooting/crashloopbackoff
- https://oneuptime.com/blog/post/2026-02-09-kubectl-describe-pod-events/view
- https://oneuptime.com/blog/post/2026-01-22-kubectl-logs-debug-containers/view
- https://krishnendubhowmick.medium.com/this-kubernetes-troubleshooting-method-will-make-you-the-hero-on-call-e6bb568479b3