- Published on
Argo CD Sync 실패 - Helm diff·hook 충돌 해결
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서로 다른 도구가 같은 리소스를 서로 다른 관점으로 해석하면, GitOps 파이프라인은 생각보다 쉽게 깨집니다. 특히 Argo CD로 Helm 차트를 배포할 때 helm diff 를 프리체크로 붙이거나, 차트 내부에 pre-install·pre-upgrade hook Job이 포함돼 있으면 Sync 단계에서 실패하거나, 성공해도 OutOfSync가 반복되는 경우가 많습니다.
이 글은 다음 문제를 대상으로 합니다.
- Argo CD Sync가 실패하면서
ComparisonError또는 diff 관련 메시지가 보임 - Helm hook Job이 매 Sync마다 새로 생성되거나, 완료된 Job이 남아 diff가 계속 발생
helm diff는 실패하는데 실제helm upgrade는 되는 이상 현상
관련해서 OutOfSync 무한 루프 자체가 주 증상이라면 아래 글도 함께 보면 원인 분리가 빨라집니다.
증상 패턴: 어디서부터 꼬이나
1) Sync 직전 diff 단계에서 실패
CI에서 helm diff upgrade 를 돌리고 통과해야 Argo CD가 Sync하도록 구성한 경우가 많습니다. 이때 diff가 다음과 같은 이유로 실패합니다.
- hook 리소스가 diff 대상에 포함돼, 이미 존재하는 Job과 충돌
lookup함수나 클러스터 상태를 참조하는 템플릿이 diff 시점에 다른 결과를 만듦--three-way-merge유무, 서버사이드 드라이런 사용 여부에 따라 결과가 달라짐
2) Sync는 성공했는데 OutOfSync가 다시 뜸
Helm hook Job은 본질적으로 “일회성” 실행을 의도합니다. 하지만 GitOps는 “선언된 상태가 유지”되길 기대합니다. 그래서 다음이 자주 발생합니다.
- 완료된 Job의
status필드가 계속 변해서 diff가 발생 - Job 이름이 고정이면 다음 Sync에서
already exists로 실패 - Job 이름이 랜덤이면 Argo CD가 “Git에 없는 리소스가 생김”으로 판단
원인 1: Helm hook Job이 Argo CD의 선언적 모델과 충돌
Helm hook은 Helm이 릴리스 라이프사이클에 맞춰 리소스를 생성하고, 필요하면 삭제합니다. 반면 Argo CD는 “Git에 있는 매니페스트는 클러스터에 존재해야 하고, Git에 없는 것은 제거되어야 한다”는 모델에 가깝습니다.
즉, Helm hook으로 생성된 Job이 다음 조건을 만족하면 충돌이 커집니다.
- Job이 Git에 포함돼 있고, Argo CD가 이를 평범한 리소스로 관리
- Job이 실행 후 남아있어 상태 변화가 계속 diff로 잡힘
- hook delete policy가 없거나, Argo CD 쪽 prune 정책과 맞지 않음
해결 1-1) hook Job을 Argo CD 관리 대상에서 제외하거나, diff에서 무시
가장 현실적인 접근은 “hook은 실행만 하고, 선언적 동기화 대상에서 제외”하는 것입니다.
옵션 A: Argo CD에서 특정 리소스 diff 무시
Argo CD ConfigMap에서 리소스 커스터마이징을 추가해 Job의 특정 필드를 무시합니다.
apiVersion: v1
kind: ConfigMap
metadata:
name: argocd-cm
namespace: argocd
data:
resource.customizations: |
batch/Job:
ignoreDifferences: |
jsonPointers:
- /status
이렇게 하면 Job의 status 변동으로 OutOfSync가 뜨는 문제를 크게 줄일 수 있습니다.
옵션 B: hook 리소스를 아예 Sync에서 제외
차트 쪽에서 hook Job에 Argo CD 어노테이션을 추가해 스킵할 수 있습니다.
metadata:
annotations:
argocd.argoproj.io/sync-options: SkipDryRunOnMissingResource=true
주의: 위 어노테이션은 “드라이런에서 누락 리소스 처리”에 가깝고, 완전한 제외 전략은 상황에 따라 다릅니다. Job을 완전히 GitOps 관리에서 빼려면 hook을 차트 외부(예: 별도 파이프라인)로 분리하는 편이 더 확실합니다.
해결 1-2) hook delete policy로 잔존 리소스 제거
hook Job이 남아 diff를 유발한다면 Helm hook delete policy를 명시합니다.
metadata:
annotations:
"helm.sh/hook": pre-install,pre-upgrade
"helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded
before-hook-creation: 다음 실행 전 기존 hook 리소스 삭제hook-succeeded: 성공 시 삭제
이 조합은 “이미 존재하는 Job 때문에 Sync가 실패”하는 케이스를 줄여줍니다.
원인 2: helm diff 가 hook 리소스를 포함해 충돌을 키움
helm diff 는 기본적으로 “업그레이드 시 적용될 변경”을 비교합니다. 그런데 hook 리소스는 업그레이드의 본질적 상태라기보다 “절차”에 가깝습니다. 그래서 diff에 hook이 포함되면 다음 문제가 생깁니다.
- hook Job이 항상 새로 생성되거나 삭제되며 diff가 커짐
before-hook-creation이 걸려 있으면 diff가 리소스 삭제/생성으로 요동
해결 2-1) helm diff 에서 hook 제외
helm-diff 플러그인은 hook을 제외하는 옵션을 제공합니다. 환경에 따라 옵션 이름이 다를 수 있으니, 실제 사용 중인 버전에서 helm diff --help 를 확인하세요.
일반적으로는 아래 형태로 구성합니다.
helm diff upgrade myrel ./chart \
--namespace myns \
--values values.yaml \
--no-hooks
CI에서 “diff 통과 후 배포”를 강제하는 경우, hook을 diff에서 제외하면 불필요한 실패를 크게 줄일 수 있습니다.
해결 2-2) 서버사이드 드라이런과 동작 일치시키기
Argo CD는 내부적으로 쿠버네티스 API 기반 비교를 수행합니다. 반면 helm diff 는 로컬 렌더링 결과와 클러스터 상태를 비교하는 방식이라, 드라이런/머지 전략 차이로 결과가 달라질 수 있습니다.
가능하면 diff 단계도 서버사이드 드라이런에 최대한 가깝게 맞춥니다.
helm template ./chart \
--namespace myns \
--values values.yaml \
| kubectl apply --dry-run=server -f -
이 흐름은 “렌더링 결과가 실제 API에서 거부되는지”를 조기에 잡는 데 유용합니다.
원인 3: Sync Wave, hook 순서가 꼬여 의존 리소스가 먼저 적용됨
hook Job이 ConfigMap, Secret, CRD, RBAC 같은 선행 리소스를 필요로 하는데 Argo CD가 동시에 적용하면, Job이 먼저 실행되며 실패할 수 있습니다.
해결 3-1) Argo CD Sync Wave로 순서 강제
리소스에 argocd.argoproj.io/sync-wave 를 부여해 적용 순서를 제어합니다. 숫자가 낮을수록 먼저 적용됩니다.
apiVersion: v1
kind: ConfigMap
metadata:
name: app-config
annotations:
argocd.argoproj.io/sync-wave: "-10"
---
apiVersion: batch/v1
kind: Job
metadata:
name: migrate
annotations:
argocd.argoproj.io/sync-wave: "0"
Job이 의존하는 리소스들을 음수 웨이브로 먼저 깔아두면, hook이든 일반 Job이든 실패율이 크게 내려갑니다.
실전 디버깅 체크리스트
1) Argo CD 이벤트와 비교 에러부터 확인
다음 순서로 보면 원인 분리가 빠릅니다.
- Argo CD UI의 Application
Events argocd-application-controller로그- 실패한 리소스의
kubectl describe이벤트
특히 ComparisonError 는 “렌더링 결과가 불안정하거나, API 버전/필드 차이”일 가능성이 큽니다.
2) 동일한 렌더링 결과가 매번 나오는지 확인
다음 명령으로 두 번 렌더링해서 결과가 달라지면, diff와 Sync가 계속 흔들릴 수 있습니다.
helm template myrel ./chart -n myns -f values.yaml > out1.yaml
helm template myrel ./chart -n myns -f values.yaml > out2.yaml
diff -u out1.yaml out2.yaml
- 랜덤 suffix
now같은 시간 기반 템플릿- 클러스터 조회 기반
lookup
이런 요소가 있으면 GitOps와 상성이 나쁩니다.
3) hook Job이 정말 필요한지 재평가
DB 마이그레이션 같은 작업은 hook으로 많이 처리하지만, 운영에서는 다음 대안이 더 안정적일 때가 많습니다.
- 별도 워크플로(예: Argo Workflows, GitHub Actions)로 마이그레이션 실행 후 앱 배포
- 마이그레이션을 애플리케이션 시작 로직으로 이동하되, 롤링 전략과 락을 명확히 설계
DB 락과 경합이 겹치면 장애로 번질 수 있으니, 마이그레이션이 자주 실패한다면 락/데드락 관점 점검도 권장합니다.
추천 조합: 가장 많이 통하는 해법 템플릿
환경별 차이는 있지만, 현장에서 재현율 높게 통하는 조합은 아래입니다.
- CI의
helm diff에서--no-hooks적용 - hook Job에는
helm.sh/hook-delete-policy를before-hook-creation,hook-succeeded로 설정 - Argo CD에서는
batch/Job의/statusdiff 무시 - 의존 리소스는 Sync Wave로 선행 적용
이렇게 하면 “diff는 깔끔하게, Sync는 순서대로, Job은 잔존하지 않게” 정리됩니다.
마무리
Argo CD Sync 실패를 Helm 차트 문제로만 보면 해결이 늦어집니다. 핵심은 Helm hook이 절차적(Imperative) 성격이고, Argo CD는 선언적(Declarative) 성격이라는 모델 차이입니다. helm diff 와 hook을 같은 레벨에서 다루면 충돌이 생기기 쉬우므로, hook은 diff에서 제외하고, 실행 후 흔적은 삭제하며, Argo CD 비교에서는 변동 필드를 무시하는 쪽으로 설계를 정리하는 것이 가장 안정적입니다.
추가로 OutOfSync가 계속 반복되는 케이스는 원인이 겹칠 수 있으니, 아래 글의 체크리스트도 함께 적용해보세요.