K8s Finalizer란?
Kubernetes에서 리소스를 삭제하면 즉시 사라질 것 같지만, 실제로는 Finalizer가 설정된 리소스는 삭제되지 않고 “삭제 대기” 상태에 머문다. Finalizer는 리소스가 삭제되기 전에 정리 작업(cleanup)을 보장하는 메커니즘이다.
kubectl delete를 실행하면 Kubernetes는 즉시 삭제하지 않고, metadata.deletionTimestamp를 설정한 뒤 Finalizer를 가진 컨트롤러가 정리를 완료할 때까지 기다린다.
삭제 흐름: 단계별 동작
kubectl delete pod my-pod실행- API Server가
metadata.deletionTimestamp설정 (리소스는 아직 존재) - Finalizer를 가진 컨트롤러가 정리 작업 수행
- 컨트롤러가
metadata.finalizers목록에서 자신의 Finalizer를 제거 - 모든 Finalizer가 제거되면 API Server가 리소스를 실제 삭제
# Finalizer가 있는 리소스
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: my-pvc
finalizers:
- kubernetes.io/pvc-protection # PVC 보호 Finalizer
# → 이 PVC를 사용하는 Pod가 있는 한 삭제되지 않음
빌트인 Finalizer 종류
| Finalizer | 대상 | 역할 |
|---|---|---|
kubernetes.io/pvc-protection |
PVC | 사용 중인 PVC 삭제 방지 |
kubernetes.io/pv-protection |
PV | 바인딩된 PV 삭제 방지 |
foregroundDeletion |
모든 리소스 | 종속 리소스 먼저 삭제 후 부모 삭제 |
orphan |
모든 리소스 | 부모만 삭제, 종속 리소스 유지 |
Operator에서 Custom Finalizer 구현
CRD Operator를 만들 때 Finalizer는 필수 패턴이다. 외부 리소스(클라우드 인프라, DB, DNS 레코드 등)를 정리해야 하기 때문이다.
Go Operator 예시
const finalizerName = "database.example.com/cleanup"
func (r *DatabaseReconciler) Reconcile(ctx context.Context,
req ctrl.Request) (ctrl.Result, error) {
var db databasev1.Database
if err := r.Get(ctx, req.NamespacedName, &db); err != nil {
return ctrl.Result{}, client.IgnoreNotFound(err)
}
// 삭제 요청이 들어온 경우
if !db.DeletionTimestamp.IsZero() {
if containsString(db.Finalizers, finalizerName) {
// 1. 외부 리소스 정리
if err := r.deleteExternalDatabase(ctx, &db); err != nil {
return ctrl.Result{}, err
}
// 2. DNS 레코드 삭제
if err := r.deleteDNSRecord(ctx, &db); err != nil {
return ctrl.Result{}, err
}
// 3. Finalizer 제거 → K8s가 CR 삭제 완료
db.Finalizers = removeString(db.Finalizers, finalizerName)
if err := r.Update(ctx, &db); err != nil {
return ctrl.Result{}, err
}
}
return ctrl.Result{}, nil
}
// 생성/업데이트: Finalizer 등록
if !containsString(db.Finalizers, finalizerName) {
db.Finalizers = append(db.Finalizers, finalizerName)
if err := r.Update(ctx, &db); err != nil {
return ctrl.Result{}, err
}
}
// 정상 reconcile 로직...
return r.reconcileDatabase(ctx, &db)
}
핵심 패턴
- 생성 시: Finalizer를 먼저 등록 → 외부 리소스 생성
- 삭제 시: 외부 리소스 정리 → Finalizer 제거
- 이 순서가 바뀌면 외부 리소스가 고아(orphan)로 남을 수 있다
Finalizer가 걸려 삭제 안 되는 경우
운영 중 가장 흔한 문제다. 컨트롤러가 죽었거나, 외부 리소스 정리가 실패하면 Finalizer가 제거되지 않아 리소스가 Terminating 상태에 영원히 머문다.
진단
# Terminating 상태 리소스 찾기
kubectl get ns --field-selector status.phase=Terminating
# Finalizer 확인
kubectl get namespace stuck-ns -o jsonpath='{.metadata.finalizers}'
# ["kubernetes"]
# 모든 리소스의 Finalizer 확인
kubectl api-resources --verbs=list --namespaced -o name |
xargs -I {} kubectl get {} -n stuck-ns -o json 2>/dev/null |
jq '.items[] | select(.metadata.finalizers != null) |
{kind: .kind, name: .metadata.name, finalizers: .metadata.finalizers}'
해결: Finalizer 수동 제거
# 방법 1: kubectl patch
kubectl patch namespace stuck-ns -p
'{"metadata":{"finalizers":null}}' --type=merge
# 방법 2: kubectl edit
kubectl edit namespace stuck-ns
# metadata.finalizers 배열을 비우고 저장
# 방법 3: API 직접 호출 (Namespace 전용)
kubectl get namespace stuck-ns -o json |
jq '.spec.finalizers = []' |
kubectl replace --raw "/api/v1/namespaces/stuck-ns/finalize" -f -
⚠️ 경고: Finalizer를 강제 제거하면 외부 리소스가 정리되지 않은 채 남을 수 있다. 반드시 외부 리소스를 먼저 수동 확인/정리한 후 제거하라.
OwnerReference와 Cascade 삭제
Finalizer와 함께 이해해야 하는 것이 ownerReferences다. 부모 리소스가 삭제될 때 자식 리소스의 처리 방식을 결정한다:
# Deployment 삭제 시 ReplicaSet도 함께 삭제됨 (Cascade)
apiVersion: apps/v1
kind: ReplicaSet
metadata:
ownerReferences:
- apiVersion: apps/v1
kind: Deployment
name: my-app
uid: abc-123
controller: true
blockOwnerDeletion: true # foreground 삭제 시 부모 삭제 차단
| 삭제 정책 | 동작 | 사용법 |
|---|---|---|
| Background (기본) | 부모 즉시 삭제, 자식은 GC가 비동기 삭제 | kubectl delete deploy my-app |
| Foreground | 자식 먼저 삭제 → 부모 삭제 | --cascade=foreground |
| Orphan | 부모만 삭제, 자식 유지 (고아) | --cascade=orphan |
# Foreground: Pod 모두 종료 후 Deployment 삭제
kubectl delete deployment my-app --cascade=foreground
# Orphan: Deployment만 삭제, ReplicaSet/Pod는 유지
kubectl delete deployment my-app --cascade=orphan
운영 베스트 프랙티스
- Finalizer는 빠르게 처리하라: 외부 API 호출이 느리면 타임아웃과 재시도 로직을 반드시 구현하라. 영원히 Terminating에 빠지는 원인이 된다
- 멱등성 보장: Finalizer 정리 로직은 여러 번 실행되어도 안전해야 한다. 컨트롤러가 재시작되면 동일한 정리를 다시 시도한다
- Finalizer 이름 규칙: 도메인 접두사를 사용하라 (
mycompany.com/resource-cleanup). 다른 컨트롤러의 Finalizer와 충돌을 방지한다 - 모니터링: Terminating 상태가 5분 이상 지속되는 리소스를 Prometheus 알림으로 감지하라
- 강제 제거는 최후의 수단: 외부 리소스 정리를 먼저 확인한 후에만 Finalizer를 수동 제거하라
정리
Finalizer는 Kubernetes의 안전한 삭제 보장 메커니즘이다. 외부 리소스 정리, PVC 보호, Cascade 삭제 제어 등 리소스 라이프사이클의 핵심 역할을 한다. Operator 개발 시 Finalizer 패턴은 필수이며, 운영 시 Terminating 상태 리소스 진단·해결 능력은 K8s 엔지니어의 기본기다. OwnerReference와 함께 이해하면 Kubernetes의 리소스 삭제 모델을 완전히 파악할 수 있다.