Published on

Argo CD Sync 실패 - OutOfSync·Health Degraded 9가지

Authors

서버 운영에서 Argo CD는 “원하는 상태(Desired)”와 “실제 상태(Live)”를 맞추는 도구입니다. 그런데 어느 날부터 OutOfSync가 계속 남거나, Sync는 끝났는데 Health Degraded가 떠서 배포가 멈춘 경험이 흔합니다. 더 난감한 점은 원인이 한 가지가 아니라는 겁니다.

이 글은 현장에서 반복적으로 만나는 Sync 실패/OutOfSync/Degraded 케이스를 9가지로 분류하고, 각 케이스별로 어떤 신호로 구분하는지, 어떤 커맨드로 확인하는지, 어떻게 고치는지를 빠르게 정리합니다.

참고: 이미지 풀/권한 문제로 Pod가 못 뜨는 케이스는 ImagePullBackOff로도 이어집니다. EKS라면 이 글도 같이 보세요: EKS Pod ImagePullBackOff 401 해결 가이드

먼저 공통 진단 루틴(5분 컷)

문제 유형을 가르기 전에 아래 3가지를 먼저 확보하면 대부분의 케이스가 빨리 풀립니다.

1) Argo CD 앱 상태/이벤트 확인

argocd app get my-app
argocd app diff my-app
argocd app history my-app
argocd app logs my-app

argocd app diff에서 어떤 리소스가 drift(차이) 나는지, argocd app get에서 어떤 리소스가 Degraded인지 먼저 좁힙니다.

2) Kubernetes 이벤트와 실패 지점 확인

kubectl -n my-ns get events --sort-by=.lastTimestamp | tail -n 50
kubectl -n my-ns get pods -o wide
kubectl -n my-ns describe pod my-pod
kubectl -n my-ns logs my-pod --all-containers --tail=200

Sync 자체가 실패한 건지(Apply 실패), Apply는 됐는데 워크로드가 정상화되지 않은 건지(Health Degraded)를 분리합니다.

3) Argo CD 컨트롤러 로그(권한/연결/리포지토리)

kubectl -n argocd get pods
kubectl -n argocd logs deploy/argocd-application-controller --tail=200
kubectl -n argocd logs deploy/argocd-repo-server --tail=200
kubectl -n argocd logs deploy/argocd-server --tail=200

리포지토리 접근/Helm 렌더링/권한 부족은 대개 여기서 단서가 나옵니다.


1) Git에는 변경이 없는데 OutOfSync: 웹훅/폴링/Revision 불일치

증상

  • Git에는 최신인데 Argo CD UI는 예전 revision을 바라보거나 OutOfSync가 남음
  • argocd app get에서 Sync StatusOutOfSync, Target revision이 기대와 다름

진단

argocd app get my-app | sed -n '1,120p'
argocd app diff my-app

또는 repo-server에서 fetch 에러가 있는지 확인합니다.

해결

  • Webhook이 막혀서 갱신이 안 되는 경우: Git provider에서 Webhook delivery 로그 확인
  • 폴링 주기를 짧게: (운영 정책에 따라) Argo CD repo refresh 주기 조정
  • 강제 리프레시:
argocd app get my-app --refresh
argocd app sync my-app
  • 멀티 브랜치/태그를 쓰는 경우 targetRevision이 실제로 존재하는지 확인

2) 리소스 필드가 계속 되돌아가며 OutOfSync: Mutating Webhook/컨트롤러가 덮어씀

증상

  • Sync 직후 잠깐 Synced였다가 다시 OutOfSync
  • diff를 보면 특정 필드가 매번 바뀜(예: labels/annotations, env, securityContext)

진단

argocd app diff my-app

특히 admission(OPA/Gatekeeper, Kyverno), Service Mesh(sidecar injection), 보안 에이전트가 리소스를 변형하는 경우가 많습니다.

해결

  • 변형되는 필드를 Argo CD에서 무시(ignoreDifferences)
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: my-app
spec:
  ignoreDifferences:
    - group: apps
      kind: Deployment
      jsonPointers:
        - /spec/template/metadata/annotations
  • 또는 변형을 유발하는 webhook의 정책을 해당 네임스페이스/리소스에 대해 예외 처리

“왜 특정 필드가 변경됐는지”를 추적할 때는, 결국 누가 권한을 갖고 바꿨는지(Admission/Controller/RBAC)로 귀결됩니다. 권한 추적 관점은 이 글이 사고방식에 도움이 됩니다: AWS IAM AccessDenied 403, 정책 시뮬레이터로 추적


3) Sync 실패(Apply 에러): CRD 미설치 또는 API 버전 불일치

증상

  • Sync 단계에서 바로 실패
  • 메시지 예: no matches for kind ... 또는 the server could not find the requested resource

진단

argocd app sync my-app
# 또는 UI의 Sync 실패 이벤트 메시지 확인

kubectl api-resources | grep -i mycrd
kubectl get crd | grep -i mycrd

해결

  • CRD를 먼저 배포(선행 앱으로 분리하거나 sync wave 사용)
  • Helm chart가 오래되어 apiVersion이 클러스터 버전과 안 맞는 경우 차트 업그레이드
  • Argo CD에서 SkipDryRunOnMissingResource를 임시로 쓰는 방법도 있지만, 근본은 CRD 선행 배포입니다.

예시(동일 앱에서 wave로 CRD 먼저):

apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
  name: widgets.example.com
  annotations:
    argocd.argoproj.io/sync-wave: "-1"

4) Sync 실패: RBAC/권한 부족으로 리소스 생성·패치 불가

증상

  • Sync 실패 메시지에 forbidden 포함
  • 특정 네임스페이스/클러스터 리소스에서만 실패

진단

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

# 실제 에러는 application-controller 로그가 더 정확한 경우가 많음
kubectl -n argocd logs deploy/argocd-application-controller --tail=200 | grep -i forbidden

해결

  • Argo CD가 관리해야 하는 리소스(예: ClusterRole, IngressClass, CRD)가 있다면 적절한 권한 부여
  • 멀티테넌시라면 프로젝트(Project) 레벨의 허용 리소스/대상 네임스페이스 설정도 확인

권한을 무턱대고 넓히기보다, “어떤 리소스에 어떤 verb가 필요한지”를 에러 메시지 기반으로 최소화합니다.


5) OutOfSync가 계속: 서버 사이드 필드/기본값(defaulting) 차이

증상

  • Git에 없는 필드가 Live에 생기며 diff 발생
  • 주로 spec.replicas, strategy, resources, terminationMessagePolicy 같은 기본값

진단

argocd app diff my-app

해결

  • 기본값이 들어가도 상관없다면 ignoreDifferences로 무시
  • 반대로 기본값까지 고정하고 싶다면 매니페스트에 명시적으로 넣어 drift를 제거

예시(명시적으로 기본값을 선언해 drift 제거):

apiVersion: apps/v1
kind: Deployment
metadata:
  name: api
spec:
  replicas: 2
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 25%
      maxUnavailable: 25%

6) Health Degraded: Pod가 안 뜸(ImagePullBackOff, CrashLoopBackOff, Pending)

증상

  • Sync는 성공했는데 앱 Health가 Degraded
  • Pod 상태가 ImagePullBackOff, CrashLoopBackOff, Pending

진단

kubectl -n my-ns get pods
kubectl -n my-ns describe pod my-pod
kubectl -n my-ns logs my-pod --all-containers --tail=200

원인별 힌트:

  • ImagePullBackOff: 레지스트리 인증/이미지 태그 오타/권한
  • CrashLoopBackOff: 앱 설정값/시크릿 누락/마이그레이션 실패
  • Pending: 리소스 부족, 노드 셀렉터/테인트, PVC 바인딩 실패

해결

  • ImagePull 관련은 환경별로 처방이 다릅니다. EKS라면 아래 글이 바로 도움이 됩니다.
  • CrashLoop은 보통 ConfigMap/Secret 누락과 함께 옵니다. envFrom, volumeMounts를 우선 확인하세요.

7) Health Degraded: Ingress/ALB/외부 연동 이슈로 Readiness 실패

증상

  • Pod는 Running인데 서비스가 안 열리고 Degraded
  • Readiness probe가 실패하거나, Ingress 컨트롤러 이벤트에 에러가 찍힘

진단

kubectl -n my-ns describe deploy api
kubectl -n my-ns get ingress
kubectl -n my-ns describe ingress my-ing
kubectl -n my-ns get events --sort-by=.lastTimestamp | tail -n 50

ALB Ingress를 쓰면 타겟 그룹 헬스체크/보안그룹/경로 규칙 때문에 readiness와 비슷한 장애가 자주 납니다.

해결

  • readiness/liveness probe 경로, 포트, 초기 지연 시간 재점검
  • Ingress 컨트롤러(예: AWS Load Balancer Controller) 이벤트에서 “왜 타겟이 unhealthy인지”를 먼저 확인

관련 케이스를 더 깊게 보려면: EKS에서 ALB Ingress 408 Request Timeout 해결 가이드


8) Sync 실패 또는 OutOfSync: Kustomize/Helm 렌더링 실패(값/템플릿/플러그인)

증상

  • UI에서 ComparisonError 또는 Failed to load target state
  • repo-server 로그에 Helm/Kustomize 에러

진단

kubectl -n argocd logs deploy/argocd-repo-server --tail=200

로컬에서도 동일 렌더링을 재현해보면 빠릅니다.

Helm 예:

helm template myrel ./chart -n my-ns -f values.yaml

Kustomize 예:

kustomize build overlays/prod

해결

  • values 파일 누락/오타, required 템플릿 함수로 인한 실패 수정
  • Argo CD에서 허용되지 않는 플러그인/바이너리 의존성이 있다면 repo-server 커스텀 이미지 또는 ConfigManagementPlugin 설정 정비
  • Chart 의존성 다운로드 이슈면 helm dependency build가 필요한 구조인지 확인

9) OutOfSync인데 지워지지 않음: Finalizer/삭제 대기, 리소스가 Terminating에 걸림

증상

  • 리소스 삭제/교체 과정에서 Sync가 계속 꼬임
  • 네임스페이스 또는 특정 CR이 Terminating에서 멈춤

진단

kubectl -n my-ns get all
kubectl -n my-ns get <resource> <name> -o yaml | sed -n '1,200p'

여기서 metadata.finalizers가 남아 있고, 해당 컨트롤러가 이미 죽었거나 외부 시스템 정리가 안 되면 영원히 대기합니다.

해결

  • 컨트롤러가 살아있는지(예: ingress controller, external-dns, cert-manager) 확인 후 정상 경로로 삭제
  • 정말로 막혀있다면 finalizer를 제거(주의: 외부 리소스가 orphan 될 수 있음)
kubectl -n my-ns patch <kind> <name> -p '{"metadata":{"finalizers":[]}}' --type=merge

<kind><name>은 실제 리소스에 맞게 바꿔야 하며, 위 커맨드의 부등호 문자는 반드시 인라인 코드로 다뤄야 합니다.


운영에서 재발을 줄이는 설정 팁

Sync 옵션을 목적에 맞게

  • 자동 동기화가 필요한 앱: drift를 빠르게 줄이되, 변형 리소스는 ignoreDifferences로 통제
  • 삭제까지 Git이 권위가 되게: prune 사용(단, 실수 방지를 위해 보호 장치 필요)

예시:

spec:
  syncPolicy:
    automated:
      prune: true
      selfHeal: true
    syncOptions:
      - CreateNamespace=true

“OutOfSync”와 “Degraded”를 분리해서 온콜 대응

  • OutOfSync: 선언 상태와 실제 상태의 차이(대개 Git/렌더링/변형/권한/순서)
  • Degraded: 리소스는 적용됐지만 워크로드가 정상 동작하지 않음(대개 Pod/네트워크/스토리지/외부 연동)

이 기준으로 먼저 분류하면, 같은 알람이라도 담당자가 바로 올바른 로그로 들어갈 수 있습니다.


마무리 체크리스트(요약)

  • argocd app diff로 drift 리소스를 먼저 특정
  • Apply 실패면 application-controller 로그에서 forbidden, no matches for kind, 렌더링 에러를 찾기
  • Apply 성공인데 Degradedkubectl describe pod와 이벤트로 ImagePullBackOff, CrashLoopBackOff, Pending, readiness 실패를 먼저 분리
  • 반복 drift면 webhook/컨트롤러 변형을 의심하고 ignoreDifferences 또는 정책 예외로 해결
  • 삭제/교체가 걸리면 finalizer와 Terminating 리소스를 점검

위 9가지는 대부분의 Argo CD Sync 장애를 커버합니다. 만약 argocd app diff 결과(민감정보 제거)와 repo-server 또는 application-controller 로그의 핵심 에러 라인을 공유할 수 있다면, 케이스를 더 정확히 좁혀서 재발 방지까지 포함한 처방으로 정리할 수 있습니다.