- Published on
Argo CD Sync 실패 - OutOfSync·Health Degraded 9가지
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서버 운영에서 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 Status는OutOfSync,Targetrevision이 기대와 다름
진단
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 성공인데
Degraded면kubectl describe pod와 이벤트로ImagePullBackOff,CrashLoopBackOff,Pending, readiness 실패를 먼저 분리 - 반복 drift면 webhook/컨트롤러 변형을 의심하고
ignoreDifferences또는 정책 예외로 해결 - 삭제/교체가 걸리면 finalizer와
Terminating리소스를 점검
위 9가지는 대부분의 Argo CD Sync 장애를 커버합니다. 만약 argocd app diff 결과(민감정보 제거)와 repo-server 또는 application-controller 로그의 핵심 에러 라인을 공유할 수 있다면, 케이스를 더 정확히 좁혀서 재발 방지까지 포함한 처방으로 정리할 수 있습니다.