Published on

Argo CD Sync Failed/OutOfSync 원인 10가지

Authors

서버를 GitOps로 운영하다 보면 Argo CD의 OutOfSync는 “정상적인 변화 감지”일 수도 있지만, Sync Failed가 동반되거나 같은 리소스가 계속 롤백/재적용되면 대부분 원인이 따로 있습니다. 이 글은 현장에서 가장 많이 나오는 패턴 10가지를 증상 → 원인 → 확인 명령 → 해결 순서로 정리합니다.

> 전제: Argo CD는 기본적으로 Git(Desired)클러스터(Live) 의 차이를 비교해 OutOfSync를 표시하고, Sync 시에는 kubectl apply(서버 사이드/클라이언트 사이드 설정에 따라 다름)와 유사한 방식으로 리소스를 적용합니다. 따라서 실패는 대개 (1) 렌더링 단계, (2) API 서버 적용 단계, (3) 적용 후 컨트롤러가 되돌리는 단계 중 하나에서 발생합니다.

빠른 트리아지(5분 체크)

먼저 “어디서” 실패하는지부터 고정합니다.

# 1) 애플리케이션 이벤트/상태
argocd app get <app> --refresh
argocd app history <app>

# 2) 실패한 리소스/메시지
argocd app sync <app> --dry-run

# 3) 서버에서 실제로 뭐가 다른지
argocd app diff <app> --local ./manifests

# 4) 컨트롤러/웹훅/권한 문제는 컨트롤 플레인 로그로
kubectl -n argocd logs deploy/argocd-application-controller -f --tail=200
kubectl -n argocd logs deploy/argocd-repo-server -f --tail=200

이제부터는 대표 원인 10가지를 케이스별로 봅니다.

1) Admission Webhook(OPA/Gatekeeper/Kyverno 등) 거부

증상

  • Sync 시 Sync Failed.
  • 이벤트에 admission webhook ... denied the request.
  • 로컬에서 kubectl apply도 동일하게 실패.

원인

정책 엔진이 라벨/어노테이션/보안 컨텍스트/이미지 레지스트리 등을 강제하며, Git 매니페스트가 정책을 만족하지 못함.

확인

kubectl get events -A --sort-by=.lastTimestamp | tail -n 50
kubectl describe <kind> <name> -n <ns>

해결

  • 정책이 요구하는 필드(예: runAsNonRoot, resources.limits, imagePullPolicy)를 매니페스트에 반영.
  • 정책 예외가 필요하면 namespace/label 기반 예외 규칙을 정책에 추가.

2) CRD 미설치/버전 불일치로 Custom Resource 적용 실패

증상

  • no matches for kind "X" in version "group/v1".
  • Helm/Kustomize 렌더링은 성공하지만 적용 단계에서 실패.

원인

  • CRD가 클러스터에 없거나(설치 순서 문제)
  • CRD 버전이 달라서(예: v1beta1v1) 리소스 스키마가 맞지 않음.

확인

kubectl get crd | grep -i <crd>
kubectl api-resources | grep -i <kind>

해결

  • CRD를 먼저 설치하도록 App-of-Apps 또는 Sync Wave를 구성.
  • Helm 차트라면 crds/ 디렉터리 처리 방식 확인(차트 업그레이드 시 CRD 자동 갱신이 안 되는 경우 많음).

3) Server-Side Apply(SSA) 필드 충돌 / ManagedFields 충돌

증상

  • Apply failed with conflict 또는 특정 필드에서 충돌.
  • 같은 리소스를 다른 컨트롤러(예: HPA, Operator, Terraform, kubectl)가 함께 만짐.

원인

SSA는 필드 소유권을 managedFields로 추적합니다. Argo CD가 소유하려는 필드를 이미 다른 매니저가 소유하고 있으면 충돌이 납니다.

확인

kubectl get <kind> <name> -n <ns> -o yaml | yq '.metadata.managedFields'

해결

  • 필드 소유권을 정리: 한 도구만 해당 필드를 관리하도록 분리.
  • 필요 시 Argo CD에서 SSA 옵션/Sync 옵션을 조정하거나(환경에 따라 다름), 충돌 필드를 Git에서 제거하고 컨트롤러에게 맡김.

4) 컨트롤러가 자동으로 바꾸는 필드 때문에 OutOfSync가 계속 발생

증상

  • Sync는 성공하지만 곧바로 OutOfSync로 복귀.
  • 차이는 주로 status, annotations, spec.replicas, imagePullSecrets 등.

원인

  • HPA가 spec.replicas를 변경.
  • Ingress Controller가 어노테이션을 추가.
  • Service/Endpoints 관련 컨트롤러가 값을 보정.
  • Operator가 spec 일부를 재작성.

확인

argocd app diff <app>
# 차이가 반복되는 필드를 확인

해결

  • Argo CD ignoreDifferences로 컨트롤러가 관리해야 하는 필드를 무시.
apiVersion: argoproj.io/v1alpha1
kind: Application
spec:
  ignoreDifferences:
  - group: apps
    kind: Deployment
    jsonPointers:
    - /spec/replicas

5) Sync 순서 문제(Sync Wave/Hook)로 의존 리소스가 먼저 적용됨

증상

  • 첫 Sync는 실패, 두 번째 Sync는 성공.
  • 예: Namespace/Secret/ConfigMap이 아직 없어서 Deployment가 실패.

원인

리소스 간 의존성이 있는데 Argo CD가 기본 정렬로 먼저/나중을 맞추지 못함.

확인

  • 실패 이벤트에 not found가 섞여 있는지 확인.
kubectl describe deploy <name> -n <ns>

해결

  • argocd.argoproj.io/sync-wave로 적용 순서를 명시.
metadata:
  annotations:
    argocd.argoproj.io/sync-wave: "-1"  # 예: Secret/Namespace 먼저

6) RBAC/권한 부족으로 특정 리소스만 Sync 실패

증상

  • forbidden: User "system:serviceaccount:argocd:argocd-application-controller" cannot ....
  • 특정 네임스페이스/클러스터 스코프 리소스에서만 실패.

원인

Argo CD의 Application Controller가 사용하는 ServiceAccount에 필요한 ClusterRole/Role 권한이 없음.

확인

kubectl auth can-i create clusterrole --as system:serviceaccount:argocd:argocd-application-controller
kubectl auth can-i patch deployments -n <ns> --as system:serviceaccount:argocd:argocd-application-controller

해결

  • 최소권한 원칙으로 필요한 권한만 추가.
  • 멀티테넌시라면 Project/Namespace 범위를 재설계.

7) 리소스가 삭제/교체되지 못해 Sync가 막힘(finalizer, Terminating)

증상

  • Sync가 deleting 단계에서 멈추거나, 오래된 리소스가 남아 새 리소스를 적용하지 못함.
  • PV/PVC, Namespace, Pod 등이 Terminating에 고착.

원인

finalizer가 남아 컨트롤러가 정리하지 못하거나, 외부 리소스(클라우드 볼륨 등) 삭제가 실패.

확인

kubectl get pv,pvc -A | grep Terminating
kubectl get <resource> <name> -o json | jq '.metadata.finalizers'

해결

8) 이미지 태그/레지스트리 인증 문제로 롤아웃이 실패하고 자동 롤백처럼 보임

증상

  • Argo CD Sync는 성공인데, 곧바로 앱이 불안정.
  • Deployment는 적용됐지만 Pod가 ImagePullBackOff로 죽고, 사용자는 “왜 계속 OutOfSync냐”로 인지.

원인

  • 잘못된 이미지 태그, 프라이빗 레지스트리 인증 실패, IRSA/노드 IAM 권한 문제.

확인

kubectl get pods -n <ns>
kubectl describe pod <pod> -n <ns> | sed -n '/Events:/,$p'

해결

  • imagePullSecrets/ECR 권한/레지스트리 크리덴셜을 점검.
  • Secret이 외부에서 동기화되는 구조라면 ExternalSecret 파이프라인도 함께 확인.

참고: EKS ExternalSecret 미동작 - IRSA·KMS·권한 10분 진단

9) Kustomize/Helm 렌더링 실패(Repo Server 단계)

증상

  • Failed to load target state.
  • kustomize build 오류, Helm 템플릿 오류(값 타입 불일치, 누락된 value, chart dependency 미다운로드 등).

원인

  • values.yaml 타입 오류(문자열/숫자 혼동), 누락된 Chart.yaml dependency, 잘못된 패치 경로.
  • Repo Server 환경에 필요한 바이너리/플러그인이 없음.

확인

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

# 로컬에서도 동일 재현
kustomize build .
helm template . -f values.yaml

해결

  • 로컬 렌더링을 CI에서 강제(merge 전에 실패 차단).
  • Helm dependency는 helm dependency build가 필요한지 확인.

10) 리소스 드리프트의 진짜 원인: 사람이 kubectl로 수정하거나 다른 자동화가 덮어씀

증상

  • 계속 OutOfSync인데, Sync하면 잠깐 맞았다가 다시 틀어짐.
  • 차이가 “누가 봐도 사람이 바꾼 값”(예: env, replicas, annotation)으로 나타남.

원인

  • 운영 중 긴급 조치로 kubectl edit를 했거나,
  • Terraform/Operator/파이프라인이 동일 리소스를 관리.

확인

# 최근 변경 흔적(감사 로그가 있으면 가장 좋음)
argocd app diff <app>

# managedFields의 manager 힌트
kubectl get deploy <name> -n <ns> -o yaml | yq '.metadata.managedFields[].manager' | sort -u

해결

  • “단일 소스 오브 트루스”를 강제: Git으로만 변경.
  • 불가피하게 공존해야 한다면 책임 범위(필드 단위) 분리 + ignoreDifferences 조합.

운영에서 자주 쓰는 패턴: OutOfSync를 ‘정상’과 ‘병적’으로 나누기

  • 정상 OutOfSync: PR 머지 직후, 아직 Sync 전. 혹은 Auto-Sync가 잠깐 따라오는 중.
  • 병적 OutOfSync: 같은 필드가 계속 바뀜(컨트롤러/사람/다른 자동화), 혹은 Sync Failed가 반복.

병적 OutOfSync는 대개 위 10가지 중 하나로 귀결됩니다. 특히 4번(컨트롤러가 바꾸는 필드)과 10번(다중 관리)이 가장 흔합니다.

체크리스트(요약)

  1. 이벤트에 denied/forbidden/not found/no matches for kind가 있는가?
  2. 실패 로그가 Repo Server(렌더링)인지 Controller(적용)인지 분리했는가?
  3. managedFields에 다른 manager가 동일 필드를 소유하고 있는가?
  4. 반복 차이는 ignoreDifferences로 무시해야 하는 필드인가?
  5. 삭제가 막히는 리소스(finalizer/Terminating)가 있는가?

부록: ignoreDifferences 실전 예시 2개

HPA가 replicas를 바꾸는 Deployment

spec:
  ignoreDifferences:
  - group: apps
    kind: Deployment
    name: api
    namespace: prod
    jsonPointers:
    - /spec/replicas

Ingress Controller가 어노테이션을 주입하는 Ingress

spec:
  ignoreDifferences:
  - group: networking.k8s.io
    kind: Ingress
    jqPathExpressions:
    - '.metadata.annotations | with_entries(select(.key|test("nginx.ingress.kubernetes.io/.*") ))'

위 10가지를 순서대로 대입하면, 대부분의 Sync Failed/OutOfSync는 “원인 추적 → 재현 → 고정”이 가능합니다. 원하시면 현재 Argo CD의 Application YAML과 argocd app get 출력(민감정보 제거)을 기준으로, 어떤 케이스에 해당하는지 함께 좁혀드릴게요.