- Published on
Argo CD Sync 실패 - OutOfSync 무한 반복 해결
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
Argo CD를 운영하다 보면 가장 사람을 지치게 만드는 케이스가 있습니다. Sync를 눌러도 잠깐 Synced였다가 다시 OutOfSync로 돌아가고, 자동 동기화(auto-sync)가 켜져 있으면 계속 재시도하면서 이벤트가 쌓이는 현상입니다. 겉으로는 “동기화 실패”처럼 보이지만, 실제로는 쿠버네티스 클러스터 쪽에서 리소스를 계속 바꾸거나(변이), Argo CD가 의도치 않은 차이를 계속 감지(드리프트) 하는 상황인 경우가 많습니다.
이 글에서는 OutOfSync 무한 반복의 대표 원인과, Argo CD UI/CLI로 빠르게 증상 분류하는 방법, 그리고 **원인별 해결책(무시 규칙, SSA 설정, 차이 제거, 웹훅/컨트롤러 조정)**을 정리합니다.
참고로 EKS 환경에서 이 문제가 더 자주 보이는데, IRSA/OIDC/웹훅 등 주변 요소가 많기 때문입니다. 관련 이슈를 함께 운영한다면 EKS OIDC Thumbprint 변경 후 IRSA 403 복구도 같이 체크해두면 좋습니다.
OutOfSync 무한 반복의 전형적인 징후
다음 중 하나라도 해당하면 “무한 반복” 케이스일 가능성이 큽니다.
- Argo CD에서
Sync를 누르면 성공으로 보이는데, 몇 초~몇 분 뒤 다시OutOfSync Auto-Sync가 켜져 있으면 계속Syncing이벤트가 발생- 특정 리소스(예:
Deployment,MutatingWebhookConfiguration,Ingress,HPA)만 계속OutOfSync Diff화면에서 특정 필드가 계속 바뀌는 패턴(예: annotation, replica, imagePullSecrets, webhook caBundle)
핵심은 “Git의 desired state”와 “클러스터의 live state”가 계속 달라지는 이유를 찾는 것입니다.
1단계: 어떤 리소스가 드리프트를 만드는지 찾기
Argo CD CLI로 문제 리소스 빠르게 특정
아래 명령으로 애플리케이션 상태와 리소스별 sync 상태를 봅니다.
argocd app get my-app
다음으로 diff를 확인합니다.
argocd app diff my-app
특정 리소스만 보고 싶다면 UI에서 App Details의 Resources 목록에서 OutOfSync 항목을 클릭해 Diff를 확인하는 편이 더 빠를 때가 많습니다.
kubectl로 live 리소스의 변경 주체(컨트롤러/웹훅) 추적
드리프트가 발생하는 리소스가 Deployment라면, 실제로 누가 바꾸는지 이벤트/매니저를 확인합니다.
kubectl -n my-ns get deploy my-deploy -o yaml | sed -n '1,120p'
특히 아래를 봅니다.
metadata.managedFields: 어떤 매니저(예:kube-controller-manager,argocd-application-controller,helm,istio,kyverno)가 어떤 필드를 소유하는지metadata.annotations: 외부 컨트롤러가 주기적으로 덮어쓰는 값이 있는지
이 단계에서 **“바뀌는 필드가 무엇인지”**와 **“누가 바꾸는지”**가 보이면 해결이 절반입니다.
원인 A: Mutating/Validating Webhook이 리소스를 계속 변이
증상
Deployment에 annotation이 자동으로 추가/변경PodSpec에securityContext,tolerations,nodeSelector등이 주입Ingress에 annotation이 자동 변경caBundle이 주기적으로 바뀌는WebhookConfiguration
예를 들어 서비스메시(Istio/Linkerd), 보안정책(Kyverno/Gatekeeper), 사이드카 주입, 이미지 정책, admission webhook이 대표적입니다.
해결 방향
- 변이가 “정상”이라면 Argo CD에서 해당 필드를 무시(ignore differences)하도록 설정
- 변이가 “불필요”라면 웹훅/정책을 조정하거나, 특정 네임스페이스/리소스에 예외 처리
Argo CD ignoreDifferences 예시
Application에 다음을 추가합니다.
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: my-app
spec:
ignoreDifferences:
- group: apps
kind: Deployment
namespace: my-ns
name: my-deploy
jsonPointers:
- /spec/template/metadata/annotations
주의할 점:
- annotation 전체를 무시하면 너무 넓습니다. 가능하면 특정 키만 무시하고 싶지만, JSON Pointer만으로는 키 단위가 애매할 수 있습니다.
- 더 정밀하게 하려면
jqPathExpressions를 활용합니다.
spec:
ignoreDifferences:
- group: apps
kind: Deployment
jqPathExpressions:
- .spec.template.metadata.annotations["sidecar.istio.io/status"]
MDX 환경에서 " 같은 이스케이프가 번거롭다면 YAML 자체는 그대로 두되, 문서 본문에서 부등호를 직접 쓰지 않도록만 주의하면 됩니다.
원인 B: HPA/VPA가 replicas를 계속 변경
증상
Deployment의spec.replicas가 Git 값과 다르게 계속 바뀜- Argo CD가 replicas를 다시 원복하려고 하고, HPA가 다시 조정하면서 루프
해결책 1: Argo CD가 replicas를 건드리지 않게 무시
spec:
ignoreDifferences:
- group: apps
kind: Deployment
jsonPointers:
- /spec/replicas
해결책 2: Git에서 replicas를 제거하고 HPA가 유일한 소스로 만들기
Helm/Kustomize를 쓰고 있다면 replicas를 조건부로 생성하지 않거나, 값 파일에서 비워서 템플릿이 렌더링하지 않게 만드는 방식이 안정적입니다.
원인 C: Server-Side Apply(SSA)와 3-way merge의 충돌로 필드 소유권이 꼬임
Argo CD는 기본적으로 3-way merge 방식으로 적용해왔고, 최근에는 SSA를 선택적으로 사용합니다. 클러스터 내 다른 도구(예: kubectl apply --server-side, 오퍼레이터)가 같은 필드를 만지면 managedFields 기반의 필드 소유권이 충돌하면서 다음이 발생할 수 있습니다.
- Argo CD가 적용했는데도 live가 다시 바뀜
- 특정 필드가 계속 “내 것 vs 네 것”처럼 튕김
해결책: SSA 사용 여부를 명확히 하고 일관성 유지
애플리케이션 단위로 SSA를 켤 수 있습니다.
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: my-app
spec:
syncPolicy:
syncOptions:
- ServerSideApply=true
반대로, 이미 SSA로 인해 충돌이 심해졌다면 SSA를 끄고(또는 다른 도구의 SSA를 중단) “한 도구만 apply”하도록 정리하는 게 중요합니다.
추가로 아래 옵션이 도움이 되는 경우가 있습니다.
spec:
syncPolicy:
syncOptions:
- ApplyOutOfSyncOnly=true
- RespectIgnoreDifferences=true
ApplyOutOfSyncOnly=true: 불필요한 재적용을 줄여 루프를 완화RespectIgnoreDifferences=true: 무시 규칙이 sync 과정에도 적용되도록
원인 D: 컨트롤러가 상태 필드를 계속 갱신(특히 status, observedGeneration)
원칙적으로 Argo CD는 status를 desired state로 보지 않습니다. 하지만 다음과 같은 경우 차이가 계속 보일 수 있습니다.
- CRD/CR(커스텀 리소스)에서 status 외에도 spec 일부를 컨트롤러가 보정
- 오퍼레이터가 spec을 “정규화(normalize)”하여 필드 순서/기본값을 주기적으로 바꿈
해결책
- 해당 CRD/CR에 대해
ignoreDifferences로 컨트롤러가 보정하는 필드를 무시 - 가능하면 Git에서 컨트롤러가 강제하는 기본값과 동일하게 맞춰 diff 자체를 없애기
예시(가상의 CR):
spec:
ignoreDifferences:
- group: example.com
kind: ExampleResource
jqPathExpressions:
- .spec.template.defaults
원인 E: caBundle/인증서 자동 갱신으로 WebhookConfiguration이 계속 변경
MutatingWebhookConfiguration 또는 ValidatingWebhookConfiguration의 webhooks.clientConfig.caBundle은 cert-manager나 설치 스크립트가 자동으로 갱신하는 경우가 많습니다. Git에 caBundle이 포함되어 있으면 거의 항상 드리프트가 납니다.
해결책
- Git에서
caBundle을 제거(가능하면)하고 클러스터에서 동적으로 관리 - 또는 해당 필드를 ignore 처리
spec:
ignoreDifferences:
- group: admissionregistration.k8s.io
kind: MutatingWebhookConfiguration
jqPathExpressions:
- .webhooks[].clientConfig.caBundle
원인 F: Kustomize/Helm 렌더 결과가 매번 달라지는 비결정성
다음은 “클러스터가 바꾸는 게 아니라, Git에서 생성되는 매니페스트가 매번 달라져서” OutOfSync가 나는 케이스입니다.
- Helm chart가
now,randAlphaNum같은 함수를 사용 - Kustomize generator가 매번 다른 해시/이름을 만들어냄(설정에 따라)
- 이미지 태그를
latest로 두고 이미지 업데이트 자동화 도구가 섞임
해결책
- 렌더링 결과가 결정적(deterministic)이 되도록 템플릿 수정
- Kustomize generator 옵션을 조정
- 이미지 태그를 고정하고, 이미지 업데이트는 별도 파이프라인으로 관리
Helm에서 랜덤/시간 의존 값이 있으면 제거하거나 값 파일로 고정합니다.
# values.yaml
rolloutId: "2026-02-24"
그리고 템플릿은 해당 값을 그대로 사용하도록 합니다.
2단계: 안전한 복구 절차(운영 중 사고 방지)
무한 루프를 멈추고 원인을 정리하려면 다음 순서가 안전합니다.
1) Auto-Sync 일시 중지
UI에서 자동 동기화를 끄거나, CLI로 패치를 합니다.
argocd app set my-app --sync-policy none
2) Diff에서 “계속 변하는 필드”를 한 줄로 요약
운영자 입장에서는 원인이 무엇이든, 결국 “어떤 필드가 바뀌는지”가 결론입니다. 예:
Deployment.spec.replicasDeployment.spec.template.metadata.annotations["sidecar.istio.io/status"]MutatingWebhookConfiguration.webhooks[].clientConfig.caBundle
3) 원인별 처방 적용
- HPA면 replicas ignore
- 웹훅/오퍼레이터면 변이 필드 ignore 또는 정책 예외
- SSA 충돌이면 apply 주체를 단일화하고 SSA 옵션 정리
4) 강제 동기화는 마지막에
--force는 리소스를 재생성하거나 필드를 강제로 덮어써 장애를 만들 수 있어, 원인 파악 없이 습관적으로 쓰면 위험합니다.
argocd app sync my-app
정말 필요할 때만 다음을 고려합니다.
argocd app sync my-app --force
실전 예시: HPA 때문에 Deployment가 OutOfSync로 돌아가는 경우
상황
- Git에는
replicas: 2 - HPA가 부하에 따라
replicas를 3, 4로 바꿈 - Argo CD가 다시 2로 되돌림
- HPA가 다시 올림
해결
Deployment의 spec.replicas를 무시합니다.
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: my-app
spec:
ignoreDifferences:
- group: apps
kind: Deployment
jsonPointers:
- /spec/replicas
syncPolicy:
syncOptions:
- RespectIgnoreDifferences=true
이후 argocd app diff my-app에서 replicas 관련 diff가 사라지는지 확인합니다.
체크리스트: OutOfSync 루프를 끝내는 질문 7개
- OutOfSync가 발생하는 리소스가 항상 동일한가?
- Diff에서 바뀌는 필드는 무엇인가? (replicas, annotations, caBundle, image, env 등)
managedFields에서 그 필드를 소유/수정하는 매니저는 누구인가?- 웹훅(Admission)이 해당 리소스에 변이를 가하는가?
- HPA/VPA 같은 자동 스케일러가 spec을 바꾸는가?
- Helm/Kustomize 렌더 결과가 결정적인가?
- apply 주체가 여러 개인가? (Argo CD, kubectl, 오퍼레이터, GitHub Actions 등)
이 질문에 답이 나오면, 해결책은 대개 ignoreDifferences 또는 “변이 주체 단일화”로 귀결됩니다.
마무리
Argo CD의 OutOfSync 무한 반복은 단순히 “Sync 버튼이 안 먹는다” 문제가 아니라, GitOps 모델에서 desired state와 live state의 소유권이 충돌하고 있다는 신호입니다. 먼저 argocd app diff로 변하는 필드를 특정하고, managedFields로 변경 주체를 찾은 뒤, 필요하면 ignoreDifferences로 의도된 변이를 허용하거나 apply 주체를 하나로 정리하세요.
EKS에서 함께 자주 얽히는 인증/권한 이슈(예: IRSA 403)까지 겹치면 원인 파악이 더 어려워질 수 있으니, 권한 문제는 별도로 분리해 진단하는 것도 추천합니다. 관련해서는 EKS OIDC Thumbprint 변경 후 IRSA 403 복구를 참고해 환경 변수를 줄여두면 Sync 루프 분석이 훨씬 쉬워집니다.