- Published on
Kubernetes Pod가 Terminating에 멈출 때 - finalizer·grace·SIGTERM 실전 디버깅
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서버 운영 중 가장 불쾌한 순간 중 하나가 Pod가 Terminating에서 영원히 멈춰 있는 상황입니다. 롤링 업데이트는 멈추고(특히 Deployment/StatefulSet), 노드는 드레이닝이 끝나지 않으며, 오토스케일링/배포 파이프라인 전체가 지연됩니다. 이 현상은 단순히 “쿠버네티스가 느리다”가 아니라, 삭제(Deletion)가 완료되기 위한 조건이 충족되지 않았기 때문입니다.
이 글에서는 Pod 삭제 흐름을 짧게 복기한 뒤, 현장에서 가장 많이 맞닥뜨리는 원인인 finalizer, terminationGracePeriodSeconds(그레이스 기간), SIGTERM 처리, 볼륨/네트워크 정리 지연을 중심으로 “어디를 보면 원인이 보이는지”를 단계적으로 정리합니다.
> 참고: 비슷하게 운영을 막는 상태로는 Pending도 흔합니다. 원인별 체크는 EKS Pod Pending 0/XX nodes available 원인별 해결도 함께 보면 좋습니다.
Pod 삭제(termination) 동작 원리: 무엇이 끝나야 삭제가 끝나는가
Pod를 삭제하면 Kubernetes는 즉시 오브젝트를 지우지 않고 다음 과정을 거칩니다.
- API 서버가 Pod에
deletionTimestamp를 찍고 상태를Terminating으로 보이게 함 - Kubelet이 컨테이너 런타임(containerd 등)에 종료 요청
- 각 컨테이너에 SIGTERM 전송 (일반적으로 PID 1)
preStop훅이 있다면 실행(주의: SIGTERM과 거의 동시에/직후로 동작하며, 구현에 따라 체감상 순서가 꼬여 보일 수 있음)terminationGracePeriodSeconds동안 정상 종료를 기다림- 시간 내 종료가 안 되면 SIGKILL
- 볼륨 언마운트/네트워크 정리(CNI), 엔드포인트/서비스 엔트리 정리
- finalizer가 있으면 finalizer가 모두 제거될 때까지 오브젝트 삭제가 보류
즉, Terminating에 멈춘다는 것은 아래 중 하나가 흔합니다.
- finalizer가 남아 있음(가장 자주 봄)
- 컨테이너가 SIGTERM을 무시/미처리하거나, 프로세스가 좀비/데드락 상태
- grace period가 너무 길거나, preStop이 장시간 블로킹
- 볼륨 detach/umount가 지연(특히 PV, CSI)
- 노드/런타임 이슈로 kubelet이 종료 처리를 마무리 못함(노드 NotReady, containerd hang 등)
1단계: “무엇이 Pod 삭제를 막는지” 30초 안에 확인하는 명령
먼저 Pod의 메타데이터를 봐야 합니다. 특히 metadata.finalizers, deletionTimestamp, deletionGracePeriodSeconds가 핵심입니다.
# Pod의 삭제 관련 필드 빠르게 확인
kubectl get pod -n <ns> <pod> -o json | jq '{name:.metadata.name, deletionTimestamp:.metadata.deletionTimestamp, grace:.spec.terminationGracePeriodSeconds, deletionGrace:.metadata.deletionGracePeriodSeconds, finalizers:.metadata.finalizers}'
# 이벤트에서 힌트 찾기
kubectl describe pod -n <ns> <pod>
# kubelet/노드 문제 감지: Pod가 어느 노드에 있는지
kubectl get pod -n <ns> <pod> -o wide
여기서 finalizers가 비어 있지 않다면 2단계로, finalizer가 없다면 SIGTERM/그레이스/볼륨/노드 쪽으로 바로 넘어가면 됩니다.
2단계: Finalizer 때문에 Terminating에 멈추는 케이스
finalizer란?
Finalizer는 “오브젝트 삭제 전에 반드시 수행되어야 하는 정리 작업”을 보장하기 위한 장치입니다. 흔한 예:
- Ingress/Service 관련 컨트롤러가 외부 로드밸런서/보안그룹을 정리
- CSI 드라이버가 볼륨 detach를 보장
- 커스텀 컨트롤러가 외부 리소스(예: DNS 레코드, IAM 등)를 삭제
문제는 컨트롤러가 죽었거나 권한이 깨졌거나(예: IRSA/권한 변경), 외부 API가 막혀 정리 작업이 실패하면 finalizer가 제거되지 않아 Pod(혹은 상위 리소스)가 계속 Terminating에 남습니다.
finalizer 확인
kubectl get pod -n <ns> <pod> -o jsonpath='{.metadata.finalizers}'
어떤 컨트롤러가 finalizer를 걸었는지 추적
finalizer 문자열에 힌트가 있습니다(예: kubernetes.io/pvc-protection, external-attacher/..., .../foregroundDeletion). 또한 이벤트/컨트롤러 로그를 확인하세요.
# 관련 네임스페이스 컨트롤러 로그(예시: csi, ingress controller 등)
kubectl get pods -A | egrep 'csi|ingress|controller'
kubectl logs -n kube-system <controller-pod> --tail=200
(주의) finalizer 강제 제거
운영에서 “정말로” 외부 정리를 포기하고라도 오브젝트를 지워야 할 때가 있습니다. 이때는 finalizer를 제거합니다. 다만 외부 리소스가 고아(orphan)로 남을 수 있음을 감수해야 합니다.
# finalizer 제거: JSON Patch
kubectl patch pod -n <ns> <pod> --type='json' -p='[
{"op":"remove","path":"/metadata/finalizers"}
]'
만약 remove가 실패하면(필드가 비어있거나 path 문제) replace를 씁니다.
kubectl patch pod -n <ns> <pod> --type='json' -p='[
{"op":"replace","path":"/metadata/finalizers","value":[]}
]'
> EKS에서 컨트롤러 권한/IRSA 문제로 외부 정리가 막히는 경우도 흔합니다. 비슷한 진단 흐름은 EKS ExternalSecret 미동작 - IRSA·KMS·권한 10분 진단처럼 “컨트롤러 권한/외부 API 호출”을 의심하는 방식이 도움이 됩니다.
3단계: terminationGracePeriodSeconds와 preStop이 삭제를 ‘지연’시키는 케이스
Finalizer가 없는데도 Terminating이 오래 간다면, 대부분은 그레이스 기간이 길거나(preStop 포함) 애플리케이션 종료가 늦는 문제입니다.
그레이스 기간 확인
kubectl get pod -n <ns> <pod> -o jsonpath='{.spec.terminationGracePeriodSeconds}'
- 기본값은 30초
- 300초, 600초처럼 과도하게 크면 배포/스케일 인에 큰 지연을 유발
preStop 훅이 병목인 경우
preStop이 외부 API 호출/긴 sleep/락 대기 등을 하면 Pod 삭제가 그만큼 늦어집니다.
kubectl get pod -n <ns> <pod> -o json | jq '.spec.containers[].lifecycle'
권장 패턴:
- preStop은 “최소 작업”만 수행(예: readiness false 전환, 짧은 drain)
- 긴 정리 작업은 앱 내부에서 SIGTERM 처리로 옮기거나, 별도 컨트롤 플레인(잡/워커)로 분리
4단계: SIGTERM을 제대로 처리하지 못해 종료가 안 되는 케이스
Kubernetes는 컨테이너에 SIGTERM을 보냅니다. 그런데 다음과 같은 이유로 프로세스가 안 죽습니다.
- PID 1이 신호 처리를 안 함(특히 쉘 스크립트 wrapper)
- 멀티 프로세스에서 자식 프로세스가 남음
- 앱이 SIGTERM에서 데드락(락/IO 대기) 상태
- 무한 재시도/블로킹 I/O로 종료 루틴이 끝나지 않음
PID 1 문제(쉘 래퍼) 점검
컨테이너 CMD가 sh -c "..." 형태면 PID 1이 쉘이 되어 신호 전달이 꼬일 수 있습니다. 가능하면 exec 형태로 PID 1이 앱이 되게 하세요.
나쁜 예:
CMD ["sh","-c","python app.py"]
좋은 예:
CMD ["python","app.py"]
(예시) Python에서 SIGTERM 핸들링
import signal
import time
import threading
shutdown = threading.Event()
def handle_sigterm(signum, frame):
shutdown.set()
signal.signal(signal.SIGTERM, handle_sigterm)
while not shutdown.is_set():
# main loop
time.sleep(0.5)
# cleanup here (close sockets, flush, stop workers)
(예시) Node.js에서 SIGTERM 핸들링
const server = app.listen(3000);
process.on('SIGTERM', () => {
server.close(() => {
process.exit(0);
});
// hard timeout to avoid hanging forever
setTimeout(() => process.exit(1), 25_000).unref();
});
종료가 안 되는지 컨테이너 내부에서 확인
삭제 중인 Pod는 kubectl exec가 막히기도 하지만, 가능하면 프로세스 상태를 봅니다.
kubectl exec -n <ns> <pod> -- ps aux
kubectl exec -n <ns> <pod> -- cat /proc/1/status | head
5단계: 볼륨(PV/CSI) detach·umount 지연으로 Terminating이 길어지는 케이스
Stateful workload에서 특히 흔합니다.
- CSI 드라이버가 detach를 못함
- 노드가 NotReady가 되어 unmount가 완료되지 않음
- NFS/EFS 같은 네트워크 파일시스템이 끊기며 umount가 지연
체크 포인트:
# PVC/PV 이벤트 확인
kubectl describe pvc -n <ns> <pvc>
# Pod가 사용하는 볼륨 목록
kubectl get pod -n <ns> <pod> -o json | jq '.spec.volumes'
# kube-system의 CSI 관련 파드 로그(드라이버/attacher/provisioner)
kubectl get pods -n kube-system | grep csi
kubectl logs -n kube-system <csi-pod> --tail=200
여기서도 finalizer가 PV/PVC에 걸려 삭제를 막는 경우가 있으니, Pod만 보지 말고 PVC/PV의 finalizer도 확인해야 합니다.
6단계: 노드/런타임 문제로 kubelet이 마무리를 못하는 케이스
Pod가 특정 노드에 붙어 있고 그 노드가 문제라면, 삭제가 지연될 수 있습니다.
- Node가
NotReady - kubelet이 멈춤/과부하
- containerd가 hang
- CNI 플러그인이 teardown에서 실패
확인:
kubectl get node <node> -o wide
kubectl describe node <node> | tail -n 80
# 해당 노드의 시스템 로그(접근 가능할 때)
# journalctl -u kubelet -f
# journalctl -u containerd -f
이 경우 운영적으로는 노드 드레인/재부팅이 가장 빠른 해결책이 되는 경우도 많습니다(단, 상태 저장 워크로드는 PV 영향 고려).
7단계: “지금 당장 지워야 한다” 강제 삭제 옵션과 부작용
상황이 급하면 강제 삭제를 하게 됩니다. 다만 이는 “정상 종료/정리 작업을 건너뛰는” 선택이므로, 이후 장애(커넥션 끊김, 데이터 손상, 고아 리소스)가 생길 수 있습니다.
강제 삭제 명령
# grace period 0 + force
kubectl delete pod -n <ns> <pod> --grace-period=0 --force
주의할 점:
- API 오브젝트는 지워져도 노드에 컨테이너가 남는 ghost container 상황이 드물게 발생 가능
- PV/네트워크 정리가 미완료일 수 있음
- 애플리케이션이 트랜잭션/버퍼 flush를 못 하고 죽을 수 있음
그래서 권장 순서는 보통:
- finalizer/이벤트로 원인 확인 → 2) 컨트롤러/권한/외부 API 복구 → 3) 그래도 안 되면 finalizer 제거 → 4) 최후에 강제 삭제
운영 체크리스트: 재발 방지 설계 포인트
1) 종료 시간을 수치로 관리
- 앱 종료(드레인+정리)가 30초 넘는다면 이유가 있습니다.
terminationGracePeriodSeconds를 “막연히 크게” 두지 말고, 실제 종료 시간을 측정해 상한을 둡니다.
2) Readiness와 종료를 연동
- SIGTERM 받으면 readiness를 빠르게 false로 만들어 트래픽을 먼저 끊고, 그 다음 정리
- 프록시/ALB/NLB 등과의 타임아웃도 함께 조정(백엔드 종료가 느리면 502/504가 증가)
> 트래픽 단에서의 502/504 증상과 연결되는 경우가 많습니다. 원인별 점검은 AWS ALB 502·504 난사 - 원인별 해결 체크리스트도 참고하세요.
3) finalizer는 “필요 최소”로
- 커스텀 컨트롤러가 finalizer를 건다면, 외부 API 실패 시 재시도/타임아웃/백오프를 명확히
- 컨트롤러 다운 시에도 운영자가 안전하게 해제할 수 있는 런북 마련
4) 상태 저장 워크로드는 CSI/스토리지 이벤트를 1급으로 모니터링
kubectl describe pvc/pv이벤트를 수집- CSI 컨트롤러/노드 플러그인 로그를 관측
마무리: Terminating은 “원인이 있는 대기”다
Pod의 Terminating은 쿠버네티스가 멈춘 게 아니라, 삭제 완료 조건(정리 작업)이 충족되지 않아 기다리는 상태입니다. 가장 빠른 진단은 다음 3줄로 시작합니다.
kubectl get pod -n <ns> <pod> -o json | jq '{deletionTimestamp:.metadata.deletionTimestamp, finalizers:.metadata.finalizers, grace:.spec.terminationGracePeriodSeconds}'
kubectl describe pod -n <ns> <pod>
kubectl get pod -n <ns> <pod> -o wide
여기서 finalizer가 보이면 컨트롤러/권한/외부 리소스 정리를, finalizer가 없으면 SIGTERM/그레이스/preStop/볼륨/노드를 순서대로 의심하면 대부분의 케이스는 해결됩니다.