kubectl exec 가 /bin/bash: no such file or directory 로 실패하는 distroless 컨테이너를 들여다보는 방법, 이미 죽은 파드의 종료 원인을 남은 흔적에서 찾는 순서, 그리고 kubectl annotate 의 용법을 정리했다.
exec 는 컨테이너 안의 바이너리를 실행하므로 sh, ls, busybox 까지 없으면 들어갈 방법 자체가 없다. 정적 바이너리 하나만 든 scratch 이미지가 그렇다. 안으로 들어가는 대신 밖에서 관찰한다.
kubectl logs <pod>
kubectl describe pod <pod>
kubectl get pod <pod> -o yaml # 이미지, command/args 확인
# K8s 1.23+ : 디버그 툴이 든 임시 컨테이너를 같은 프로세스 네임스페이스에 붙인다
kubectl debug -it <pod> --image=busybox --target=<container> -- sh
ls -la /proc/1/root/ # 대상 컨테이너의 파일시스템
cat /proc/1/cmdline | tr '\0' ' '
# 노드에서 런타임으로 직접
kubectl get pod <pod> -o wide
crictl ps | grep <pod>; crictl inspect <container-id>
rpc error: code = 2 ... container_linux.go 문구는 dockershim 시절의 구버전 런타임을 뜻하며, 이 경우 kubectl debug 가 지원되지 않을 수 있다. 파드에 컨테이너가 여럿이면 -c <container> 로 대상을 지정해야 하며, 기본 컨테이너가 사이드카(fluent-bit 등)라 셸이 없는 경우가 흔하다.
파드 객체가 남아 있는지에 따라 경로가 갈린다.
kubectl get pod -n <ns> -o wide # RESTARTS, AGE
kubectl describe pod <pod> -n <ns> | grep -A 10 "Last State" # Reason, Exit Code, Finished At
kubectl logs <pod> -n <ns> --previous
OOMKilled 는 restartPolicy 에 따라 같은 파드 안에서 컨테이너만 다시 뜨므로 Last State: Terminated / Reason: OOMKilled / Exit Code: 137 이 그대로 남는다. 그 사이 다시 재시작됐으면 --previous 는 직전 것만 보여주므로 Finished At 시각을 대조한다.
lastState, 컨테이너 로그, --previous 가 함께 사라진다. 수동 재기동이나 rollout restart 도 여기에 해당한다.
kubectl rollout history deploy/<name> -n <ns> # 배포 시각과 겹치면 정상 교체(Exit 143)
kubectl get rs -n <ns> -l app=<label> -o wide; kubectl describe rs <rs> -n <ns>
kubectl get events -n <ns> --sort-by=.lastTimestamp | grep -Ei 'kill|oom|evict|fail|unhealthy'
kubectl get events -n <ns> --field-selector involvedObject.name=<pod>
journalctl -u kubelet --since "2026-09-07 00:00" --until "2026-09-08 00:00" | grep -i <deployment>
journalctl -k --since "2026-09-07" --until "2026-09-08" | grep -iE "oom_kill|memory cgroup"
이벤트 TTL 은 기본 1시간이라 전날 일은 남지 않는다. 그 뒤로는 로그 수집기(Loki, ELK 등)에서 라벨과 시간 범위로 조회하거나, Prometheus 의 container_memory_working_set_bytes, kube_pod_container_status_restarts_total, kube_pod_container_status_last_terminated_reason 으로 추정한다. 누가 지웠는지는 API 서버 감사 로그에 남는다.
| 코드 | 의미 |
|---|---|
| 137 | SIGKILL, 대부분 OOMKilled 또는 liveness probe 실패 |
| 143 | SIGTERM, 배포·스케일다운에 따른 정상 종료 |
| 139 | Segfault |
| 1, 2 | 애플리케이션 오류 |
| 0 | 정상 종료 후 restartPolicy 에 따른 재시작 |
컨테이너 OOM 은 노드 커널의 cgroup memory controller 가 처리하므로 노드의 dmesg 에 기록되지만, 문구가 시스템 전역 OOM(Killed process) 과 달리 Memory cgroup out of memory 또는 oom-kill:constraint=CONSTRAINT_MEMCG 형태다. grep "killed process" 만으로는 놓치므로 oom|memory cgroup 으로 잡는다. dmesg 의 PID 는 노드 네임스페이스 기준이라 파드 안 PID 와 다르며, 링버퍼가 밀렸으면 journalctl -k 를 본다. kubelet 은 이 이벤트를 받아 파드 상태에 OOMKilled 로 기록하므로 여부만 볼 때는 describe 가 더 확실하다.
어노테이션은 라벨과 달리 셀렉터로 조회되지 않고 도구가 참조하는 메타데이터를 담는다.
kubectl annotate pod my-pod description="테스트용"
kubectl annotate pod my-pod description="변경됨" --overwrite
kubectl annotate pod my-pod description- # 삭제
kubectl get pod my-pod -o jsonpath='{.metadata.annotations}'
Ingress 컨트롤러 설정(nginx.ingress.kubernetes.io/rewrite-target), 배포 이력(kubernetes.io/change-cause), 모니터링 스크랩(prometheus.io/scrape) 이 대표적인 용도다.
파드가 사라지면 원인 추적이 거의 불가능하므로 로그를 클러스터 밖으로 내보내는 수집 파이프라인이 있어야 한다. 수동 재기동 전에 describe 와 logs --previous 를 먼저 남겨 둔다.