- Published on
Argo CD Sync 실패 - OutOfSync·Degraded 해결법
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서버에 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 DETAILS의 Sync Status / Health StatusTREE에서 빨간 리소스를 클릭 후 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)” 중심으로 튜닝
실전 트러블슈팅 플로우(요약)
argocd app get으로 OutOfSync vs Degraded 분리argocd app diff로 OutOfSync의 “변경 필드”를 특정- Sync 실패 메시지에
immutable/forbidden/no matches for kind/webhook가 있으면 해당 카테고리로 즉시 이동 - Degraded는
kubectl describe pod+ events로 원인 확정 - 해결 후
argocd app sync및kubectl 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 중심으로 맞추면 운영 피로도가 크게 줄어듭니다.