Published on

Argo CD Sync 실패 - OutOfSync·Degraded 해결법

Authors

서버에 Argo CD를 붙여 GitOps로 운영하다 보면, 어느 날 대시보드에 OutOfSync가 떠 있고 건강 상태는 Degraded로 내려가며 Sync failed가 반복되는 순간을 맞습니다. 이때 중요한 건 “무슨 리소스가 왜 달라졌는지(OutOfSync)”와 “클러스터에서 실제로 무엇이 깨졌는지(Degraded)”를 분리해서 보는 것입니다. 전자는 Git ↔ Live 상태 비교, 후자는 Kubernetes 런타임/의존성 문제에 가깝습니다.

이 글은 Argo CD Sync 실패를 원인별로 빠르게 분류하고, 가장 흔한 케이스(필드 드리프트, 웹훅/ALB, ImagePull, RBAC, CRD, Finalizer, Server-side apply 충돌 등)를 명령어 중심으로 해결하는 체크리스트입니다.

OutOfSync vs Degraded: 무엇이 다른가

  • OutOfSync: Git에 선언된 Desired 상태와 클러스터 Live 상태가 다름
    • 예: HPA가 replicas를 바꿨다, MutatingWebhook이 필드를 주입했다, 컨트롤러가 status/annotation을 업데이트했다
  • Health Degraded: 리소스가 생성/업데이트는 되었으나 정상 동작하지 않음
    • 예: Deployment의 Pod가 CrashLoopBackOff, ImagePullBackOff, Readiness 실패, PVC Pending

즉, Sync가 성공해도 Degraded일 수 있고(배포는 됐지만 앱이 깨짐), Degraded가 없어도 OutOfSync일 수 있습니다(자동 변경으로 드리프트만 생김).

1단계: 어떤 리소스가 문제인지 3분 안에 좁히기

Argo CD CLI로 증상 요약

# 애플리케이션 상태 요약
argocd app get myapp

# 어떤 리소스가 OutOfSync/Degraded인지 테이블로 확인
argocd app resources myapp

# Sync 이벤트/오류 메시지 확인
argocd app history myapp
argocd app logs myapp --since 10m

UI에서 꼭 확인할 것

  • APP DETAILSSync Status / Health Status
  • TREE에서 빨간 리소스를 클릭 후 Diff
  • EVENTS 탭의 에러: Forbidden, Invalid, Webhook, timeout, immutable field

2단계: OutOfSync 원인 Top 7과 해결

2.1 컨트롤러/HPA가 바꾼 값(드리프트)로 OutOfSync

대표적으로 Deployment.spec.replicas는 HPA가 계속 바꿉니다. Git에 replicas를 고정하면 Argo CD는 계속 “되돌리기”를 시도하고, HPA는 다시 바꾸며 루프가 됩니다.

해결: replicas를 Git에서 제거하거나, 해당 필드를 ignore 하도록 설정합니다.

Application에 ignoreDifferences 적용 예시

apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: myapp
spec:
  ignoreDifferences:
  - group: apps
    kind: Deployment
    jsonPointers:
    - /spec/replicas

HPA뿐 아니라 Service의 clusterIP, 일부 컨트롤러가 넣는 annotation들도 유사합니다.

2.2 Mutating/Validating Webhook이 필드를 주입/변경

예: service mesh(sidecar), security 정책, ingress controller가 annotation을 추가/수정하면 Diff가 지속됩니다.

진단

kubectl get mutatingwebhookconfigurations
kubectl get validatingwebhookconfigurations

# 특정 리소스에 어떤 필드가 주입됐는지 diff로 확인
argocd app diff myapp

해결 방향

  • 주입되는 필드를 Git에 맞추거나
  • Argo CD에서 해당 필드를 ignoreDifferences로 제외하거나
  • 웹훅 조건(네임스페이스 라벨, annotation)을 조정

2.3 Server-Side Apply/ManagedFields 충돌로 계속 OutOfSync

여러 도구(예: kubectl apply, Helm, Operator, Argo CD)가 같은 필드를 관리하면 managedFields가 꼬이며 드리프트가 반복될 수 있습니다.

진단

kubectl get deploy myapp -n ns -o yaml | yq '.metadata.managedFields[] | {manager, operation, apiVersion}'

해결

  • “한 리소스는 한 도구가 소유”하도록 정리
  • Argo CD Sync 옵션을 명확히(Apply 방식 통일)
  • 필요 시 리소스를 재생성(immutable/충돌 필드가 많을 때)

2.4 immutable field 변경(특히 Service, PVC, Deployment selector)

Argo CD Sync가 실패하며 에러에 field is immutable가 뜨는 케이스입니다.

  • Service의 spec.type 일부 변경, clusterIP 관련
  • PVC의 storageClassName/accessModes 변경
  • Deployment의 spec.selector 변경

해결

  • 리소스를 삭제 후 재생성(데이터 리스크 있는 PVC는 주의)
  • 설계를 바꿔 immutable 변경을 피함(새 리소스 이름으로 생성 후 트래픽 전환)
# 예: Service immutable 변경이 필요하면
kubectl delete svc mysvc -n ns
argocd app sync myapp

PVC는 백업/스냅샷/마이그레이션 전략이 필요합니다.

2.5 CRD/CR 순서 문제(존재하지 않는 Kind)

Argo CD가 CR(Custom Resource)을 먼저 적용하려다 no matches for kind ...로 실패합니다.

해결

  • CRD를 별도 Application으로 분리하고 Sync wave로 선행 적용
metadata:
  annotations:
    argocd.argoproj.io/sync-wave: "0"
---
metadata:
  annotations:
    argocd.argoproj.io/sync-wave: "1"

또는 argocd.argoproj.io/sync-options: SkipDryRunOnMissingResource=true로 임시 완화할 수 있지만, 근본은 순서 정리입니다.

2.6 Finalizer로 삭제가 막혀 Sync가 꼬임

리소스가 Terminating에 걸려 있으면 Argo CD가 원하는 상태로 수렴하지 못합니다.

진단

kubectl get all -n ns | grep Terminating || true
kubectl get <kind> <name> -n ns -o json | jq '.metadata.finalizers'

해결(주의): 컨트롤러가 이미 죽었거나 복구 불가일 때만 finalizer 제거

kubectl patch <kind> <name> -n ns --type=merge -p '{"metadata":{"finalizers":[]}}'

2.7 Argo CD가 의도치 않게 “되돌리는” 리소스(외부 컨트롤러 소유)

예: Ingress/Service annotation을 AWS Load Balancer Controller가 관리하는데 Git이 덮어쓰면, 컨트롤러가 다시 바꾸고 OutOfSync가 반복됩니다. ALB/Ingress 계열 문제가 동반되면 아래 글도 함께 보면 원인 분리가 빨라집니다.

3단계: Health Degraded 원인 Top 6과 해결

Degraded는 “배포는 됐는데 실행이 안 됨”입니다. 따라서 kubectl describe와 이벤트가 핵심입니다.

3.1 ImagePullBackOff / ErrImagePull

가장 흔한 Degraded 원인입니다.

진단

kubectl get pods -n ns
kubectl describe pod <pod> -n ns
kubectl get events -n ns --sort-by=.lastTimestamp | tail -n 30

해결: ECR 토큰, IRSA, imagePullSecrets, 레지스트리 권한 등을 점검합니다.

3.2 CrashLoopBackOff (앱 자체 오류/환경변수/시크릿)

진단

kubectl logs -n ns deploy/myapp --tail=200
kubectl logs -n ns pod/<pod> -c <container> --previous --tail=200
kubectl describe pod/<pod> -n ns

해결 포인트

  • Secret/ConfigMap 키 누락
  • DB/외부 의존성 연결 실패
  • 마이그레이션 잡(Job) 실패
  • 리소스 제한으로 OOMKilled

3.3 Readiness/Liveness probe 실패

프로브 실패는 Degraded를 만들고 롤아웃이 멈춥니다.

진단

kubectl describe pod/<pod> -n ns | sed -n '/Events/,$p'
kubectl get endpoints -n ns mysvc -o wide

해결

  • readiness 경로/포트가 실제와 일치하는지
  • 초기 구동 시간이 길면 initialDelaySeconds/failureThreshold 조정
  • 서비스 디스커버리/네트워크(ingress는 되는데 pod ingress만 실패 등) 이슈 확인

3.4 Pending (PVC/노드 자원/스케줄링)

진단

kubectl get pod -n ns
kubectl describe pod/<pod> -n ns | sed -n '/Events/,$p'
kubectl get pvc -n ns
kubectl describe pvc/<pvc> -n ns

노드 디스크 압박으로 Evicted가 나면 연쇄적으로 Degraded가 됩니다.

3.5 RBAC/권한 문제로 리소스 생성 실패

Argo CD Application Controller가 특정 네임스페이스/리소스에 권한이 없으면 Sync failed와 함께 일부만 적용되고, 결과적으로 Degraded가 됩니다.

진단

# Argo CD가 사용하는 SA 확인(설치 방식에 따라 다름)
kubectl -n argocd get sa

# 특정 리소스에 대한 권한 체크
kubectl auth can-i create deployments -n ns --as system:serviceaccount:argocd:argocd-application-controller
kubectl auth can-i patch services -n ns --as system:serviceaccount:argocd:argocd-application-controller

해결: Role/ClusterRole 및 Binding을 조정하고, “필요 최소 권한”으로 맞춥니다.

3.6 Hook(Job) 실패로 Sync가 실패 처리

Argo CD는 PreSync/Sync/PostSync Hook Job이 실패하면 전체 Sync를 실패로 표시합니다.

진단

kubectl get jobs -n ns
kubectl logs -n ns job/<job-name> --tail=200
argocd app get myapp | sed -n '/Hooks/,$p'

해결

  • Job 재시도/백오프 설정
  • DB 마이그레이션과 앱 롤아웃 순서 정리(sync-wave)
  • 실패해도 무시해야 하면 Hook 정책을 명확히(권장되진 않음)

4단계: “Sync는 실패했는데 실제로는 적용됨” 케이스 정리

가끔은 리소스가 적용되었지만 Argo CD가 실패로 표기합니다.

  • 네트워크 타임아웃/일시적 API 서버 오류
  • Dry-run 단계에서만 실패(예: CRD 미존재)
  • Webhook이 간헐적으로 실패

이때는 Live 상태를 먼저 확인하세요.

argocd app diff myapp
kubectl get deploy,svc,ing -n ns
kubectl rollout status deploy/myapp -n ns

차이가 없고 롤아웃도 완료라면, “Sync 실패”의 원인이 일시적이었는지(이벤트/로그) 확인하고 재시도하면 됩니다.

운영 팁: 재발 방지를 위한 Argo CD 설정 체크

자동 동기화는 ‘안전장치’와 함께

  • automated.prune: Git에서 삭제된 리소스를 제거(주의 필요)
  • selfHeal: 드리프트 자동 복구(외부 컨트롤러와 충돌 가능)

외부 컨트롤러가 관리하는 필드가 많다면 selfHeal을 켜기 전에 ignoreDifferences를 충분히 정의하세요.

Diff 노이즈 줄이기(정확한 경보를 위해)

  • HPA/Service clusterIP/주입 annotation 등은 ignoreDifferences로 정리
  • Argo CD Notifications/Alert는 “진짜 장애(Degraded)” 중심으로 튜닝

실전 트러블슈팅 플로우(요약)

  1. argocd app get으로 OutOfSync vs Degraded 분리
  2. argocd app diff로 OutOfSync의 “변경 필드”를 특정
  3. Sync 실패 메시지에 immutable/forbidden/no matches for kind/webhook가 있으면 해당 카테고리로 즉시 이동
  4. Degraded는 kubectl describe pod + events로 원인 확정
  5. 해결 후 argocd app synckubectl rollout status로 수렴 확인
# 가장 자주 쓰는 마무리 체크
argocd app sync myapp
argocd app wait myapp --health --timeout 300
kubectl rollout status deploy/myapp -n ns

마치며

Argo CD의 OutOfSync는 “선언과 현실의 차이”를 알려주는 신호이고, Degraded는 “현실에서 이미 깨졌다”는 신호입니다. 둘을 섞어 보면 해결 시간이 길어집니다. Diff로 드리프트를 먼저 정리하고(ignoreDifferences/순서/immutable), 그 다음 Pod 이벤트와 로그로 런타임 문제(ImagePull/CrashLoop/Probe/PVC/RBAC)를 잡으면 대부분의 Sync 실패는 짧은 시간 안에 수습됩니다.

다음 단계로는, 자주 발생하는 원인을 팀 표준으로 문서화하고(예: HPA 사용 시 replicas 금지, webhook 주입 필드 목록화), Argo CD 알림을 Degraded 중심으로 맞추면 운영 피로도가 크게 줄어듭니다.