본문으로 건너뛰기
10.3 이벤트와 디버깅 도구

10.3 이벤트와 디버깅 도구

로그와 메트릭이 컨테이너 안쪽의 이야기라면, Event는 Kubernetes가 그 오브젝트에 무엇을 했고 무엇에 실패했는지를 남기는 바깥쪽 기록이다. 스케줄러가 Pod를 배치하지 못한 이유, kubelet이 이미지를 받지 못한 이유, probe가 실패한 횟수가 전부 Event로 남는다. 이 챕터는 Event를 읽는 법과 함께, 실행 중인 워크로드와 노드를 조사하는 도구인 kubectl debug, exec, port-forward를 다룬다.

Event 리소스의 성질

Event는 namespace에 속하는 보통의 API 오브젝트다. 다만 수명이 짧다. kube-apiserver의 --event-ttl 기본값이 1시간이라, 지난밤의 장애를 아침에 조사하려고 하면 Event는 이미 사라진 뒤다. etcd가 감당할 부하를 제한하기 위한 설계이므로 TTL을 늘리는 것이 능사도 아니다. 장기 보존이 필요하면 이벤트를 로그 백엔드로 내보내는 exporter를 별도로 둔다.

같은 이벤트가 반복되면 새 오브젝트를 만드는 대신 기존 이벤트의 count를 올리고 lastTimestamp를 갱신하는 방식으로 집계된다. “Back-off restarting failed container” 이벤트 하나에 count 수백이 붙어 있는 식이다.

Event의 type은 Normal과 Warning 둘이다. 정상 동작의 기록(Scheduled, Pulled, Started)은 Normal로, 문제의 신호(FailedScheduling, BackOff, Unhealthy)는 Warning으로 남으므로, 진단할 때는 Warning만 추려 보는 것이 빠르다. 조회할 때 또 하나 주의할 점은 kubectl get events의 기본 출력이 시간순 정렬이 아니라는 것이다. --sort-by를 붙이는 것이 사실상 표준 사용법이다.

kubectl get events --sort-by='.lastTimestamp'
kubectl get events --field-selector type=Warning
kubectl get events --field-selector involvedObject.name=mypod
kubectl events --for pod/mypod --watch     # 전용 서브커맨드. 특정 오브젝트 추적

kubectl describe가 마지막에 출력하는 Events 섹션은 해당 오브젝트로 필터링된 이벤트 뷰다. 개별 오브젝트를 진단할 때는 describe가 가장 빠른 진입점이고, 클러스터 전체에서 지금 무슨 일이 벌어지는지 폭넓게 볼 때는 get events를 시간순 정렬로 실행한다.

kubectl debug: ephemeral container 주입

프로덕션 이미지는 공격 표면과 크기를 줄이려고 셸과 디버깅 도구를 뺀 distroless로 만드는 경우가 많다. 이런 컨테이너에는 kubectl exec로 들어갈 셸 자체가 없다. ephemeral container는 이 문제를 위해 만들어진 기능으로, 실행 중인 Pod에 임시 컨테이너를 재시작 없이 추가한다. 1.25에서 GA다.

kubectl debug -it mypod --image=busybox:1.36 --target=app

--target으로 대상 컨테이너를 지정하면 그 컨테이너와 프로세스 네임스페이스를 공유한다. 주입된 busybox 셸에서 ps로 대상 프로세스를 확인하고, /proc/<pid>/root 경로로 대상 컨테이너의 파일시스템에 접근한다. ephemeral container는 일반 컨테이너와 달리 port, probe, resources 필드를 갖지 않으며, 한 번 추가하면 Pod에서 제거할 수 없고 Pod를 지워야 사라진다.

--target의 프로세스 네임스페이스 공유는 컨테이너 런타임이 지원해야 동작한다. 또한 ephemeral container 주입은 pods/ephemeralcontainers 서브리소스에 대한 별도 권한이므로, 프로덕션에서는 RBAC로 누가 실행할 수 있는지 따로 통제한다.

크래시하는 컨테이너처럼 원본을 그대로 둔 채 조사하기 어려운 경우에는 --copy-to 변형을 쓴다. Pod의 사본을 만들면서 command를 셸로 바꾸면, 죽기를 반복하는 원본 대신 멈춰서 대기하는 사본에서 환경을 조사하게 된다.

kubectl debug mypod -it --copy-to=mypod-debug --container=app -- sh

노드 디버깅

조사 대상이 노드 자체일 때, SSH 접근이 막혀 있거나 번거로우면 kubectl debug의 node 모드를 쓴다.

kubectl debug node/worker-1 -it --image=busybox:1.36

이 명령은 해당 노드에 호스트의 IPC, network, PID 네임스페이스를 공유하는 Pod를 하나 만들고, 노드의 루트 파일시스템을 컨테이너의 /host에 마운트한다. journalctl 같은 노드 도구가 필요하면 chroot /host로 호스트 환경에 진입한다. 조사가 끝난 디버깅 Pod는 자동으로 삭제되지 않으므로 kubectl delete pod로 직접 정리한다.

노드 위에서 특정 컨테이너의 네트워크 문제를 조사할 때는 nsenter 패턴이 유용하다. 컨테이너 안에 ss나 tcpdump가 없어도, 노드에서 해당 컨테이너의 프로세스 ID를 찾아 그 네임스페이스로 들어가면 노드에 설치된 도구를 그대로 쓴다.

# 노드에서 실행: 컨테이너 PID를 찾아 네트워크 네임스페이스 진입
crictl ps                            # 컨테이너 ID 확인
crictl inspect <컨테이너ID> | grep pid
nsenter -t <PID> -n ss -tlnp         # 그 컨테이너의 리스닝 포트를 노드 도구로 조회

port-forward와 exec

exec는 컨테이너 안에서 명령을 실행하는 가장 기본적인 조사 수단이다. 셸이 들어 있는 이미지라는 전제가 필요하고, 그 전제가 깨지는 곳에서 앞의 kubectl debug가 시작된다.

kubectl exec -it mypod -c app -- sh
kubectl exec mypod -- cat /etc/resolv.conf     # 셸 없이 일회성 명령

port-forward는 로컬 포트를 API server 경유 터널로 Pod나 Service의 포트에 연결한다. ClusterIP뿐인 내부 서비스에 진단용 클라이언트를 붙일 때 유용하다. 예를 들어 클러스터 안의 PostgreSQL에 로컬 psql로 접속해 상태를 조회하는 식이다.

kubectl port-forward svc/postgres 5432:5432
kubectl port-forward pod/mypod 8080:8080

터널은 명령이 살아 있는 동안만 유지되는 일회성 경로다. 이 성질이 오히려 장점인데, 방화벽 규칙이나 Ingress 설정을 변경하지 않고 필요한 순간에만 내부에 닿는 통로를 연다. 서비스 노출 수단이 아니라 진단 수단이며, 상시 접근이 필요하면 Part V. 네트워킹의 Service와 Ingress로 해결한다.