1. 개요

ArgoCD Application 의 syncPolicy.automated 에는 스위치가 둘 있다. prune 과 selfHeal. 기본은 둘 다 false 다. 이 둘의 조합이 “장애 때 무엇을 할 수 있는가” 를 결정한다는 걸 운영하면서 알았다. 그리고 GitOps 저장소에 봇이 태그를 밀어 넣는 워크플로를 짜면서, 브랜치 보호가 없는 저장소에서 검증을 어디에 둬야 하는지도 정리했다.


2. 핵심 내용

2-1. prune: git 에서 지운 것을 클러스터에서도 지우는가

prune: false 면 git 에서 리소스를 삭제해도 클러스터에는 남는다. ArgoCD 는 OutOfSync 로 표시만 한다. 안전하지만, “git 이 곧 클러스터 상태” 라는 GitOps 의 약속이 깨진다. 지운 줄 알았던 NetworkPolicy 나 CronJob 이 살아 있다.

prune: false 로 운영하면 삭제는 두 단계 다. git 에서 지우고, kubectl delete 로 클러스터에서도 지운다. 이걸 잊으면 드리프트가 쌓인다. 드리프트 점검 스크립트를 주기적으로 돌려 “클러스터에 있는데 git 에 없는 것” 을 나열하고, 의도된 것은 allow 목록에 적는다.

2-2. selfHeal: 손으로 고친 것을 되돌리는가

selfHeal: false 면 kubectl edit 로 바꾼 것이 다음 git 커밋까지 유지된다. 장애 때 replicas 를 0 으로 내리거나 env 를 급히 바꾸는 “긴급 조치” 가 가능하다. 대신 그 조치가 git 에 없으니, 다음 sync 에서 되돌아간다. 언제? 다음 커밋이 들어올 때. 그게 새벽 3시일 수도 있다.

selfHeal: true 면 손으로 고친 것이 즉시(수 분 내) 되돌아간다. 긴급 조치를 하려면 git 에 커밋 해야 한다. 절차가 전부 git 경로로 바뀐다. 이걸 켜는 순간 “delete 로 롤백” 같은 문서의 문장이 거짓말이 된다. 그래서 켤 때 문서를 동반 PR 로 고쳤다.

어느 쪽이 맞나. 혼자 운영하고 git 커밋이 빠르면 selfHeal: true 가 낫다. 드리프트가 생길 틈이 없다. 여러 사람이 클러스터에 손대는 환경이면 false 로 두고 드리프트 점검을 돌리는 편이 현실적이다. 세 프로젝트 중 하나만 true 로 켰고, 나머지는 점검 스크립트로 간다.

2-3. 브랜치 상태에서 apply 한 것은 되돌아간다

selfHeal 과 무관하게 밟는 함정. 기능 브랜치에서 매니페스트를 고치고 kubectl apply -k 로 클러스터에 직접 적용해 테스트했다. 잘 됐다. PR 을 올리고 리뷰를 기다리는 사이 main 에 다른 커밋이 들어왔다. ArgoCD 가 main 을 sync 하면서 내 변경을 덮었다. 테스트가 잘 됐던 이유는 내 apply 가 살아 있던 짧은 시간 동안 봤기 때문이다.

클러스터에 직접 apply 한 것은 다음 sync 까지의 임시 상태다. 검증은 main 머지 후 sync 된 상태에서 다시 한다.

2-4. 봇이 태그를 치환할 때: fail-closed

배포 워크플로가 infra 저장소의 kustomization 에서 이미지 태그를 바꿔 push 한다. 이 저장소는 GitHub 무료 플랜의 private 이라 브랜치 보호를 걸 수 없다. PR CI 는 빨간불이 떠도 머지가 되는 감지기다. 봇의 push 는 PR 도 없이 main 에 직접 간다.

그래서 검증을 봇이 push 하기 전 에 둔다. 워크플로가 스스로 검증하고, 하나라도 의심스러우면 멈춘다.

- name: 태그 형식 검증
  run: echo "$TAG" | grep -Eq '^prod-[0-9a-f]{7}$'
 
- name: GHCR 에 이미지가 실제로 있는지
  run: docker manifest inspect "ghcr.io/$ORG/$APP:$TAG" > /dev/null
 
- name: 대상 항목이 정확히 1개인지 확인 후 치환
  run: |
    n=$(yq '[.images[] | select(.name == "'"$IMG"'")] | length' kustomization.yaml)
    [ "$n" = "1" ] || { echo "expected 1 image entry, got $n"; exit 1; }
    yq -i '(.images[] | select(.name == "'"$IMG"'")).newTag = "'"$TAG"'"' kustomization.yaml
 
- name: 렌더 결과에 새 태그가 나오는지
  run: kustomize build . | grep -q "$IMG:$TAG"
 
- name: push (충돌 시 rebase 재시도)
  run: |
    for i in 1 2 3 4 5; do
      git push && break
      git pull --rebase && sleep $((i * 2))
    done
  • 형식: latest 나 빈 문자열이 들어오는 걸 막는다.
  • 존재: 빌드가 스킵된 커밋(paths-ignore)은 태그가 없다. 없는 이미지를 배포하면 ArgoCD ImagePullBackOff.
  • 항목 개수: yq 의 select 가 0개나 2개를 잡으면 아무것도 안 바뀌거나 둘 다 바뀐다. 정확히 1개일 때만 진행.
  • 렌더: 치환이 실제로 최종 매니페스트에 반영됐는지. patch 나 overlay 가 덮을 수 있다.
  • 재시도: 두 앱이 동시에 배포하면 충돌한다. rebase 후 재시도.

롤백은 같은 워크플로에 옛 태그다. 배포 경로와 롤백 경로가 같으면 롤백을 따로 연습할 필요가 없다.

2-5. 스키마 검증이 통과시키는 조용한 실패

infra PR CI 에는 kubeconform 이 있다. 스키마는 잡는다. 하지만 스키마상 유효한데 의미가 틀린 것은 못 잡는다.

  • Gateway SecurityPolicy 의 targetRefs 에 오타 → 아무것도 보호하지 않는 유효한 정책
  • principal 조건 없는 Allow 규칙 → 전부 허용하는 유효한 규칙
  • Redis 인자에 --appendonly yes 누락 → AOF 없이 뜨는 유효한 StatefulSet

이런 건 렌더 결과를 읽는 스크립트로 따로 잡는다. “이 리소스가 참조하는 대상이 렌더 결과에 존재하는가”, “이 Allow 에 조건이 하나 이상 있는가”. 스크립트 하나에 케이스 셋. kubeconform 이 exit 0 을 줘도 이 스크립트가 exit 1 을 준다.


3. 마무리

요약

  • prune: false 면 삭제는 두 단계(git + kubectl). 드리프트 점검을 돌린다.
  • selfHeal: true 는 긴급 조치를 전부 git 경로로 바꾼다. 켤 때 문서도 함께.
  • 브랜치에서 apply 한 것은 다음 sync 에 되돌아간다. 검증은 main 에서.
  • 브랜치 보호가 없으면 검증은 봇이 push 하기 전에. 형식·존재·개수·렌더.
  • 스키마 검증은 의미를 못 잡는다. 렌더 결과를 읽는 스크립트를 따로.