kubectl 디버깅 패턴
5부 (운영 · 디버깅 · 비용)의 첫 챕터입니다. 운영 클러스터에서 가장 자주 마주치는 사고 (CrashLoopBackOff, OOMKilled, ImagePullBackOff, Pending, Service가 안 닿음)의 진단 트리를 한 곳에 정리합니다. describe · events · logs의 세 명령에서 출발해 kubectl debug의 ephemeral container, 네트워크 진단 패턴, 19장 옵저버빌리티 스택과의 결합까지 신입 SRE의 첫 reference가 되는 매뉴얼로 묶습니다.
5부 (운영 · 디버깅 · 비용)의 첫 챕터입니다. 4부까지를 거쳐 EKS 클러스터 위에 myshop-api가 한 사이클로 굴러가는 상태에 도착했지만, 실제 운영은 사고로부터 자유롭지 않습니다. 어떤 사고가 어떤 모양으로 들어오는지, 그때 어디부터 봐야 하는지, 어느 도구가 어떤 신호를 보여 주는지의 깊이는 자동화로 풀 수 없는 영역입니다. 이번 챕터는 그 결을 한 권의 디버깅 매뉴얼로 정리합니다.
운영 클러스터에서 한 사람이 1년에 만나는 사고의 절반 이상은 다섯 가지 패턴으로 압축됩니다 — CrashLoopBackOff, OOMKilled, ImagePullBackOff, Pending, Service가 안 닿음. 이 다섯의 진단 트리를 머릿속에 두고 있으면 새벽에 PagerDuty가 울려도 5분 안에 1차 원인 후보가 좁아집니다. 본 챕터의 목표는 그 다섯 트리와 그 위에 얹힌 kubectl debug · ephemeral container · 네트워크 진단의 도구가 한 사람의 머릿속에 들어와 있는 상태입니다.
디버깅의 출발선 — 세 명령 #
거의 모든 디버깅의 출발점은 다음 세 명령입니다. 순서가 정해져 있고, 각 명령이 보여 주는 결이 다릅니다.
kubectl describe pod <name> -n <ns>
kubectl get events -n <ns> --sort-by='.lastTimestamp'
kubectl logs <pod> -n <ns> [-c <container>] [--previous]각 명령의 책임을 한 줄로 정리합니다.
describe— Pod의 현재 spec + status + 최근 events가 한 화면에. 사고의 첫 5 줄에서 답이 보이는 경우가 많습니다.events— 클러스터 전체의 시간순 이벤트. scheduler · kubelet · controller가 누가 무슨 말을 했는지가 한 흐름으로 잡힙니다.logs— 컨테이너 자체의 stdout / stderr. 애플리케이션의 진짜 에러는 여기에 있습니다.
세 명령의 결합으로 풀리지 않는 사고만 kubectl debug나 옵저버빌리티 스택으로 넘어갑니다. 사고를 보면 무조건 이 셋을 먼저가 운영 디버깅의 첫 규칙입니다.
Pod 상태의 의미 — 9 가지와 다음 액션 #
kubectl get pods의 STATUS 컬럼에 자주 등장하는 상태를 한 표로 정리합니다.
| 상태 | 의미 | 첫 액션 |
|---|---|---|
Pending | 스케줄링 대기 | describe의 Events에서 scheduler 메시지 |
ContainerCreating | kubelet이 컨테이너 준비 중 | describe의 Events (이미지 풀, 볼륨 마운트) |
Running | 컨테이너 실행 중 (단, ready와는 다름) | kubectl get pod -o wide로 READY 컬럼 확인 |
CrashLoopBackOff | 컨테이너가 반복 크래시 | logs --previous와 exit code |
OOMKilled | 메모리 한도 초과로 종료 | events의 reason + 11장 한도 |
ImagePullBackOff | 이미지를 가져오지 못함 | events의 reason (권한 / 레지스트리 / 태그) |
Error | 종료 (재시작 안 됨) | restartPolicy와 exit code |
Completed | 성공 종료 (Job의 정상 상태) | 정상 — Job인지 확인 |
Init:Error 등 | initContainer 단계 실패 | logs -c <init-container-name> |
READY 컬럼이 0/1 인데 STATUS가 Running이라면 12장 헬스 체크의 readinessProbe가 실패 중이라는 신호입니다. STATUS가 Running이라고 정상이 아닙니다. 운영 디버깅에서 자주 놓치는 한 칸입니다.
describe pod의 events 섹션 읽기 #
kubectl describe pod의 출력 마지막에 있는 Events 섹션이 사고의 90%를 보여 줍니다. 어느 컴포넌트가 보냈는지에 따라 의미가 다릅니다.
Source | 보는 결
default-scheduler | 스케줄링 단계 — Pending 의 원인 (taint, affinity, resources, PVC)
kubelet | 노드 위에서의 단계 — image pull, mount, probe, OOM
controller-manager | 상위 컨트롤러의 액션 — ReplicaSet 의 Pod 생성 / 삭제
attachdetach | EBS / EFS 볼륨의 마운트 단계운영 디버깅에서 가장 자주 보는 출처는 kubelet과 default-scheduler입니다. kubelet이 보낸 Failed to pull image와 default-scheduler가 보낸 0/3 nodes are available: 3 Insufficient memory가 각각 ImagePullBackOff와 Pending의 진짜 원인을 직접 알려 줍니다.
kubectl logs의 패턴 #
# 실시간 스트림
kubectl logs -f <pod>
# 마지막 N 줄
kubectl logs --tail=200 <pod>
# 멀티 컨테이너 Pod
kubectl logs <pod> -c <container>
# 직전에 죽은 컨테이너 (CrashLoopBackOff 의 핵심)
kubectl logs <pod> --previous
# Deployment / Label 단위
kubectl logs -l app.kubernetes.io/name=myshop-api --tail=100CrashLoopBackOff 디버깅의 95%는 --previous 한 옵션입니다. 현재 컨테이너는 시작도 못 했으니 현재 로그가 없고, 직전에 죽은 컨테이너의 로그를 봐야 진짜 에러 메시지가 나옵니다. 이 한 줄을 모르면 “로그가 비어 있는데 뭘 봐야 할지 모름"의 함정에 빠집니다.
라벨 셀렉터로 여러 Pod의 로그를 한 번에 보는 패턴은 22장 앱 배포 골격의 app.kubernetes.io/name 표준 라벨 덕분에 자연스럽게 굴러갑니다.
kubectl exec의 한계 와 kubectl debug #
kubectl exec -it <pod> -n <ns> -- /bin/shexec의 한계는 명확합니다.
- 컨테이너가 죽었으면 exec 불가 — CrashLoopBackOff Pod에 exec가 안 됩니다.
- distroless / scratch 이미지엔 shell이 없음 —
/bin/sh가 없는 컨테이너에는 exec가 의미 없습니다. - 디버깅 도구가 컨테이너에 없음 —
curl,dig,tcpdump같은 도구가 없는 게 보통입니다.
이 세 한계를 푸는 도구가 **kubectl debug**입니다. Pod의 같은 네임스페이스 안에 ephemeral container를 끼워 넣어, 거기서 진단을 수행합니다.
# Pod 안에 busybox 컨테이너 끼워 넣기
kubectl debug -it <pod> -n <ns> \
--image=busybox:1.36 \
--target=<container-name>
# distroless Pod 의 파일시스템 보기 (--target 으로 같은 PID namespace 공유)
kubectl debug -it <pod> -n <ns> \
--image=nicolaka/netshoot \
--target=<container-name>--target 옵션이 핵심 — 같은 PID namespace를 공유해서 원본 컨테이너의 프로세스를 볼 수 있고, --profile=netadmin 옵션을 더하면 네트워크 진단까지 가능합니다.
kubectl debug node/<node-name> -it --image=busybox노드의 호스트 파일시스템이 /host에 마운트된 컨테이너가 떠올라, /host/var/log/의 kubelet 로그를 볼 수 있습니다. EBS CSI Driver의 마운트 오류처럼 노드 레벨의 사고에서 결정적인 도구입니다.
CrashLoopBackOff 진단 트리 #
가장 흔한 사고입니다. Pod가 시작 직후 죽고, 다시 시작되고, 다시 죽는 사이클입니다.
1. kubectl logs <pod> --previous
-> 직전 컨테이너의 stderr 가 진짜 원인. 95% 의 경우 여기서 답이 나옴.
2. kubectl describe pod <pod>
-> Events 의 Last State / Reason / Exit Code 확인.
-> Exit Code 137 = SIGKILL (OOM 또는 강제 종료)
-> Exit Code 139 = SIGSEGV (segfault)
-> Exit Code 1 = 일반 에러
3. probe 실패 의심
-> [12장 헬스 체크](./health-checks/) 의 liveness 가 너무 빨리 fail 하면
애플리케이션이 살아 있어도 kubelet 이 컨테이너를 죽임.
-> initialDelaySeconds 가 충분한지 점검.
4. initContainer 단계 의심
-> STATUS 가 Init:Error 면 init 단계 실패.
-> kubectl logs <pod> -c <init-container-name>
5. ConfigMap / Secret 누락
-> describe Events 의 "MountVolume.SetUp failed".
-> [6장 ConfigMap · Secret](./configmap-and-secret/) 의 이름 일치 확인.backoff의 지수 증가도 알아 두면 도움이 됩니다 — 10초 → 20초 → 40초 → … → 최대 5분으로 늘어납니다. “한 시간째 같은 사고가 안 풀린다"가 아니라 “kubelet이 5분에 한 번씩 재시도하는 정상 상태” 일 수 있습니다.
OOMKilled 진단 트리 #
1. kubectl get events -n <ns> --field-selector reason=OOMKilling
-> 어느 Pod 가 언제 OOMKilled 됐는지 시간순 목록.
2. kubectl describe pod <pod>
-> Last State -> Terminated -> Reason: OOMKilled, Exit Code: 137.
3. [11장 자원 요청과 한도](./resources-and-limits/) 의 limits.memory 확인
-> 컨테이너의 실제 사용량이 limits 를 넘어선 시점에 종료.
-> [25장 모니터링·알람](./monitoring-and-alerts/) 의
container_memory_working_set_bytes 메트릭으로 패턴 확인.
4. memory leak 의심
-> 시간 그래프에서 우상향 직선이면 코드 레벨 누수.
-> heap profile (Java jmap, Go pprof, Python memray) 으로 추가 진단.
5. 잘못된 limits 의심
-> JVM 의 -Xmx 가 container limits 보다 크게 잡혀 있을 수 있음.
-> Native 라이브러리의 메모리 사용은 heap 외에 별도.cgroup v2 환경 (커널 4.5+ / EKS 1.25+ 기본)에서는 OOMKilled의 동작이 약간 다릅니다. cgroup v1에서는 컨테이너 안의 PID 1이 죽었지만, v2에서는 메모리를 가장 많이 쓰는 프로세스가 먼저 죽을 수 있습니다. 멀티 프로세스 컨테이너에서는 어느 프로세스가 죽었는지 확인이 필요합니다.
ImagePullBackOff 진단 트리 #
1. kubectl describe pod <pod>
-> Events 의 reason 으로 6 가지 원인 좁히기:
- "manifest unknown" / "not found"
-> 이미지 태그 오타 또는 ECR 에 푸시 실패.
[24장 CI/CD](./cicd-pipeline/) 의 ECR push 단계 확인.
- "unauthorized" / "denied"
-> ECR 권한 부족. Node IAM Role 의 ECR read 권한 확인,
또는 imagePullSecret 의 자격 증명 확인.
- "no basic auth credentials"
-> imagePullSecret 자체가 없거나 이름 오타.
- "x509: certificate signed by unknown authority"
-> private registry 의 인증서 신뢰 문제.
- "context deadline exceeded"
-> 네트워크 지연. NAT Gateway / VPC Endpoint 의 경로 확인.
- "ECR repository ... does not exist"
-> 다른 region / 다른 account 의 ECR 을 가리키는 경우.EKS 환경에서는 21장 EKS 클러스터 셋업에서 노드 IAM Role에 AmazonEC2ContainerRegistryReadOnly 정책이 붙어 있는 것이 ECR pull의 기본 권한 경로입니다. 다른 account의 ECR을 쓴다면 repository policy에서 cross-account 권한을 명시적으로 열어 줘야 합니다.
Pending 진단 트리 #
1. kubectl describe pod <pod>
-> Events 의 default-scheduler 메시지가 즉시 답을 알려 줌.
2. "0/N nodes are available: ... Insufficient cpu/memory"
-> [11장](./resources-and-limits/) 의 requests 가 노드 가용량 초과.
-> [13장 오토스케일링](./autoscaling/) 의 Karpenter / Cluster Autoscaler 가
새 노드를 띄우는 중인지 확인.
3. "Insufficient nodes match node selector / affinity"
-> nodeSelector 의 라벨 키 오타, 또는 그 라벨을 가진 노드 없음.
-> kubectl get nodes --show-labels 로 라벨 확인.
4. "had taints that the pod didn't tolerate"
-> 노드의 taint 와 Pod 의 toleration 불일치.
-> Karpenter 의 NodePool 이 spot 만 띄우는데 Pod 에 toleration 없음 등.
5. "pod has unbound immediate PersistentVolumeClaims"
-> [9장 PV/PVC/StorageClass](./pv-pvc-storageclass/) 의 동적 프로비저닝 실패.
-> StorageClass 의 EBS CSI Driver 가 정상 동작하는지 확인.
6. Karpenter 의 응답 시간 의심
-> 새 노드 프로비저닝까지 30 초 ~ 2 분.
-> 그 시간 동안 Pending 은 정상.Pending의 진단은 “왜 스케줄러가 거절했는가” 한 질문으로 좁아집니다. describe의 Events 한 줄이 항상 답을 가지고 있다는 점이 OOMKilled / CrashLoop와 다른 결입니다.
Service / Ingress가 안 닿을 때 #
1. Pod 자체가 ready 인가?
-> kubectl get pods -l <selector> -o wide
-> READY 가 1/1 이고 STATUS 가 Running 인지.
-> readinessProbe 가 실패 중이면 Endpoints 에서 자동 제외됨.
2. Service 의 selector 가 Pod 라벨과 일치하는가?
-> kubectl describe service <svc>
-> kubectl get endpoints <svc> -- Endpoints 가 비어 있으면 selector 불일치.
3. Service 의 port 가 Pod 의 containerPort 와 맞는가?
-> targetPort 는 Pod 안의 포트 (또는 포트 이름).
-> port 는 Service 자체의 가상 포트.
4. NetworkPolicy 가 막고 있는가?
-> [14장 RBAC/NetworkPolicy/ResourceQuota](./rbac-networkpolicy-quota/) 의 ingress 룰 확인.
5. Ingress 의 ALB 가 실제로 떴는가?
-> kubectl describe ingress -- Address 필드.
-> kubectl logs -n kube-system deployment/aws-load-balancer-controller.
-> ALB target group 의 healthy / unhealthy 상태 (AWS 콘솔).
6. DNS 해상도 자체 점검
-> kubectl run -it --rm dns-test --image=busybox:1.36 \
-- nslookup <svc>.<ns>.svc.cluster.local5장 Service의 selector → endpoints → port의 3 단 체인이 운영 디버깅의 기본 사고 모형입니다. 셋 중 어디가 끊겼는지를 좁히는 게 Service 디버깅의 핵심입니다.
네트워크 진단 도구 #
# 가장 가벼운 임시 컨테이너
kubectl run -it --rm net-test --image=busybox:1.36 \
-n <ns> -- /bin/sh
# 풀 패키지 (curl, dig, traceroute, tcpdump, nslookup)
kubectl run -it --rm net-test --image=nicolaka/netshoot \
-n <ns> -- /bin/bashnicolaka/netshoot가 사실상 표준 진단 이미지입니다. 다음 시나리오를 한 컨테이너 안에서 모두 풀 수 있습니다.
# DNS
dig myshop-api.myshop.svc.cluster.local
# HTTP 연결
curl -v http://myshop-api.myshop:80/health/ready
# 경로 추적
traceroute api.myshop.example.com
# 노드 IP 와 Pod IP 의 라우팅
ip route15장 CNI 깊이의 VPC CNI 모델 (Pod가 직접 ENI의 IP를 받는 구조) 덕분에 EKS에서는 Pod의 IP가 노드의 라우팅 테이블에 자연스럽게 들어옵니다. 그래서 tcpdump가 노드의 host network에서도 Pod 트래픽을 잡을 수 있습니다.
kubectl debug node/<node-name> -it --image=nicolaka/netshoot
# 그 안에서:
tcpdump -i any -n 'host <pod-ip>'이 도구가 손에 들어와 있으면 10장 Ingress의 ALB → Pod 경로의 어디서 패킷이 사라지는지를 한 번에 추적할 수 있습니다.
옵저버빌리티와의 결합 — 언제 어디로 가는가 #
kubectl describe와 Grafana 대시보드 중 어느 쪽으로 먼저 가야 하는지는 사고의 결에 따라 다릅니다.
[단일 Pod 의 문제] — describe / logs / events 가 더 빠름
- CrashLoopBackOff, ImagePullBackOff, OOMKilled
- "특정 Pod 한 개만 안 됨"
[분산 / 통계적 문제] — 옵저버빌리티 스택이 더 빠름
- "에러율이 5% 넘음"
- "P95 latency 가 1초 넘음"
- "지난 1시간 대비 트래픽 30%"
- 사고 시점의 다른 워크로드와의 상관관계19장 옵저버빌리티와 25장 모니터링 · 알람의 스택이 본 챕터의 단일 Pod 디버깅과 결합되어 사고 대응의 양 축을 이룹니다. Grafana의 분산 트레이스가 “어느 서비스의 어느 핸들러가 느리다"까지 좁혀 주면, 그 다음 kubectl logs가 그 Pod의 진짜 stderr를 보여 줍니다.
24장 CI / CD 파이프라인의 ArgoCD UI도 디버깅 도구의 하나입니다. OutOfSync 상태의 Application을 클릭하면 어느 리소스가 desired state와 다른지가 시각적으로 보입니다 — describe의 텍스트 출력보다 빠른 경우가 많습니다.
사고 한 사이클의 표준 흐름 #
이 챕터의 도구들을 사고 한 건에 어떻게 엮는지의 표준 흐름을 정리합니다.
1. PagerDuty / Slack 에서 알람 본문 확인 (alertname, severity, runbook_url)
2. runbook 의 "1차 점검" 섹션 따라가기
3. Grafana 대시보드로 사고 범위 확인 (한 Pod / 한 Service / 전체)
4. 범위가 한 Pod 면 kubectl describe + logs --previous
범위가 한 Service 면 endpoints + NetworkPolicy
범위가 전체면 노드 / CNI / 클러스터 컴포넌트
5. 1차 대응 (스케일 업, 재시작, 트래픽 차단)
6. Slack 사고 채널에 상태 공유 + 사후 RCA 준비이 흐름이 한 사람의 머릿속에 들어와 있으면 새벽 사고에서도 5분 안에 1차 진단이 가능합니다. 25장의 runbook_url이 본 챕터의 진단 트리로 직접 연결되는 게 운영 클러스터의 목표입니다.
연습문제 #
- dev 클러스터의 myshop-api Deployment에 일부러 잘못된 이미지 태그 (
myshop-api:does-not-exist)를 적용해 Pod가 ImagePullBackOff가 되는 상태를 만듭니다.kubectl describe pod의 Events 섹션에서 어떤 reason이 나오는지 기록하고, 본 챕터의 §“ImagePullBackOff 진단 트리"의 6 가지 원인 중 어디에 해당하는지를 정리합니다. 같은 사고를 일부러 권한 부족 (ECR 접근 권한 없는 IAM Role)으로도 재현해 둘의 reason 차이를 비교합니다. - 메모리를 점진적으로 누수하는 컨테이너 (예: Python의
while True: data.append(...))를 작은 limits.memory와 함께 배포해 OOMKilled가 발생하는 모양을 만듭니다.kubectl get events --field-selector reason=OOMKilling로 시간을 잡고, 25장 모니터링 · 알람의MyshopApiPodMemoryHigh알람이 OOMKilled보다 먼저 울리도록 임계치를 조정합니다. 알람이 사고를 예측하는 모양과 사고가 발생한 뒤 알람이 울리는 모양의 운영 가치 차이를 한 단락으로 정리합니다. - Service의 selector를 일부러 한 글자 오타로 바꿔 (“app.kubernetes.io/name: myshop-apia”)
kubectl get endpoints가 비어 있는 상태를 만듭니다. 본 챕터의 §“Service / Ingress가 안 닿을 때"의 6단계 트리를 위에서부터 따라가며 어느 단계에서 사고가 발견되는지 기록하고, 같은 사고를 옵저버빌리티 스택 (Grafana의 traffic 패널 + Loki 로그)으로 발견하는 경로와 비교합니다. 어느 쪽이 더 빠른지, 그 이유가 무엇인지 한 단락으로 정리합니다.
한 줄 요약: 운영 클러스터 디버깅의 출발선은
describe+events+logs의 세 명령이고, 그 위에kubectl debug의 ephemeral container가 distroless · 죽은 컨테이너 · 노드 진단의 한계를 푼다. CrashLoopBackOff는logs --previous한 옵션, OOMKilled는 11장 limits와 메트릭, ImagePullBackOff는 events의 reason 6 가지, Pending은 default-scheduler의 한 줄, Service는 selector → endpoints → port의 3 단 체인이 핵심 진단 트리다. 단일 Pod의 문제는kubectl이 빠르고, 분산 · 통계적 문제는 옵저버빌리티 스택이 빠르다 — 두 도구를 사고의 결에 맞춰 골라 쓰는 게 운영의 표준이다.
다음 챕터 #
이번 챕터에서 사고가 발생했을 때 어디부터 봐야 하는지의 결을 한 매뉴얼로 정리했습니다. 다음 챕터에서는 사고가 아닌 청구서의 결을 다룹니다.
28장 비용 최적화에서는 26장 운영 체크리스트에서 다섯 가지 출처로 짚었던 비용 항목을 본격적으로 다룹니다. 컴퓨트와 부가의 두 축, requests의 비용 의미, VPA / Goldilocks의 right-sizing, Karpenter와 Cluster Autoscaler의 결정 트리, namespace · 라벨 단위 비용 분배까지의 한 사이클을 다룹니다. 13장 오토스케일링과 11장 자원 요청과 한도의 결정들이 본격적인 운영 비용 결로 이어지는 단계입니다.