- Published on
Argo CD Sync Failed - drift·Helm 값·RBAC 해결
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서버는 멀쩡한데 Argo CD만 Sync Failed를 띄우는 순간이 있습니다. 이때 중요한 건 “무엇이 실패했는지”를 감으로 때려맞추는 게 아니라, drift(클러스터 실제 상태와 Git 선언 상태의 불일치), Helm values 차이, RBAC 권한 부족 중 어디에 속하는지 빠르게 분류하는 것입니다.
이 글은 운영 환경에서 자주 맞닥뜨리는 패턴을 기준으로, 확인 순서와 해결책을 정리합니다. (특히 kubectl apply로 핫픽스가 들어간 뒤, Helm 차트 업데이트가 겹치거나, Argo CD 서비스 어카운트 권한이 미묘하게 부족한 경우에 효과적입니다.)
1) 먼저: Sync Failed의 실패 지점을 분류하기
Argo CD에서 실패는 크게 두 갈래로 나뉩니다.
- Diff는 나는데 적용이 실패: 대개 RBAC, Admission Webhook, immutable 필드 변경, CRD 누락, 서버사이드 필드 충돌
- Diff 자체가 이상하거나 계속 drift가 남음: Helm values/템플릿 차이,
ignoreDifferences미설정, HPA나 컨트롤러가 계속 덮어씀
확인 명령(필수)
# 앱 상태/이벤트/리소스별 에러를 한 번에 확인
argocd app get myapp
# 어떤 리소스가 OutOfSync/Failed인지 상세 확인
argocd app diff myapp
# 실제 Sync를 걸며 실패 메시지 로그를 더 명확히 보기
argocd app sync myapp --prune --retry-limit 1
UI를 쓴다면 APP DETAILS에서 Sync failed 클릭 후, 실패한 리소스의 메시지를 그대로 복사해두세요. 해결은 대부분 그 메시지 한 줄에서 시작합니다.
2) Drift: 클러스터가 Git과 다르게 계속 변할 때
drift는 “누군가 수동으로 바꿨다” 외에도, 컨트롤러가 자동으로 필드를 채우거나 바꾸는 경우가 많습니다.
- HPA가
spec.replicas를 계속 변경 - Ingress Controller가 annotation을 추가/정규화
- Service가
clusterIP같은 필드를 자동 할당 - MutatingWebhook이
securityContext나 label을 주입
2-1. 가장 흔한 drift: HPA와 Deployment replicas 충돌
Git에는 replicas: 3이 있고, HPA가 replicas를 5로 올리면 Argo CD는 계속 OutOfSync로 보일 수 있습니다.
해결 옵션 A: Git에서 replicas 제거하고 HPA에 맡기기
Deployment의 spec.replicas를 명시하지 않으면 HPA가 주도권을 가져가고 drift가 줄어듭니다.
해결 옵션 B: Argo CD에서 특정 필드 diff 무시
ignoreDifferences로 spec.replicas를 무시합니다.
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: myapp
spec:
ignoreDifferences:
- group: apps
kind: Deployment
jsonPointers:
- /spec/replicas
이 설정은 “HPA가 건드리는 필드”를 GitOps 관리 대상에서 제외하는 방식이라, 운영 안정성이 좋아집니다.
2-2. drift가 계속 남는 경우: Server-Side Apply 필드 충돌
Argo CD가 적용하려는 필드가 이미 다른 필드 매니저에 의해 소유되고 있으면 충돌이 납니다. 특히 kubectl apply나 다른 CD 도구가 섞인 환경에서 발생합니다.
점검
kubectl get deploy myapp -n myns -o yaml | sed -n '1,120p'
# metadata.managedFields가 길게 나오면 필드 매니저 충돌 가능성이 큼
해결 방향
- “한 리소스는 한 도구만” 소유하도록 정리
- 불가피하면 Argo CD 적용 전략을
ServerSideApply로 일관되게 가져가거나, 반대로 기존 적용 방식을 정리
3) Helm values: 값이 달라서 템플릿이 의도와 다르게 렌더링될 때
Argo CD에서 Helm을 쓰면 values.yaml, values-xxx.yaml, --set 오버라이드가 섞이면서 “내가 생각한 값”과 “Argo가 렌더링한 값”이 달라지는 일이 흔합니다.
대표 증상:
- 로컬에서는 문제 없는데 Argo CD에서만
Sync Failed - 특정 환경에서만 ConfigMap, Secret, Ingress가 다르게 생성
required값 누락으로 렌더링 단계에서 실패
3-1. Argo CD가 어떤 값으로 렌더링하는지 확인
Argo CD는 최종 렌더링 결과를 확인할 수 있습니다.
# Argo CD가 생성한 매니페스트를 그대로 확인
argocd app manifests myapp | sed -n '1,120p'
이 결과를 로컬 helm template 결과와 비교하면 차이가 명확해집니다.
3-2. 로컬에서 Argo CD와 동일 조건으로 재현하기
아래처럼 “values 파일 순서”와 “릴리즈 이름”, “네임스페이스”를 맞춰 재현하세요.
helm template myapp ./chart \
--namespace myns \
-f values.yaml \
-f values-prod.yaml \
--set image.tag=20260224-1 \
--debug > rendered.yaml
kubectl apply -n myns --dry-run=server -f rendered.yaml
--dry-run=server에서 실패하면, Argo CD도 거의 동일하게 실패합니다. 이 단계에서 오류가 나면 대개 다음 중 하나입니다.
- 존재하지 않는 API 버전(예: 클러스터가 해당 CRD를 아직 모름)
- immutable 필드 변경(예: Service
clusterIP, PVC spec) - Admission Webhook 거부
3-3. Helm 값의 “빈 문자열”과 “null” 차이로 망가지는 케이스
예를 들어 Ingress annotation을 비우려다 ""로 들어가면 컨트롤러가 거부하거나, 템플릿 조건문이 다르게 평가될 수 있습니다.
# values.yaml
ingress:
enabled: true
annotations:
nginx.ingress.kubernetes.io/proxy-body-size: "" # 빈 문자열
차트 템플릿이 if .Values.ingress.annotations 같은 조건이면 빈 문자열이 포함된 맵이 “존재”로 평가되어 의도치 않게 annotation이 생성됩니다. 이런 류는 null 처리(값 자체 제거)나 템플릿 조건 보강으로 해결합니다.
4) RBAC: Argo CD는 보이는데 적용 권한이 없을 때
Sync Failed에서 가장 빨리 확인해야 하는 축이 RBAC입니다. 특히 다음 메시지가 보이면 거의 확정입니다.
forbidden: User "system:serviceaccount:..." cannot patch resource ...cannot create resource ... in API group ...cannot list resource ...
여기서 중요한 포인트는 “Argo CD UI 로그인 사용자 권한”이 아니라, 클러스터에 적용하는 주체는 Argo CD의 service account라는 점입니다.
4-1. 어떤 서비스 어카운트로 적용하는지 확인
Argo CD는 기본적으로 argocd-application-controller가 적용을 수행합니다. 설정에 따라 App 별로 다른 SA를 쓸 수도 있습니다.
kubectl -n argocd get sa
kubectl -n argocd get deploy argocd-application-controller -o yaml | sed -n '1,120p'
4-2. 권한을 빠르게 재현: kubectl auth can-i
실패한 리소스 기준으로 “그 SA가 할 수 있는지”를 확인합니다.
# 예: myns 네임스페이스에서 deployment patch 가능 여부
kubectl auth can-i patch deployments.apps \
--as system:serviceaccount:argocd:argocd-application-controller \
-n myns
# 예: cluster-scoped 리소스(예: CRD, ClusterRole) 접근 여부
kubectl auth can-i create customresourcedefinitions.apiextensions.k8s.io \
--as system:serviceaccount:argocd:argocd-application-controller
no가 나오면 RBAC 수정이 필요합니다.
4-3. 최소 권한으로 해결하기: Role과 RoleBinding 예시
네임스페이스 범위라면 ClusterRole까지 가지 말고 Role로 해결하는 편이 안전합니다.
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: argocd-deployer
namespace: myns
rules:
- apiGroups: ["apps"]
resources: ["deployments", "replicasets"]
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
- apiGroups: [""]
resources: ["services", "configmaps", "secrets"]
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: argocd-deployer-binding
namespace: myns
subjects:
- kind: ServiceAccount
name: argocd-application-controller
namespace: argocd
roleRef:
kind: Role
name: argocd-deployer
apiGroup: rbac.authorization.k8s.io
클러스터 범위 리소스가 필요하다면 ClusterRole을 쓰되, 리소스 범위를 좁혀서 운영 리스크를 줄이세요.
5) 자주 같이 터지는 원인 3가지와 처방
5-1. immutable 필드 변경으로 Sync 실패
대표적으로 Service clusterIP, PVC spec, Deployment selector 등은 바꿀 수 없습니다.
- 처방: 리소스를 삭제 후 재생성(다운타임 고려), 또는 이름을 바꿔 새 리소스로 롤아웃
# 어떤 필드가 immutable인지 메시지에 보통 명시됨
kubectl -n myns describe svc mysvc | sed -n '1,160p'
5-2. CRD가 없는데 CR을 먼저 적용해서 실패
Argo CD가 CR을 적용하려는데 CRD가 아직 없으면 실패합니다.
- 처방: CRD를 먼저 배포(별도 App), 또는 Sync Wave로 순서 보장
metadata:
annotations:
argocd.argoproj.io/sync-wave: "0"
CR은 wave를 더 크게 설정합니다.
metadata:
annotations:
argocd.argoproj.io/sync-wave: "10"
5-3. 외부 시스템 의존으로 Webhook이 거부
예: 정책 엔진, 보안 스캐너, Gatekeeper, Kyverno 등이 특정 라벨/이미지 정책을 강제하면 Sync Failed로 보입니다.
- 처방: 거부 메시지에서 요구 조건을 충족(라벨 추가, 이미지 레지스트리 허용 등)
- 운영 팁: 정책 변경은 GitOps와 같은 흐름으로 관리해야 drift가 줄어듭니다
6) 운영에서 통하는 “진단 순서” 체크리스트
아래 순서대로 보면 대부분 10분 내에 원인 범주를 확정할 수 있습니다.
argocd app get에서 실패 리소스와 메시지 확보argocd app manifests로 “Argo가 렌더링한 결과” 확인(Helm values 의심)kubectl auth can-i로 SA 권한 확인(RBAC 의심)kubectl apply --dry-run=server로 API 서버가 거부하는지 확인(Webhook, immutable, CRD)- drift면
ignoreDifferences또는 소유권 정리로 재발 방지
7) 재발 방지: GitOps 흐름을 단단하게 만드는 팁
- 핫픽스가 필요하면
kubectl apply후 반드시 Git에 반영하거나, 반대로 Git만 변경하고 Argo CD로만 반영되게 규칙화 - Helm values는 환경별 파일을 명시적으로 분리하고, Argo CD Application에 어떤 파일을 쓰는지 고정
- RBAC은 “일단 cluster-admin”으로 땜질하지 말고,
can-i로 필요한 verb와 resource를 좁혀 최소 권한으로 구성
CI가 꼬여서 엉뚱한 매니페스트가 배포되는 경우도 드물지 않습니다. 캐시나 아티팩트가 잘못 재사용되면 Argo CD는 정상인데 Git 산출물이 틀어질 수 있습니다. 이런 흐름 점검은 GitHub Actions 캐시로 CI 꼬일 때 진단·해결 가이드도 함께 참고하면 좋습니다.
또한 Sync 실패가 곧바로 장애로 이어지진 않더라도, 잘못된 롤아웃이 들어가면 Pod가 반복 재시작하며 지표가 급격히 나빠질 수 있습니다. 애플리케이션 레벨 증상까지 같이 보려면 K8s CrashLoopBackOff 진단 - OOMKilled·Probe를 같이 체크하세요.
EKS 환경에서 Ingress나 ALB 쪽 변경이 섞이면 “배포는 됐는데 트래픽이 깨짐”으로 보이는 경우도 많습니다. 이때는 EKS ALB Ingress 502 Target reset 원인과 해결처럼 네트워크 계층도 함께 확인하는 편이 안전합니다.
마무리
Argo CD Sync Failed는 증상 이름일 뿐, 원인은 보통 drift, Helm values, RBAC 세 축 중 하나로 수렴합니다.
- drift는 “누가 계속 바꾸는가”를 찾고
ignoreDifferences나 소유권 정리로 끝냅니다. - Helm values는 “Argo가 실제로 렌더링한 매니페스트”를 기준으로 비교하면 감이 아니라 증거로 해결됩니다.
- RBAC은
kubectl auth can-i로 재현하고 Role 최소 권한으로 좁혀 해결합니다.
이 세 가지를 분리해서 접근하면, Sync 실패를 장애가 아니라 “정상적인 운영 이벤트”로 다룰 수 있게 됩니다.