Published on

Argo CD Sync 실패 - OutOfSync·Degraded 해결

Authors

서버에 배포는 됐는데 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분 진단 루틴: 어디부터 볼 것인가

다음 순서로 보면 대부분 빠르게 좁혀집니다.

  1. Argo CD Application 이벤트/오퍼레이션 로그 확인
  2. 어떤 리소스가 OutOfSync인지 diff 확인
  3. Degraded를 유발하는 리소스의 Kubernetes 이벤트 확인
  4. 필요하면 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 StateMessage를 먼저 읽습니다.
  • 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-ino로 나옵니다.

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 또는 Sync hook 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 사용 시 replicas ignore
  • 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, 노드, 스토리지)로 범위를 확장해보는 것이 다음 단계입니다.