- Published on
Argo CD Sync 실패 - OutOfSync·Degraded 해결
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서버에 배포는 됐는데 Argo CD 화면은 OutOfSync, 상태는 Health: Degraded, 그리고 Sync는 실패. GitOps를 도입한 팀이라면 한 번쯤 겪는 장면입니다. 문제는 이 조합이 원인이 하나가 아니라는 점입니다.
이 글은 “왜 OutOfSync가 되었는지”와 “왜 Degraded가 되었는지”를 분리해서 접근하고, Argo CD 이벤트/디프/리소스 상태를 기반으로 가장 흔한 실패 패턴을 빠르게 분류하는 방법을 다룹니다.
참고로, 인프라 이슈가 동반되는 경우도 많습니다. 예를 들어 클러스터 DNS 장애로 애플리케이션이 준비 상태가 되지 못하면 Degraded로 이어질 수 있는데, 이런 케이스는 EKS CoreDNS CrashLoopBackOff - upstream 타임아웃 해결 같은 글과 함께 보면 원인 추적이 빨라집니다.
OutOfSync vs Degraded: 각각 무엇을 의미하나
OutOfSync
Argo CD가 Git(Desired)과 클러스터(Live)의 매니페스트를 비교했을 때 차이가 존재함을 의미합니다.
대표 원인:
- 수동으로
kubectl edit등으로 리소스를 변경함(드리프트) - Admission Webhook이 필드를 변형함(예: label/annotation 자동 주입)
- HPA/Operator가 리소스를 지속적으로 수정함(예: replicas)
- Argo CD가 추적하는 리소스 집합이 바뀜(Helm/Kustomize 값 변경)
Health Degraded
Argo CD가 리소스의 상태를 평가했을 때 정상적으로 동작하지 않는다고 판단했다는 뜻입니다.
대표 원인:
- Deployment/StatefulSet의 Pod가
CrashLoopBackOff,ImagePullBackOff,Pending - Readiness/Liveness Probe 실패
- Service/Ingress는 있으나 백엔드가 준비되지 않음
- Job이 실패하거나(특히 PreSync/Sync hooks) 완료되지 않음
핵심은:
OutOfSync는 차이(diff) 문제Degraded는 런타임(health) 문제
둘이 동시에 뜨면 “적용 자체가 실패했거나”, “적용은 됐는데 동작이 망가졌거나”, “동작은 되는데 자동 변경 때문에 diff가 계속 생기거나” 중 하나입니다.
10분 진단 루틴: 어디부터 볼 것인가
다음 순서로 보면 대부분 빠르게 좁혀집니다.
- Argo CD Application 이벤트/오퍼레이션 로그 확인
- 어떤 리소스가 OutOfSync인지 diff 확인
- Degraded를 유발하는 리소스의 Kubernetes 이벤트 확인
- 필요하면 Sync 옵션/IgnoreDifferences/Hook 설계를 조정
1) Argo CD에서 실패 지점 확인
CLI가 있으면 가장 빠릅니다.
argocd app get my-app
argocd app sync my-app --prune --retry-limit 1
argocd app history my-app
argocd app logs my-app
app get에서Operation State와Message를 먼저 읽습니다.- Sync 실패 메시지가
permission denied,failed to apply,timeout,hook failed등으로 갈립니다.
UI에서는 Application의 Events 탭과 Sync 상세 로그를 확인하세요.
2) OutOfSync 리소스부터 “차이의 종류”를 분류
argocd app diff my-app
이 diff 결과를 보고 다음 중 어디에 속하는지 판단합니다.
- 의도된 변경인데 아직 적용이 안 됨: Sync 자체 실패 또는 권한/검증 문제
- 의도하지 않은 변경이 계속 생김: Webhook/Controller에 의한 변경(드리프트)
- 특정 필드만 매번 바뀜: IgnoreDifferences 대상일 가능성
3) Degraded 리소스는 Kubernetes 이벤트가 답이다
Degraded가 뜨는 리소스를 찾고, 해당 네임스페이스에서 이벤트를 봅니다.
kubectl -n my-ns get deploy,rs,po
kubectl -n my-ns describe deploy my-deploy
kubectl -n my-ns describe pod my-pod
kubectl -n my-ns get events --sort-by=.lastTimestamp | tail -n 50
FailedScheduling이면 리소스 부족/노드 문제FailedMount면 PVC/CSI 문제Back-off pulling image면 이미지/레지스트리/IAM 문제Readiness probe failed면 앱/네트워크/설정 문제
자주 터지는 Sync 실패 패턴 8가지와 해결
1) RBAC 권한 부족: apply는 하려는데 막힘
증상:
- Sync 로그에
forbidden또는cannot patch가 보임 - 특정 리소스(예:
ClusterRole,IngressClass,CustomResourceDefinition)에서만 실패
해결:
- Argo CD가 사용하는 ServiceAccount/ClusterRole 권한을 점검
- AppProject의
clusterResourceWhitelist/namespaceResourceWhitelist제한 확인
kubectl -n argocd get sa
kubectl -n argocd get clusterrolebinding | grep argocd
kubectl auth can-i patch deployments -n my-ns --as system:serviceaccount:argocd:argocd-application-controller
권한이 부족하면 can-i가 no로 나옵니다.
2) CRD가 아직 없는데 CR을 먼저 적용: 순서 문제
증상:
no matches for kind에러- 예:
kind: Certificate(cert-manager) 같은 CR이 CRD보다 먼저 적용
해결:
- CRD 설치를 별도 Application으로 분리하거나
- Sync Waves로 적용 순서를 강제
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: my-app
spec:
source:
directory:
recurse: true
Sync Wave는 매니페스트에 annotation으로 지정합니다.
metadata:
annotations:
argocd.argoproj.io/sync-wave: "-1"
CRD 쪽을 -1, CR을 0 같은 방식으로 배치합니다.
3) Admission Webhook이 필드를 바꿔 OutOfSync가 지속
증상:
- Sync는 성공인데 계속 OutOfSync
- diff를 보면 특정 label/annotation,
securityContext,imagePullSecrets등이 자동 주입됨
해결:
- 해당 필드를 Git에 반영하거나
- Argo CD의
ignoreDifferences로 무시
apiVersion: argoproj.io/v1alpha1
kind: Application
spec:
ignoreDifferences:
- group: apps
kind: Deployment
jsonPointers:
- /spec/replicas
특히 HPA를 쓰는 경우 replicas는 거의 항상 ignore 대상입니다.
4) HPA/Operator가 replicas를 바꿔 OutOfSync
증상:
- diff에서
spec.replicas만 계속 변경 - 실제로는 HPA가 정상 동작 중
해결:
- 위와 같이
replicas를 ignore - 또는 Argo CD에서
selfHeal을 켜면 HPA와 “줄다리기”가 발생할 수 있으니 정책을 명확히
spec:
syncPolicy:
automated:
prune: true
selfHeal: false
HPA가 있는 서비스는 보통 selfHeal: false를 선호합니다(팀 정책에 따라 다름).
5) Prune 실패: 지우려는데 Finalizer/OwnerReference로 막힘
증상:
- Sync 단계에서 prune가 실패하거나 오래 걸림
- 리소스가
Terminating에서 멈춤
해결:
- 어떤 리소스가 삭제되지 않는지 확인
- finalizer를 제거해야 하는 케이스는 원인 컨트롤러부터 해결
kubectl -n my-ns get all
kubectl -n my-ns get <resource> <name> -o yaml
본문에 <resource> 같은 문자열을 그대로 쓰면 MDX에서 태그로 오인될 수 있으니, 실제 문서에서는 인라인 코드로 표기했습니다.
6) Hook(Job) 실패로 Sync 전체 실패
증상:
PreSync또는Synchook Job이 실패- DB 마이그레이션 Job이 실패하면서 배포가 중단
해결:
- Job 로그 확인
- 실패 시 재시도/타임아웃/rollback 전략 설계
kubectl -n my-ns get job
kubectl -n my-ns logs job/my-migration-job
kubectl -n my-ns describe job my-migration-job
Hook을 쓸 때는 “실패하면 배포를 막는 게 맞는지”를 먼저 합의해야 합니다. 마이그레이션이 포함된 배포는 사실상 SAGA/보상 트랜잭션 관점의 설계가 필요할 때도 있습니다. 이 주제는 DDD에서 분산 트랜잭션 없이 SAGA 구현하기도 참고할 만합니다.
7) Health Degraded의 흔한 원인: 이미지 풀/프로브/리소스
증상:
- Sync는 성공했는데 Degraded
- Pod가 뜨지 않거나 준비 상태가 안 됨
확인 포인트:
ImagePullBackOff: 레지스트리 인증, 이미지 태그, 네트워크CrashLoopBackOff: 앱 설정, 환경변수, 시크릿/컨피그Readiness probe failed: 엔드포인트/포트/응답 시간FailedScheduling: CPU/메모리 부족, taint/toleration
kubectl -n my-ns get pod
kubectl -n my-ns describe pod my-pod
kubectl -n my-ns logs my-pod --previous
--previous는 크래시 재시작 중인 컨테이너의 직전 로그를 볼 때 매우 유용합니다.
8) “적용은 됐는데 diff가 맞지 않음”: Server-side apply/필드 매니저 충돌
증상:
- patch 충돌 메시지 또는 특정 필드가 계속 롤백/재적용
- 여러 컨트롤러가 같은 필드를 관리
해결:
- 누가 필드를 관리해야 하는지 결정(Argo CD vs 다른 오퍼레이터)
- 필요하면 해당 필드를 ignore하거나, 리소스를 분리
kubectl -n my-ns get deploy my-deploy -o yaml | sed -n '1,120p'
managedFields를 보면 어떤 매니저가 어떤 필드를 소유하는지 단서가 됩니다.
재발 방지: 운영에서 유용한 설정 체크리스트
1) Diff를 줄이는 기본 원칙
- HPA 사용 시
replicasignore - Service의
clusterIP같은 자동 할당 필드는 Git에서 관리하지 않기 - Webhook이 주입하는 annotation/label은 Git에 포함하거나 ignore
2) Sync 옵션을 상황에 맞게
- 자동 동기화(Automated sync)를 켠 경우
selfHeal정책을 신중히 prune는 편하지만, 삭제 안전장치가 필요할 수 있음
spec:
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true
- PruneLast=true
PruneLast는 삭제를 마지막에 수행해, 의존 관계로 인한 실패를 줄이는 데 도움이 됩니다.
3) 변경 이력 정리와 릴리즈 단위 관리
OutOfSync를 줄이려면 Git 히스토리도 중요합니다. 예를 들어 rebase 과정에서 커밋이 꼬이면 “무슨 변경이 배포됐는지” 추적이 어려워지고, 결국 롤백/재동기화 판단이 느려집니다. PR 커밋이 중복되거나 폭증하는 문제는 아래 글이 도움이 됩니다.
마무리: OutOfSync는 “diff”, Degraded는 “runtime”으로 쪼개라
Argo CD Sync 실패를 빨리 푸는 핵심은 화면의 빨간 상태를 한 덩어리로 보지 않고,
OutOfSync는 어떤 필드가 왜 달라졌는지(diff)Degraded는 어떤 리소스가 왜 죽었는지(runtime)
로 분리해 증거(이벤트, 로그, diff) 기반으로 좁혀가는 것입니다.
마지막으로, 다음 명령 조합만 익숙해져도 대부분의 케이스는 10분 내로 윤곽이 나옵니다.
argocd app get my-app
argocd app diff my-app
kubectl -n my-ns get events --sort-by=.lastTimestamp | tail -n 50
kubectl -n my-ns describe pod my-pod
kubectl -n my-ns logs my-pod --previous
이 루틴으로도 원인이 안 보이면, 그때는 클러스터 레벨(DNS, CNI, 노드, 스토리지)로 범위를 확장해보는 것이 다음 단계입니다.