Published on

Argo CD Sync Failed - OutOfSync 무한루프 해결

Authors

Argo CD를 운영하다 보면 Sync FailedOutOfSync 상태가 번갈아 나타나면서, 자동 동기화가 계속 재시도되고 이벤트가 폭주하는 상황을 만납니다. 겉으로는 단순히 "동기화가 안 된다"처럼 보이지만, 실제로는 쿠버네티스가 리소스를 계속 변경(변형)하거나, Argo CD가 의도와 다르게 diff를 계산하거나, 동기화 훅/웨이브가 교착되는 등 여러 케이스가 섞여 있습니다.

이 글은 "OutOfSync 무한루프"를 원인별로 분류하고, 각 케이스를 명확한 증거(로그/이벤트/diff) 로 확인한 뒤, 재발 방지까지 연결하는 실전 가이드입니다.

관련해서 OutOfSyncDegraded 를 함께 다루는 글도 참고하면 좋습니다: 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 이 바뀌는지, metadatastatus 류가 바뀌는지
  • 서버 사이드 필드(managedFields)나 주입 필드가 원인인지

status 차이는 보통 Argo CD가 비교에서 제외하지만, 리소스 종류/버전/설정에 따라 예외가 생길 수 있습니다.

2단계: 가장 흔한 원인 7가지와 해결

원인 1) Mutating Webhook이 리소스를 계속 변형

Istio, Linkerd, Vault Agent Injector, OPA Gatekeeper, Kyverno, 사내 보안 에이전트 등이 MutatingAdmissionWebhook 으로 DeploymentPodTemplate 을 바꿉니다.

대표 증상:

  • 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 도구 사용)

해결 전략

  1. Argo CD에서 해당 필드 무시(diff ignore)

argocd-cmresource.customizations.ignoreDifferences 를 설정합니다.

apiVersion: v1
kind: ConfigMap
metadata:
  name: argocd-cm
  namespace: argocd
data:
  resource.customizations.ignoreDifferences.apps_Deployment: |
    jqPathExpressions:
      - .spec.template.metadata.annotations

주의: annotation 전체를 무시하면 중요한 변경도 놓칠 수 있으니, 가능하면 특정 키만 무시하는 방식이 더 안전합니다.

  1. 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에서 replicas diff를 무시

예시:

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

해결 전략

  • 시간/랜덤 기반 템플릿 제거
  • 값 정렬/고정
  • 생성되는 이름이 매번 바뀌지 않게 nameSuffix namePrefix 를 안정적으로 설계

원인 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단계: 무한루프를 "멈추는" 안전한 응급조치

원인 분석 중에도 이벤트 폭주/불필요한 롤아웃을 막아야 합니다.

  1. 해당 App의 auto-sync를 임시로 끄기
argocd app set my-app --sync-policy none
  1. 필요하면 현재 상태를 기준으로 고정(신중히)
  • Git이 아니라 live가 정답인 상황이라면 argocd app sync 로 강제 적용이 아니라, Git을 live에 맞추는 쪽이 안전합니다.
  1. 특정 리소스만 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 무한루프는 대부분 정리됩니다.