왜 드리프트가 문제인가

GitOps는 Git을 단일 진실 공급원(SSOT)으로 두고 클러스터 상태를 선언적으로 수렴시킨다. 하지만 운영 현장에서는 kubectl edit로 급하게 replica를 늘리거나, HPA가 값을 바꾸거나, 다른 컨트롤러가 리소스를 수정하는 일이 흔하다. 이렇게 실제 상태가 Git과 어긋나는 것을 드리프트(drift)라고 한다.

드리프트를 방치하면 세 가지가 무너진다. 첫째, 롤백 가능성이다. Git 이력이 실제 상태를 반영하지 못하면 "이전 상태로 복구"가 거짓말이 된다. 둘째, 재현성이다. 재해 복구로 클러스터를 재구축하면 손으로 바꾼 변경이 사라진다. 셋째, 감사 추적이다. 누가 무엇을 왜 바꿨는지 Git에 남지 않는다.

self-heal의 기본 동작

self-heal은 컨트롤러가 실제 상태와 목표 상태의 차이를 감지하면 사람이 sync를 누르지 않아도 자동으로 Git 상태를 다시 적용하는 정책이다. Argo CD 기준으로 selfHeal: true와 prune: true가 핵심이다. selfHeal은 손으로 바꾼 값을 되돌리고, prune은 Git에서 삭제된 리소스를 클러스터에서도 제거한다.

apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: payments
  namespace: argocd
spec:
  project: default
  source:
    repoURL: https://git.example.com/infra/payments.git
    targetRevision: main
    path: overlays/prod
  destination:
    server: https://kubernetes.default.svc
    namespace: payments
  syncPolicy:
    automated:
      prune: true
      selfHeal: true
    retry:
      limit: 5
      backoff:
        duration: 10s
        factor: 2
        maxDuration: 3m

무엇을 무시할 것인가: 정당한 드리프트 예외

모든 차이를 되돌리면 안 된다. HPA가 관리하는 replica, 특정 컨트롤러가 주입하는 필드는 정상적인 동적 값이다. 이걸 self-heal이 계속 되돌리면 두 컨트롤러가 무한 루프로 싸운다. ignoreDifferences로 소유권을 명확히 나눠야 한다.

spec:
  ignoreDifferences:
    - group: apps
      kind: Deployment
      jsonPointers:
        - /spec/replicas          # HPA 소유
    - group: ""
      kind: Service
      jqPathExpressions:
        - .spec.clusterIP         # 클러스터가 할당

원칙은 단순하다. "누가 이 필드의 소유자인가"를 먼저 정하고, Git이 소유하지 않는 필드만 무시 목록에 넣는다.

수동 개입과 자동화의 경계

상황권장 정책이유
스테이징/개발selfHeal on, prune on빠른 수렴, 실험 흔적 제거
프로덕션 앱selfHeal on, prune 신중손 수정 차단, 삭제는 위험
DB/스테이트풀selfHeal off 또는 부분데이터 손실 방지
긴급 장애 대응일시 중단(maintenance)사람 우선, 사후 Git 반영

특히 prune은 프로덕션에서 오탐 시 리소스를 지운다. prune: true와 함께 삭제 직전 단계 확인이 필요한 리소스에는 Prune=false 애노테이션을 개별 지정한다.

긴급 상황: self-heal 잠시 끄기

장애 대응 중에는 사람이 자유롭게 실험해야 한다. 이때 self-heal이 변경을 계속 되돌리면 대응이 방해된다. 애플리케이션 단위로 자동 동기화를 일시 중단하는 스크립트를 준비해 두면 좋다.

#!/usr/bin/env bash
set -euo pipefail
APP="${1:?usage: pause-selfheal.sh }"

# 자동 동기화 해제 (수동 개입 허용)
argocd app set "$APP" --sync-policy none
echo "self-heal paused for $APP. 대응 후 반드시 재개하세요."

# 재개 시:
# argocd app set "$APP" --sync-policy automated --self-heal --auto-prune

중요한 점은 "재개를 잊지 않는 것"이다. 중단 상태를 슬랙 알림이나 만료 타이머로 추적해 방치되지 않게 한다.

드리프트 감지와 관측

self-heal이 조용히 되돌리기만 하면, 누가 왜 손댔는지 파악할 기회를 잃는다. OutOfSync 이벤트를 반드시 메트릭과 알림으로 노출해야 한다. Argo CD는 argocd_app_info 등의 메트릭을 제공하므로, Prometheus 규칙으로 반복 드리프트를 잡아낸다.

groups:
  - name: gitops-drift
    rules:
      - alert: FrequentDrift
        expr: |
          sum by (name) (
            increase(argocd_app_sync_total{phase="Succeeded"}[1h])
          ) > 5
        for: 10m
        labels: { severity: warning }
        annotations:
          summary: "{{ $labels.name }} 잦은 self-heal 발생"
          description: "1시간 내 5회 초과 동기화. 소유권/파이프라인 점검 필요"

동기화가 자주 일어난다는 것은 대개 두 컨트롤러의 필드 소유권 충돌이거나, 외부 프로세스가 클러스터를 직접 건드리고 있다는 신호다.

운영 시 주의점

self-heal은 만능이 아니다. 몇 가지를 반드시 지킨다. 첫째, RBAC로 프로덕션 kubectl 쓰기 권한을 최소화한다. 되돌리기보다 애초에 손대지 못하게 하는 편이 낫다. 둘째, 시크릿·스테이트풀·CRD는 self-heal 범위를 신중히 정한다. 셋째, retry backoff를 두어 일시적 API 오류에 과도하게 재시도하지 않게 한다. 넷째, 모든 정책 변경 자체도 Git PR로 남긴다. self-heal 정책을 손으로 끄고 켜는 것이야말로 추적되지 않는 드리프트의 시작이다.

결국 self-heal의 목표는 "사람의 개입을 없애는 것"이 아니라 "개입을 반드시 Git으로 흘려보내게 강제하는 것"이다. 이 방향만 지키면 클러스터 상태와 이력의 신뢰를 오래 유지할 수 있다.