Kubernetes Pod가 Terminating에서 멈췄을 때: 원인 5가지와 안전한 강제 삭제
kubectl delete pod를 쳤는데 몇 분이 지나도 STATUS가 Terminating에서 꿈쩍도 안 한다. CI 파이프라인은 멈추고, ArgoCD는 동기화가 안 끝나고, 야간 호출이 울린다. 이 글은 K8s_Troubleshooting_Guide 시리즈 5편으로, 바로 이 "Terminating 무한 대기" 상황만 집중적으로 다룹니다.
📌 4편까지 우리는
Pending(스케줄 불가),CrashLoopBackOff(컨테이너 재기동 루프),ImagePullBackOff같은 시작/실행 단계의 상태를 다뤘습니다. 이번 편은 그 반대편, 즉 Pod가 죽어가는 종료(Termination) 단계 한정입니다. 같은 증상처럼 보여도 진단 포인트가 완전히 다르니 헷갈리지 마세요.
kubectl delete는 '명령'이 아니라 '요청'이다
가장 먼저 이해해야 할 점은, kubectl delete pod가 Pod를 즉시 죽이는 명령이 아니라는 것입니다. 이 명령은 API 서버에 Pod의 metadata.deletionTimestamp 필드를 찍는 요청일 뿐입니다. deletionTimestamp가 찍히는 순간부터 아래 라이프사이클이 시작됩니다.
deletionTimestamp가 설정되고, Pod는Terminating상태로 표시됨- kubelet이 컨테이너에
preStop훅을 실행하고SIGTERM전송 terminationGracePeriodSeconds(기본 30초) 동안 종료를 기다림- 시간이 지나면
SIGKILL로 강제 종료 - 모든
finalizers가 제거되어야 API 서버가 Pod 객체를 실제로 삭제
즉 Terminating이 안 끝난다는 건, 이 5단계 어딘가에서 막혔다는 뜻입니다. 원인을 모르고 --force부터 때리면 StatefulSet에서 데이터가 깨질 수 있으니, 먼저 어디서 막혔는지 구분하는 게 핵심입니다.
원인 5가지 진단 매트릭스
| # | 원인 | 증상 | 진단 명령 | 핵심 YAML 필드 | 해결 방향 |
|---|---|---|---|---|---|
| 1 | 프로세스가 SIGTERM 무시 | grace period(30초)만큼 정확히 버티다 사라짐 | kubectl logs <pod> 로 종료 처리 로그 확인 | spec.terminationGracePeriodSeconds | 앱에 시그널 핸들러 추가, PID 1 문제 점검 |
| 2 | preStop 훅 지연/행 | grace period를 넘겨 계속 Terminating | kubectl describe pod 이벤트, lifecycle.preStop | spec.containers[].lifecycle.preStop | 훅 타임아웃 단축, sleep 값 점검 |
| 3 | finalizer 잔류 | deletionTimestamp는 찍혔는데 영원히 안 사라짐 | kubectl get pod -o yaml | grep -A5 finalizers | metadata.finalizers | 컨트롤러 복구 또는 finalizer 수동 제거 |
| 4 | 노드 NotReady (kubelet 불통) | 특정 노드의 Pod만 다수 Terminating | kubectl describe node <node> | status.conditions (Ready=Unknown) | 노드 복구 우선, 안 되면 강제 삭제 |
| 5 | 볼륨 detach 실패 | PVC 붙은 Pod만 안 사라짐, VolumeAttachment 잔류 | kubectl get volumeattachment | status / CSI 이벤트 | CSI 드라이버·스토리지 상태 확인 |
진단 루틴 체크리스트
--force를 누르기 전에 아래 순서대로 1분만 확인하세요. (다시 강조: Terminating 상태 한정 진단입니다.)
# 1. deletionTimestamp가 정말 찍혀 있는지 (= 삭제 요청은 들어갔는지)
kubectl get pod <name> -o jsonpath='{.metadata.deletionTimestamp}'
# 2. finalizer가 남아있는지 (원인 3의 핵심)
kubectl get pod <name> -o yaml | grep -A5 finalizers
# 3. grace period 설정값 확인
kubectl get pod <name> -o jsonpath='{.spec.terminationGracePeriodSeconds}'
# 4. 이벤트로 preStop / SIGKILL 흐름 확인
kubectl describe pod <name>
# 5. Pod가 떠 있던 노드의 상태 확인 (원인 4)
kubectl describe node <node> | grep -A5 Conditions
# 6. 볼륨 detach 잔류 확인 (원인 5)
kubectl get volumeattachment | grep <pv-name>판단 기준: finalizers 배열에 값이 있으면 → 원인 3. 노드 Condition이 Ready=Unknown이면 → 원인 4. 둘 다 깨끗한데 grace period만큼 정확히 버티면 → 원인 1·2. VolumeAttachment가 남아 있으면 → 원인 5입니다.
안전한 강제 삭제 절차
진단으로 원인을 좁혔다면 이제 정리할 차례입니다. 명령의 의미부터 정확히 알고 갑시다.
kubectl delete pod <name> --grace-period=0 --force이 명령은 grace period를 0으로 만들고, API 서버의 객체를 즉시 제거합니다. 문제는 노드가 살아있다면 컨테이너가 아직 실행 중일 수 있는데도, 컨트롤 플레인은 "Pod가 사라졌다"고 믿는다는 점입니다.
⚠️ 경고 — StatefulSet에서
--force를 함부로 쓰지 마세요. StatefulSet은web-0,web-1처럼 고유 ID와 PVC를 보장합니다. 노드가 살아있는 상태에서 강제 삭제하면, 컨트롤러가 같은 ID의 Pod를 새 노드에 띄우는데 기존 컨테이너가 여전히 같은 볼륨에 쓰고 있을 수 있습니다. 두 인스턴스가 동시에 쓰면 split-brain·데이터 손상이 발생합니다. DB·Kafka·etcd 같은 stateful 워크로드에서 특히 치명적입니다.
finalizer가 원인일 때
컨트롤러 버그나 GitOps 환경(ArgoCD 등)에서 finalizer가 잔류해 동기화가 멈추는 사례가 많습니다. 정상 절차는 finalizer를 책임지는 컨트롤러를 복구하는 것이지만, 컨트롤러가 이미 사라졌다면 수동 제거가 필요합니다.
kubectl patch pod <name> -p '{"metadata":{"finalizers":null}}' --type=merge⚠️ 경고 — finalizer 제거는 "정리 로직 건너뛰기"입니다. finalizer는 보통 "볼륨 detach 완료", "외부 LB 등록 해제" 같은 후처리를 끝낼 때까지 객체 삭제를 막는 안전장치입니다. 강제로 지우면 그 후처리가 누락된 채 객체만 사라집니다. 반드시 무엇을 위한 finalizer인지 확인한 뒤 제거하세요.
노드가 NotReady일 때 (원인 4)
가장 흔한 실수가 여기서 나옵니다. 노드가 NotReady면 kubelet이 컨트롤 플레인과 통신을 못 해 Pod를 정리할 수 없을 뿐, 컨테이너는 그 노드에서 여전히 돌고 있을 수 있습니다. 올바른 순서는:
- 노드 복구를 먼저 시도 (네트워크·kubelet 재기동). 복구되면 Pod는 알아서 정리됩니다.
- 복구 불가능하고 노드가 확실히 죽었다고 판단되면 →
kubectl delete node <node>로 노드를 제거하면 그 위의 Pod도 정리됩니다. - Kubernetes 1.30+의 Graceful Node Shutdown 기능이 켜져 있으면, 계획된 노드 종료 시 kubelet이 Pod를 먼저 정상 종료시켜 이 문제 자체를 줄여줍니다.
💬 실무 경험 한마디: 저는 사고 회고에서 "Terminating 멈춤"의 원인 절반 이상이 **노드 장애(원인 4)**였습니다. 그런데 당직자들이 습관적으로
--force부터 눌렀고, stateful 워크로드에서 두 번 데이터 정합성 이슈가 났습니다. 그래서 팀 런북에 "force 전에kubectl get pod -o yaml로 finalizer·노드 상태 3초 확인"을 강제 절차로 박아 넣었더니 재발이 사라졌습니다. 진단 한 줄이 데이터를 지킵니다.
재발 방지: graceful shutdown 설계
강제 삭제는 응급처치일 뿐, 진짜 해법은 Pod가 SIGTERM에 얌전히 죽도록 설계하는 것입니다.
Before — grace period·preStop 미설정 (SIGKILL로 거칠게 죽음):
spec:
containers:
- name: api
image: myapp:1.0
# terminationGracePeriodSeconds 없음 → 기본 30초
# preStop 없음 → LB가 트래픽 보내는 중에 종료될 수 있음After — graceful shutdown 적용:
spec:
terminationGracePeriodSeconds: 45 # 앱 종료 시간 + 여유
containers:
- name: api
image: myapp:1.0
lifecycle:
preStop:
exec:
# LB가 엔드포인트에서 빠질 시간 확보 후 종료
command: ["/bin/sh", "-c", "sleep 5"]핵심 원칙 세 가지:
- 앱에 SIGTERM 핸들러 구현: 새 요청은 거부하고 진행 중 요청만 마무리 후 종료
- terminationGracePeriodSeconds 튜닝: 앱의 실제 정리 시간보다 약간 길게. 너무 길면 강제 삭제가 오래 걸리고, 너무 짧으면 SIGKILL로 잘림
- preStop으로 LB 디레지스터 시간 확보:
sleep 5정도로 엔드포인트 제거가 전파될 시간을 줌
결론
Terminating 멈춤은 "Pod가 안 지워진다"는 하나의 증상이지만, 원인은 SIGTERM 무시 · preStop 지연 · finalizer 잔류 · 노드 NotReady · 볼륨 detach 실패의 5갈래입니다. --force는 만능 버튼이 아니라 원인을 안 뒤 마지막에 쓰는 칼입니다. 진단 → 안전한 정리 → graceful shutdown 설계 순서를 런북으로 만들어 두세요.
다음 6편에서는 컨테이너가 메모리 한계를 넘겨 죽는 OOMKilled 트러블슈팅을 다룹니다. requests/limits 튜닝과 cgroup 관점까지 파헤쳐 보겠습니다.
자주 묻는 질문 (FAQ)
Q. --grace-period=0 --force를 쓰면 데이터가 날아가나요?
A. 명령 자체가 데이터를 지우진 않습니다. 하지만 노드가 살아있는데 강제 삭제하면 컨테이너가 계속 볼륨에 쓰는 동안 같은 Pod가 다른 곳에 뜨면서 동시 쓰기로 인한 손상이 발생할 수 있습니다. 위험은 명령이 아니라 split-brain에서 옵니다.
Q. StatefulSet에서 강제 삭제하면 안 되는 이유는? A. StatefulSet은 동일 식별자와 PVC의 단일 인스턴스를 보장합니다. 노드 생존 상태에서 강제 삭제하면 컨트롤러가 같은 ID·같은 볼륨의 Pod를 새로 띄워 두 인스턴스가 공존할 수 있고, DB라면 데이터 정합성이 깨집니다. 반드시 기존 컨테이너가 죽은 게 확실할 때만 진행하세요.
Q. 노드가 NotReady인데 Pod가 Terminating이면?
A. kubelet이 통신 불가라 정리를 못 하는 상태입니다. 노드 복구를 먼저 시도하고, 노드가 확실히 죽었다면 kubectl delete node로 노드를 제거해 정리하세요. 노드 생존 여부를 확인하지 않은 채 Pod만 --force하는 게 가장 위험합니다.
Q. finalizer를 그냥 지워도 되나요?
A. finalizer는 볼륨 detach·외부 리소스 해제 같은 후처리를 보장하는 장치입니다. 무엇을 위한 것인지 확인하지 않고 finalizers:null로 지우면 후처리가 누락됩니다. 컨트롤러 복구가 1순위, 수동 제거는 컨트롤러가 사라진 예외 상황의 최후 수단입니다.
이 글은 AI 에이전트가 1차 초안을 작성한 뒤, 사람 편집자가 사실관계·출처·톤과 맥락을 검토하여 발행했습니다. 오류나 부정확한 내용이 확인되면 24시간 이내에 정정합니다.
댓글
불러오는 중...