- Published on
Kubernetes PV 멈춤 - finalizer로 삭제 안될 때
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서론
운영 중 PV(PersistentVolume)를 지우려는데 Terminating에서 멈춘 채로 사라지지 않는 경우가 있습니다. 대부분의 원인은 finalizer(리소스 정리 보장 장치)가 “아직 정리할 일이 남았다”고 판단해서 삭제를 막고 있기 때문입니다.
문제는 이 상태가 단순히 kubectl delete pv ...를 여러 번 반복한다고 해결되지 않는다는 점입니다. PV는 PVC, StorageClass의 reclaim policy, CSI 컨트롤러, 클라우드 볼륨(EBS/GCE PD 등)과 얽혀 있고, finalizer는 그 얽힘을 “끝까지 정리”하려고 버티는 장치이기 때문입니다.
이 글에서는 PV가 finalizer로 인해 삭제가 안 될 때를 대상으로:
- PV/PVC/VolumeAttachment/클라우드 볼륨의 관계를 빠르게 확인하고
- 어떤 finalizer가 왜 남았는지 파악한 뒤
- 데이터 손실/리소스 유실 위험을 최소화하면서 제거하는 절차
를 단계별로 정리합니다.
PV 삭제가 멈추는 메커니즘: finalizer란?
Kubernetes의 finalizer는 리소스 메타데이터에 달린 문자열 리스트로, 리소스 삭제 요청이 들어오면 오브젝트는 즉시 사라지지 않고 metadata.deletionTimestamp가 찍힌 뒤 finalizer가 모두 제거될 때까지 남아있습니다.
PV에서 자주 보는 finalizer 예시는 다음과 같습니다.
kubernetes.io/pv-protection: PV가 아직 PVC에 의해 사용 중(또는 보호 대상)일 때 실수로 삭제되지 않도록 보호external-provisioner.volume.kubernetes.io/finalizer: CSI external-provisioner가 볼륨 정리(삭제/해제)를 마칠 때까지 보호- (환경에 따라)
external-attacher/...류: attach/detach 정리와 연관
즉 PV가 멈췄다는 건 “컨트롤러가 정리를 못 끝냈다” 또는 “정리하면 위험하니 보호 중이다” 둘 중 하나인 경우가 많습니다.
증상 체크: PV가 정말 finalizer로 막혔는지 확인
먼저 PV 상태와 finalizer를 확인합니다.
kubectl get pv
kubectl describe pv <PV_NAME>
kubectl get pv <PV_NAME> -o jsonpath='{.metadata.deletionTimestamp}{"\n"}{.metadata.finalizers}{"\n"}'
deletionTimestamp가 존재하고finalizers가 남아있으면
삭제 요청은 들어갔지만 finalizer 때문에 삭제가 보류 중입니다.
다음으로 PV가 어떤 PVC에 바인딩되어 있는지 확인합니다.
kubectl get pv <PV_NAME> -o jsonpath='{.spec.claimRef.namespace}/{.spec.claimRef.name}{"\n"}'
kubectl get pv <PV_NAME> -o jsonpath='{.status.phase}{"\n"}'
claimRef가 남아있다면 PV는 PVC와 연결되어 있을 가능성이 높습니다.
1단계: PVC/Pod가 아직 PV를 잡고 있는지 확인
가장 흔한 케이스는 PV가 실제로 아직 사용 중인데, 사용자가 PV를 먼저 지우려는 상황입니다.
PVC 상태 확인
kubectl get pvc -A | grep -E '<PVC_NAME>|<PV_NAME>'
kubectl describe pvc -n <NS> <PVC_NAME>
PVC가 Terminating이거나 finalizer(kubernetes.io/pvc-protection)가 남아있을 수 있습니다.
Pod가 PVC를 마운트 중인지 확인
kubectl get pod -n <NS> -o json | jq -r '.items[] | select(.spec.volumes[]?.persistentVolumeClaim.claimName=="<PVC_NAME>") | .metadata.name'
Pod가 존재한다면 먼저 Pod/Workload를 정리해야 합니다. (Deployment/StatefulSet이 Pod를 재생성할 수 있으니 상위 리소스부터 스케일 다운/삭제)
이 단계에서 파드가 종료되지 않고 Terminating에 멈춘다면, 스토리지 detach가 꼬였을 가능성도 있습니다. 파드 종료/노드 상태까지 함께 보는 디버깅 루틴은 Kubernetes CrashLoopBackOff 원인별 로그·Probe·리소스 디버깅 글의 “상태 기반 접근”이 의외로 도움이 됩니다(리소스 이벤트/컨트롤러 관점).
2단계: StorageClass reclaimPolicy 확인 (Delete vs Retain)
PV 삭제/정리 동작은 StorageClass의 reclaim policy에 크게 좌우됩니다.
kubectl get pv <PV_NAME> -o jsonpath='{.spec.storageClassName}{"\n"}{.spec.persistentVolumeReclaimPolicy}{"\n"}'
kubectl get sc <SC_NAME> -o yaml
Delete: PVC가 삭제되면 프로비저너가 클라우드 볼륨까지 삭제하려고 시도Retain: 클라우드 볼륨은 남기고 PV만 Released 상태가 되기 쉬움(수동 정리 필요)
Delete인데도 PV가 finalizer로 멈춘다면, 대개 CSI 컨트롤러가 클라우드 API 호출에 실패했거나, 볼륨이 아직 attach 중이라 삭제를 못하는 경우가 많습니다.
3단계: CSI/attach-detach 경로 확인 (VolumeAttachment)
EBS 같은 블록 스토리지는 ‘어느 노드에 붙어있는지(attach)’가 매우 중요합니다. PV가 안 지워지는 경우 VolumeAttachment가 남아있거나 detach가 실패해 finalizer가 못 지워지는 경우가 있습니다.
kubectl get volumeattachment
kubectl get volumeattachment -o wide | grep -E '<PV_NAME>|<VOLUME_HANDLE>|<PVC_NAME>'
해당 VolumeAttachment를 자세히 봅니다.
kubectl describe volumeattachment <VA_NAME>
kubectl get volumeattachment <VA_NAME> -o jsonpath='{.metadata.finalizers}{"\n"}{.status.attached}{"\n"}{.status.detachError.message}{"\n"}'
status.attached=true인데 노드가 이미 죽었거나detachError가 반복된다면
클러스터 컨트롤 플레인 관점에서는 detach가 끝나지 않았고, 그 결과 PV finalizer도 남아있을 수 있습니다.
EKS에서 노드/네트워크 문제가 동반되는 경우도 흔합니다. 노드가 NotReady로 빠지면 detach가 지연되거나 컨트롤러가 꼬일 수 있는데, 이때는 EKS kubelet NotReady - CNI plugin not initialized 해결 같은 노드 상태 진단 글이 간접적으로 도움이 됩니다(노드가 정상인지가 스토리지 정리의 전제).
4단계: 이벤트/컨트롤러 로그로 “왜 finalizer가 안 지워지는지” 찾기
PV/PVC 이벤트를 먼저 봅니다.
kubectl get events -A --sort-by=.lastTimestamp | grep -E '<PV_NAME>|<PVC_NAME>'
CSI 기반이라면 다음 컴포넌트 로그가 핵심입니다.
csi-provisionercsi-attachercsi-controller(드라이버)
예: kube-system에 있는 EBS CSI 드라이버(환경에 따라 이름 상이)
kubectl -n kube-system get pod | grep -E 'csi|ebs|controller|provisioner|attacher'
kubectl -n kube-system logs deploy/ebs-csi-controller -c csi-provisioner --tail=200
kubectl -n kube-system logs deploy/ebs-csi-controller -c csi-attacher --tail=200
kubectl -n kube-system logs deploy/ebs-csi-controller -c ebs-plugin --tail=200
여기서 자주 나오는 패턴은:
- 클라우드 API 권한 부족(IAM)으로 DeleteVolume/DetachVolume 실패
- 볼륨이 여전히 다른 노드에 attach되어 있어 삭제 불가
- 컨트롤러가 크래시/재시작 반복으로 작업이 진행되지 않음
EKS에서 IAM/자격증명 이슈로 컨트롤러가 AWS API를 못 치는 경우라면, 원인이 애플리케이션 Pod가 아니라 **컨트롤러 서비스어카운트(IRSA)**일 수 있습니다. 비슷한 결의 점검 루틴은 EKS Pod에서 AWS SDK 자격증명 못찾음 해결 가이드에서 “누가 어떤 자격증명으로 API를 호출하는가” 관점이 참고됩니다.
5단계: 안전한 정리 절차(권장 순서)
finalizer를 무작정 지우기 전에, 아래 순서로 “정리할 수 있는 건 정리”하고 마지막에 finalizer를 건드리는 것을 권장합니다.
(1) Workload 중지 → Pod 종료
StatefulSet/Deployment 스케일 다운 또는 삭제 후 Pod가 없어졌는지 확인합니다.
kubectl -n <NS> scale sts/<NAME> --replicas=0
kubectl -n <NS> get pod | grep <APP>
(2) PVC 삭제
kubectl -n <NS> delete pvc <PVC_NAME>
PVC가 Terminating이면 PVC finalizer도 확인합니다.
kubectl -n <NS> get pvc <PVC_NAME> -o jsonpath='{.metadata.finalizers}{"\n"}'
(3) VolumeAttachment 정리/복구
정상이라면 detach가 진행되며 VolumeAttachment가 사라져야 합니다. 노드가 죽어있다면 노드 복구/정리(클라우드에서 인스턴스 종료 등) 후 detach가 진행되는지 확인합니다.
(4) 클라우드 볼륨 상태 확인
예: AWS EBS라면 PV의 volumeHandle을 확인합니다.
kubectl get pv <PV_NAME> -o jsonpath='{.spec.csi.volumeHandle}{"\n"}'
그 다음 AWS에서 해당 볼륨이 in-use인지 available인지 확인하고, 필요 시 강제 detach/삭제를 수행합니다(운영 정책에 맞게).
6단계: 그래도 안 되면 finalizer 제거(마지막 수단)
여기부터는 리소스 누수(클라우드 볼륨이 남음) 또는 데이터 손실(잘못된 볼륨 삭제) 위험이 있습니다. 반드시 아래를 확인하세요.
- 이 PV가 더 이상 어떤 Pod에도 필요 없는가?
- 클라우드 볼륨을 남겨야 하는가(데이터 보존) vs 삭제해도 되는가?
- detach가 안 된 상태에서 삭제하면 후폭풍이 없는가?
PV finalizer 제거
Patch로 finalizer를 비웁니다.
kubectl patch pv <PV_NAME> -p '{"metadata":{"finalizers":[]}}' --type=merge
또는 특정 finalizer만 제거하고 싶다면 JSON Patch를 씁니다(리스트 인덱스가 바뀔 수 있어 주의).
kubectl get pv <PV_NAME> -o json | jq '.metadata.finalizers'
# 예시: 전체 제거가 아니라면, 편집기로 확인 후 적용 권장
kubectl patch pv <PV_NAME> --type=json -p='[
{"op":"remove","path":"/metadata/finalizers"}
]'
PVC finalizer 제거(필요한 경우)
PVC가 먼저 멈춰서 PV까지 연쇄적으로 막는 경우가 있습니다.
kubectl patch pvc -n <NS> <PVC_NAME> -p '{"metadata":{"finalizers":[]}}' --type=merge
VolumeAttachment finalizer 제거(극단적 케이스)
Detach가 영구적으로 실패하고 더 이상 복구가 불가능한 노드/클러스터라면 VolumeAttachment를 강제로 정리해야 할 수도 있습니다.
kubectl patch volumeattachment <VA_NAME> -p '{"metadata":{"finalizers":[]}}' --type=merge
kubectl delete volumeattachment <VA_NAME>
단, 이 경우 클라우드에서 실제로는 attach가 남아 있을 수 있으므로, 이후 클라우드 콘솔/CLI에서 볼륨 상태를 반드시 재확인해야 합니다.
실전 시나리오별 빠른 처방전
시나리오 A: PV finalizer가 kubernetes.io/pv-protection 하나만 남음
- 대개 PVC/Pod가 아직 연결되어 있거나, PVC가 Terminating
- PVC/Pod부터 정리하고 PV는 기다리는 것이 정석
시나리오 B: external-provisioner.../finalizer가 남고 클라우드 볼륨 삭제가 안 됨
- CSI 컨트롤러 로그에서 DeleteVolume 실패 원인을 찾기
- IAM/권한/클라우드 API 오류, 볼륨 in-use 여부 확인
- 클라우드에서 볼륨을 수동 삭제/해제 후 PV finalizer 제거
시나리오 C: 노드가 죽었고 VolumeAttachment가 남아 detach가 안 됨
- 노드/인스턴스 상태부터 정상화 또는 완전 종료 처리
- 이후 detach 재시도 유도
- 최후에 VolumeAttachment/PV finalizer 제거
재발 방지 체크리스트
- StorageClass reclaim policy를 서비스 성격에 맞게 설계(
DeletevsRetain) - CSI 컨트롤러(IRSA/IAM) 권한을 최소 필요 권한으로 정확히 부여(Delete/Detach 포함)
- 노드 장애 시 스토리지 detach가 지연될 수 있으므로 노드 수명주기(autoscaler/karpenter)와 스토리지 정책을 함께 검토
- “강제 finalizer 제거”는 런북으로 문서화하고, 수행 전 확인 항목(볼륨 핸들, attach 상태, 데이터 백업)을 표준화
마무리
PV가 Terminating에서 멈출 때 핵심은 “PV만의 문제”로 보지 않고 PVC → Pod → VolumeAttachment → CSI 컨트롤러 → 클라우드 볼륨까지 연결된 정리 체인을 따라가는 것입니다. 대부분은 이 체인 중 한 군데(특히 detach/권한/컨트롤러 장애)에서 정리가 막혀 finalizer가 남습니다.
마지막 수단으로 finalizer를 제거할 수는 있지만, 그 전에 반드시 **실제 스토리지 리소스의 상태(attach 여부, 삭제 가능 여부)**를 확인하고, 필요하다면 클라우드에서 수동 정리를 병행하세요. 그래야 “Kubernetes에서는 지웠는데 비용은 계속 나가거나, 데이터가 예기치 않게 날아가는” 사고를 피할 수 있습니다.