- Published on
Argo CD Sync Failed - OutOfSync 무한루프 해결
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
Argo CD를 운영하다 보면 Sync Failed 와 OutOfSync 상태가 번갈아 나타나면서, 자동 동기화가 계속 재시도되고 이벤트가 폭주하는 상황을 만납니다. 겉으로는 단순히 "동기화가 안 된다"처럼 보이지만, 실제로는 쿠버네티스가 리소스를 계속 변경(변형)하거나, Argo CD가 의도와 다르게 diff를 계산하거나, 동기화 훅/웨이브가 교착되는 등 여러 케이스가 섞여 있습니다.
이 글은 "OutOfSync 무한루프"를 원인별로 분류하고, 각 케이스를 명확한 증거(로그/이벤트/diff) 로 확인한 뒤, 재발 방지까지 연결하는 실전 가이드입니다.
관련해서 OutOfSync 와 Degraded 를 함께 다루는 글도 참고하면 좋습니다: Argo CD Sync 실패 - OutOfSync·Degraded 해결
증상 정의: "무한루프"가 정확히 무엇인가
보통 아래 중 하나로 관찰됩니다.
- Argo CD UI에서
OutOfSync로 바뀜->Syncing->Sync Failed->다시OutOfSync - 자동 동기화(
auto-sync)가 켜져 있고, 일정 주기로 계속sync를 시도 - 애플리케이션 이벤트에 동일한 리소스가 반복적으로
configured로 찍힘 - diff가 매번 동일하거나, 특정 필드만 계속 다르게 표시됨
핵심은 "원격(Git)과 클러스터 상태가 계속 달라진다" 입니다. 그리고 그 차이가 진짜 운영상 중요한 차이인지, 아니면 시스템이 자동으로 바꾼 "무시해도 되는 차이"인지부터 분리해야 합니다.
1단계: 어떤 리소스가 OutOfSync를 유발하는지 확정
먼저 Argo CD가 어떤 리소스를 문제로 보는지 정확히 잡습니다.
argocd app get my-app
argocd app diff my-app
또는 특정 리소스만 보고 싶다면:
argocd app diff my-app --resource apps:Deployment:my-namespace:my-deploy
여기서 중요한 포인트:
- diff가 매번 동일한지, 아니면 매번 값이 바뀌는지
spec이 바뀌는지,metadata나status류가 바뀌는지- 서버 사이드 필드(
managedFields)나 주입 필드가 원인인지
status 차이는 보통 Argo CD가 비교에서 제외하지만, 리소스 종류/버전/설정에 따라 예외가 생길 수 있습니다.
2단계: 가장 흔한 원인 7가지와 해결
원인 1) Mutating Webhook이 리소스를 계속 변형
Istio, Linkerd, Vault Agent Injector, OPA Gatekeeper, Kyverno, 사내 보안 에이전트 등이 MutatingAdmissionWebhook 으로 Deployment 나 PodTemplate 을 바꿉니다.
대표 증상:
spec.template.metadata.annotations에 무언가가 계속 추가/변경- sidecar 컨테이너가 주입되며
containers배열이 달라짐 - 특정 annotation 값이 매번 바뀜(타임스탬프/해시 등)
확인 방법
kubectl get mutatingwebhookconfigurations
kubectl describe mutatingwebhookconfiguration <name>
그리고 실제로 어떤 필드가 바뀌는지:
kubectl -n my-namespace get deploy my-deploy -o yaml > live.yaml
# Git에 있는 매니페스트와 비교 (로컬 diff 도구 사용)
해결 전략
- Argo CD에서 해당 필드 무시(diff ignore)
argocd-cm 에 resource.customizations.ignoreDifferences 를 설정합니다.
apiVersion: v1
kind: ConfigMap
metadata:
name: argocd-cm
namespace: argocd
data:
resource.customizations.ignoreDifferences.apps_Deployment: |
jqPathExpressions:
- .spec.template.metadata.annotations
주의: annotation 전체를 무시하면 중요한 변경도 놓칠 수 있으니, 가능하면 특정 키만 무시하는 방식이 더 안전합니다.
- webhook이 넣는 값이 매번 바뀌지 않도록 설정(가능하면)
예: 주입 annotation에 동적 값이 들어가지 않게 하거나, 주입 자체를 특정 네임스페이스/워크로드에서 제외.
원인 2) kubectl apply 와 서버 사이드 필드/기본값 주입의 반복
쿠버네티스 API 서버는 리소스에 기본값을 채우거나, CRD 컨트롤러가 필드를 보정합니다. 이때 Git 매니페스트가 해당 필드를 명시하지 않으면 Argo CD가 "차이"로 볼 수 있습니다.
대표 예:
spec.replicas기본값,strategy기본값securityContext기본값imagePullPolicy기본값
해결 전략
- Git 매니페스트에 명시적으로 값을 채워 drift를 없애기
- 또는 Argo CD에서 해당 기본값 필드를 ignore
원인 3) HPA/VPA가 replicas 또는 resources를 계속 변경
HPA가 Deployment.spec.replicas 를 바꾸면 Git과 클러스터가 계속 달라집니다. 자동 동기화가 켜져 있으면 Argo CD가 다시 Git 값으로 되돌리고, HPA가 다시 올리고… 루프가 됩니다.
확인 방법
kubectl -n my-namespace get hpa
kubectl -n my-namespace describe hpa my-hpa
해결 전략
Deployment.spec.replicas를 Git에서 제거하고 HPA에 맡기기- 또는 Argo CD에서
replicasdiff를 무시
예시:
data:
resource.customizations.ignoreDifferences.apps_Deployment: |
jqPathExpressions:
- .spec.replicas
원인 4) External Secrets/ConfigMap Reload 류가 annotation을 갱신
reloader 류 도구는 checksum/config 같은 annotation을 바꿔 롤아웃을 유도합니다. 이 값이 Git과 다르면 지속적으로 OutOfSync가 됩니다.
해결 전략
- checksum annotation은 런타임에서 바뀌는 것이 정상이라면 ignore
- 또는 체크섬을 Helm/Kustomize 템플릿에서 동일한 방식으로 생성해 Git에 포함
원인 5) Helm/Kustomize 렌더링 결과가 비결정적(non-deterministic)
템플릿에서 now 류 시간 함수, 랜덤 문자열, 정렬되지 않은 맵 출력 등이 있으면 렌더링 결과가 매번 달라질 수 있습니다.
확인 방법
로컬에서 동일 커밋 기준으로 두 번 렌더링해서 diff가 나는지 봅니다.
helm template mychart ./chart > a.yaml
helm template mychart ./chart > b.yaml
diff -u a.yaml b.yaml
Kustomize도 마찬가지입니다.
kustomize build . > a.yaml
kustomize build . > b.yaml
diff -u a.yaml b.yaml
해결 전략
- 시간/랜덤 기반 템플릿 제거
- 값 정렬/고정
- 생성되는 이름이 매번 바뀌지 않게
nameSuffixnamePrefix를 안정적으로 설계
원인 6) CRD/컨트롤러가 spec을 강제로 재작성
예: Service Mesh, Ingress Controller, Operator가 CR을 받아 내부 정책에 따라 spec 일부를 수정하거나 정규화합니다.
확인 방법
- 해당 CR의 컨트롤러 로그 확인
kubectl get <cr> -o yaml로 live spec이 Git과 어떻게 달라지는지 확인
해결 전략
- Git 매니페스트를 컨트롤러가 기대하는 정규화 형태로 맞추기
- 불가피하면 Argo CD에서 차이 무시
- 또는 해당 리소스만
ApplyOutOfSyncOnly같은 정책을 조정(조직 정책에 맞게)
원인 7) Sync Wave/Hook 설계 문제로 실패 후 즉시 재시도
DB 마이그레이션 Job, CRD 설치, Namespace/Secret 생성 순서가 꼬이면 Sync가 실패하고, auto-sync가 다시 시도하며 루프처럼 보일 수 있습니다.
해결 전략
sync-wave로 순서를 고정- Hook Job이 실패해도 재실행 정책을 명확히
- 의존 리소스 준비를
health checks또는wait로 보강
예: CRD 먼저, 그 다음 CR.
metadata:
annotations:
argocd.argoproj.io/sync-wave: "0"
CR은 다음 웨이브:
metadata:
annotations:
argocd.argoproj.io/sync-wave: "1"
3단계: 실제 운영에서 바로 쓰는 진단 루틴
아래 순서대로 보면 대부분 빠르게 좁혀집니다.
1) diff에서 바뀌는 필드가 무엇인지 한 줄로 요약
replicas인가annotations인가containers배열(사이드카)인가- CR spec 정규화인가
2) 변경 주체를 찾기
kubectl 이벤트와 컨트롤러 로그로 "누가" 바꾸는지 찾습니다.
kubectl -n my-namespace get events --sort-by=.lastTimestamp
또한 해당 리소스를 관리하는 컨트롤러(예: HPA controller, istiod, reloader, operator)의 로그를 확인합니다.
3) Argo CD 쪽 로그도 같이 보기
kubectl -n argocd logs deploy/argocd-application-controller
kubectl -n argocd logs deploy/argocd-repo-server
- repo-server: 렌더링/매니페스트 생성 문제
- application-controller: diff/health/sync 적용 문제
4단계: 무한루프를 "멈추는" 안전한 응급조치
원인 분석 중에도 이벤트 폭주/불필요한 롤아웃을 막아야 합니다.
- 해당 App의 auto-sync를 임시로 끄기
argocd app set my-app --sync-policy none
- 필요하면 현재 상태를 기준으로 고정(신중히)
- Git이 아니라 live가 정답인 상황이라면
argocd app sync로 강제 적용이 아니라, Git을 live에 맞추는 쪽이 안전합니다.
- 특정 리소스만 Sync 제외(임시)
Argo CD의 리소스 제외 정책이나 App 구성에서 관리 범위를 줄여, 문제 리소스만 격리합니다.
재발 방지 체크리스트
- HPA가 있는 Deployment는
replicas를 Git에서 고정하지 않는다(또는 ignore) - 사이드카/에이전트 주입이 있는 클러스터에서는 주입 annotation drift 정책을 정한다
- Helm/Kustomize 렌더링 결과가 결정적인지 CI에서 검증한다
- CRD/Operator 리소스는 컨트롤러가 정규화하는 필드를 문서화하고 Git에 반영한다
- Sync Wave/Hook은 실패 시나리오까지 포함해 설계한다
운영에서 "계속 반복되는 실패"는 대개 시스템이 스스로 회복하려다 더 큰 부하를 만드는 패턴입니다. 이런 루프를 끊는 접근은 쿠버네티스뿐 아니라 다른 시스템에도 유효합니다. 비슷한 관점의 진단 글로는 systemd 서비스 재시작 루프 진단 - 로그·유닛·쉘 도 함께 참고할 만합니다.
부록: ignoreDifferences를 안전하게 쓰는 팁
ignoreDifferences 는 강력하지만, 과하면 drift를 숨깁니다. 다음 원칙을 추천합니다.
- 가능한 "전체 블록" 대신 "특정 필드"만 무시
- 임시로 넣었다면 이슈/티켓과 함께 만료 시점을 만든다
- 보안/네트워크 정책 리소스는 무시 범위를 최소화
예를 들어 annotation 전체를 무시하기보다, 특정 키만 대상으로 하는 전략을 우선 검토하세요. 상황에 따라 jqPathExpressions 를 더 정교하게 작성해 리스크를 줄일 수 있습니다.
이제 argocd app diff 로 "어떤 필드가" 달라지는지 잡고, 위 7가지 원인 중 어디에 해당하는지 분류한 뒤, ignoreDifferences 템플릿 결정성 확보 오토스케일링 설계 정리 중 하나로 마무리하면 OutOfSync 무한루프는 대부분 정리됩니다.