K8s Finalizer 삭제 보장 심화

K8s Finalizer란?

Kubernetes에서 리소스를 삭제하면 즉시 사라질 것 같지만, 실제로는 Finalizer가 설정된 리소스는 삭제되지 않고 “삭제 대기” 상태에 머문다. Finalizer는 리소스가 삭제되기 전에 정리 작업(cleanup)을 보장하는 메커니즘이다.

kubectl delete를 실행하면 Kubernetes는 즉시 삭제하지 않고, metadata.deletionTimestamp를 설정한 뒤 Finalizer를 가진 컨트롤러가 정리를 완료할 때까지 기다린다.

삭제 흐름: 단계별 동작

  1. kubectl delete pod my-pod 실행
  2. API Server가 metadata.deletionTimestamp 설정 (리소스는 아직 존재)
  3. Finalizer를 가진 컨트롤러가 정리 작업 수행
  4. 컨트롤러가 metadata.finalizers 목록에서 자신의 Finalizer를 제거
  5. 모든 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의 리소스 삭제 모델을 완전히 파악할 수 있다.

위로 스크롤
WordPress Appliance - Powered by TurnKey Linux