Published on

Kubernetes Pod가 Terminating에 멈출 때 - finalizer·grace·SIGTERM 실전 디버깅

Authors

서버 운영 중 가장 불쾌한 순간 중 하나가 Pod가 Terminating에서 영원히 멈춰 있는 상황입니다. 롤링 업데이트는 멈추고(특히 Deployment/StatefulSet), 노드는 드레이닝이 끝나지 않으며, 오토스케일링/배포 파이프라인 전체가 지연됩니다. 이 현상은 단순히 “쿠버네티스가 느리다”가 아니라, 삭제(Deletion)가 완료되기 위한 조건이 충족되지 않았기 때문입니다.

이 글에서는 Pod 삭제 흐름을 짧게 복기한 뒤, 현장에서 가장 많이 맞닥뜨리는 원인인 finalizer, terminationGracePeriodSeconds(그레이스 기간), SIGTERM 처리, 볼륨/네트워크 정리 지연을 중심으로 “어디를 보면 원인이 보이는지”를 단계적으로 정리합니다.

> 참고: 비슷하게 운영을 막는 상태로는 Pending도 흔합니다. 원인별 체크는 EKS Pod Pending 0/XX nodes available 원인별 해결도 함께 보면 좋습니다.

Pod 삭제(termination) 동작 원리: 무엇이 끝나야 삭제가 끝나는가

Pod를 삭제하면 Kubernetes는 즉시 오브젝트를 지우지 않고 다음 과정을 거칩니다.

  1. API 서버가 Pod에 deletionTimestamp를 찍고 상태를 Terminating으로 보이게 함
  2. Kubelet이 컨테이너 런타임(containerd 등)에 종료 요청
  3. 각 컨테이너에 SIGTERM 전송 (일반적으로 PID 1)
  4. preStop 훅이 있다면 실행(주의: SIGTERM과 거의 동시에/직후로 동작하며, 구현에 따라 체감상 순서가 꼬여 보일 수 있음)
  5. terminationGracePeriodSeconds 동안 정상 종료를 기다림
  6. 시간 내 종료가 안 되면 SIGKILL
  7. 볼륨 언마운트/네트워크 정리(CNI), 엔드포인트/서비스 엔트리 정리
  8. 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를 못 하고 죽을 수 있음

그래서 권장 순서는 보통:

  1. 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/볼륨/노드를 순서대로 의심하면 대부분의 케이스는 해결됩니다.