Published on

Argo CD Sync 실패? RBAC·CRD·Drift 진단법

Authors

Argo CD를 운영하다 보면 Sync 버튼을 눌렀는데도 리소스가 적용되지 않거나, OutOfSync가 계속 남거나, 특정 리소스만 반복적으로 실패하는 상황을 자주 만납니다. 이때 감으로 접근하면 원인 규명이 길어집니다.

이 글은 Argo CD Sync 실패를 RBAC(권한), CRD(스키마/리소스 타입), Drift(클러스터와 Git 상태 불일치) 세 범주로 빠르게 분류하고, 각 범주별로 관측 포인트 → 재현/검증 커맨드 → 해결 패턴 순서로 정리합니다.

운영 환경이 EKS라면 권한 문제(특히 AWS 연동)가 함께 얽히는 경우가 많습니다. Pod IAM 권한 이슈는 별도로 아래 글도 참고하면 좋습니다.

1) 먼저: 실패를 3분 안에 분류하는 체크리스트

Sync 실패를 보면 가장 먼저 다음 3가지를 확인합니다.

1-1. Argo CD 이벤트/로그에서 에러 키워드 뽑기

  • permission / forbidden / cannot 이 보이면 RBAC 가능성이 큼
  • no matches for kind / could not find the requested resource 는 CRD/ApiVersion 문제
  • diff / OutOfSync 반복, 혹은 Sync는 성공인데 다시 OutOfSync로 돌아오면 Drift 가능성

CLI를 쓴다면 애플리케이션 이벤트와 상태를 함께 봅니다.

argocd app get myapp
argocd app history myapp
argocd app diff myapp

쿠버네티스 이벤트도 같이 보면 힌트가 더 빨리 나옵니다.

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

1-2. 실패가 "어떤 리소스"에서 나는지 확인

Argo CD UI에서 실패 리소스를 클릭하면 Message에 거의 답이 있습니다. 특히 다음 패턴은 바로 분류됩니다.

  • namespaces "X" is forbidden: User "system:serviceaccount:argocd:argocd-application-controller" cannot ...
    RBAC
  • resource mapping not found for name: ... no matches for kind "Foo" in version "example.com/v1"
    CRD
  • failed to apply: ... field is immutable 또는 cannot patch ...
    Drift 또는 선언 방식 문제(특히 Job, PVC, Service 일부 필드)

1-3. Sync 옵션이 문제를 숨기고 있지 않은지 확인

다음 옵션은 증상을 바꾸거나(=원인을 가리거나) Drift를 악화시킬 수 있습니다.

  • Prune=false 인데 Git에서 리소스를 지웠다면, 클러스터에 남아 Drift처럼 보일 수 있음
  • ApplyOutOfSyncOnly=true 는 일부 리소스 적용이 건너뛰어져 “왜 안 바뀌지?”로 이어질 수 있음
  • ServerSideApply=true 는 필드 소유권 충돌이 Drift로 나타날 수 있음

애플리케이션 spec을 확인합니다.

kubectl -n argocd get app myapp -o yaml | sed -n '1,200p'

2) RBAC 진단: "Argo CD가 적용할 권한이 없다"

RBAC 문제는 보통 2가지 축으로 나뉩니다.

  1. 쿠버네티스 RBAC: Argo CD 컨트롤러 ServiceAccount가 대상 리소스를 get/list/watch/create/patch/delete 할 권한이 없음
  2. Argo CD 내부 RBAC: UI/CLI 사용자가 Sync를 누를 권한이 없음(이 글은 주로 1번)

2-1. 실패 메시지에서 ServiceAccount 주체를 확인

대부분 에러에 주체가 그대로 찍힙니다.

  • system:serviceaccount:argocd:argocd-application-controller
  • 또는 argocd:argocd-server(일부 작업)

2-2. kubectl auth can-i로 권한을 “증명”하기

예를 들어 my-namespaceDeployment를 패치 못한다면 아래처럼 검증합니다.

kubectl auth can-i patch deployments.apps \
  --namespace my-namespace \
  --as system:serviceaccount:argocd:argocd-application-controller

CRD 리소스(예: VirtualService)도 똑같이 확인합니다.

kubectl auth can-i create virtualservices.networking.istio.io \
  -n my-namespace \
  --as system:serviceaccount:argocd:argocd-application-controller

여기서 no가 나오면 Argo CD Sync 실패는 “정상”입니다. 권한부터 해결해야 합니다.

2-3. 해결 패턴 1: 네임스페이스 단위 Role/RoleBinding

가장 안전한 방식은 앱 네임스페이스별로 Role을 주는 것입니다.

apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: argocd-sync
  namespace: my-namespace
rules:
  - apiGroups: ["", "apps", "batch", "networking.k8s.io"]
    resources: ["configmaps", "secrets", "services", "deployments", "statefulsets", "jobs", "ingresses"]
    verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: argocd-sync
  namespace: my-namespace
subjects:
  - kind: ServiceAccount
    name: argocd-application-controller
    namespace: argocd
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: Role
  name: argocd-sync

2-4. 해결 패턴 2: ClusterRole이 필요한 케이스

다음 리소스는 클러스터 범위이므로 ClusterRole이 필요합니다.

  • CustomResourceDefinition
  • ClusterRole, ClusterRoleBinding
  • Namespace
  • MutatingWebhookConfiguration / ValidatingWebhookConfiguration

예를 들어 CRD를 Argo CD가 설치해야 한다면(운영 정책상 허용할 때만) 다음 권한이 필요합니다.

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: argocd-crd-admin
rules:
  - apiGroups: ["apiextensions.k8s.io"]
    resources: ["customresourcedefinitions"]
    verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: argocd-crd-admin
subjects:
  - kind: ServiceAccount
    name: argocd-application-controller
    namespace: argocd
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: argocd-crd-admin

운영 팁: “모든 걸 다 할 수 있는 ClusterRole”로 땜빵하면 당장은 편하지만, 이후 사고나 감사에서 크게 문제가 됩니다. 최소 권한으로 좁히고, 앱 팀별 네임스페이스 경계를 분명히 하는 편이 좋습니다. (조직 경계/책임 분리 관점에서는 DDD의 경계 개념도 참고할 만합니다: DDD 애그리게이트 경계 깨짐 - 해결 7가지)

3) CRD 진단: "리소스 Kind를 모른다" 또는 "API 버전이 없다"

CRD 문제는 대부분 아래 둘 중 하나입니다.

  • CRD 자체가 아직 설치되지 않음
  • CRD는 있는데 apiVersion이 다르거나, 컨트롤 플레인/애드온 버전이 바뀌어 리소스가 더 이상 유효하지 않음

3-1. 대표 에러 패턴

  • no matches for kind "X" in version "group/v1"
  • the server could not find the requested resource

이건 Argo CD가 틀린 게 아니라, 쿠버네티스 API 서버가 그 타입을 모른다는 뜻입니다.

3-2. CRD 존재 여부 확인

예를 들어 VirtualService가 문제라면:

kubectl get crd | grep -i virtualservice
kubectl get crd virtualservices.networking.istio.io -o yaml | sed -n '1,120p'

또는 해당 리소스가 어떤 그룹/버전을 지원하는지 확인합니다.

kubectl api-resources | grep -i virtualservice
kubectl explain virtualservice --api-version=networking.istio.io/v1beta1 | head

3-3. 해결 패턴 1: CRD를 먼저 설치하고 그 다음 CR 적용

GitOps에서 흔한 실수는 “CRD와 CR을 같은 Sync wave로 적용”하는 것입니다. 그러면 CR이 먼저 적용되면서 실패합니다.

Argo CD에서는 sync-wave로 순서를 강제할 수 있습니다.

apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
  name: foos.example.com
  annotations:
    argocd.argoproj.io/sync-wave: "-10"
---
apiVersion: example.com/v1
kind: Foo
metadata:
  name: sample
  annotations:
    argocd.argoproj.io/sync-wave: "0"
spec:
  size: 3

또는 앱을 2개로 쪼갭니다.

  • platform-crds 앱: CRD/클러스터 범위 리소스
  • workloads 앱: 실제 서비스 리소스

이 분리는 RBAC 설계에도 유리합니다(플랫폼 팀만 CRD 권한 보유).

3-4. 해결 패턴 2: Helm 차트/매니페스트의 CRD 처리 방식 점검

Helm 기반이라면 CRD가 crds/ 디렉터리에 들어가 있기도 하고, 템플릿으로 렌더링되기도 합니다. Argo CD가 Helm을 렌더링할 때 CRD 적용 방식이 기대와 다를 수 있으니 다음을 확인합니다.

  • 차트가 CRD를 crds/에 두는지
  • Argo CD 앱 설정에서 Helm 파라미터/버전이 맞는지
  • CRD 업데이트가 “삭제 후 재생성”을 요구하는지(운영 중엔 위험)

실제로 Argo CD가 렌더링한 결과를 보고 싶으면:

argocd app manifest myapp | sed -n '1,200p'

4) Drift 진단: "Sync는 됐는데 또 OutOfSync" / "적용이 계속 되돌아감"

Drift는 Git에 선언한 상태와 클러스터 실제 상태가 달라서 발생합니다. 원인은 크게 4가지가 흔합니다.

  1. 다른 컨트롤러(HPA, Operator, Admission Webhook)가 필드를 계속 변경
  2. 사람이 수동으로 kubectl edit 또는 핫픽스 적용
  3. immutable field 변경처럼 “원래 patch로는 못 바꾸는” 변경
  4. Server-Side Apply 필드 소유권 충돌

4-1. argocd app diff로 Drift의 필드 단위 확인

argocd app diff myapp

여기서 특정 annotation/label/replicas 같은 값이 계속 바뀐다면 “누가 바꾸는지”를 찾아야 합니다.

4-2. kubectl로 마지막 적용 주체 추적

managedFields를 보면 누가 언제 어떤 필드를 소유했는지 단서가 나옵니다.

kubectl -n my-namespace get deploy my-deploy -o yaml | sed -n '1,220p'

manager:kube-controller-manager, argocd-controller, helm, terraform, 특정 operator 이름 등이 보일 수 있습니다.

4-3. 해결 패턴 1: HPA/컨트롤러가 바꾸는 필드는 IgnoreDifferences

예: HPA가 spec.replicas를 바꾸면 Argo CD는 계속 되돌리려 하고, HPA는 다시 올리고… 싸움이 납니다.

Argo CD Application에 ignore 설정을 둡니다.

apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: myapp
  namespace: argocd
spec:
  ignoreDifferences:
    - group: apps
      kind: Deployment
      name: my-deploy
      namespace: my-namespace
      jsonPointers:
        - /spec/replicas

4-4. 해결 패턴 2: 웹훅이 주입하는 필드(예: sidecar, CA bundle) 무시

서비스 메시/보안 제품이 annotation이나 volume을 주입하면 diff가 커집니다. 이때도 ignore로 관리합니다.

spec:
  ignoreDifferences:
    - group: apps
      kind: Deployment
      jqPathExpressions:
        - ".spec.template.metadata.annotations"

주의: 너무 넓게 무시하면 “진짜 변경”도 놓칩니다. 가능한 한 좁게(리소스/필드 단위) 제한하세요.

4-5. 해결 패턴 3: immutable field 변경은 재생성이 필요

대표적으로 다음은 변경이 제한됩니다.

  • Servicespec.clusterIP
  • Job의 일부 템플릿 필드
  • PVC의 일부 스펙

이 경우 Argo CD가 patch하다 실패합니다. 해결은 보통 둘 중 하나입니다.

  • 리소스 이름을 바꾸거나(새 리소스로 생성)
  • Replace 전략을 사용해 삭제 후 재생성(다운타임/데이터 손실 위험 평가 필요)

Argo CD에서는 리소스에 replace 동작을 유도하는 애노테이션을 쓰기도 합니다(조직 표준에 맞게 제한적으로 사용 권장).

metadata:
  annotations:
    argocd.argoproj.io/sync-options: Replace=true

4-6. 해결 패턴 4: Prune/Finalize로 “남아 있는 리소스” 정리

Git에서 삭제했는데 클러스터에 남아 있으면 Drift처럼 보입니다. Prune이 꺼져 있거나, finalizer 때문에 삭제가 막힌 경우가 많습니다.

  • 앱 설정에서 automated.prune 확인
  • 삭제가 안 되는 리소스는 finalizer를 확인
kubectl -n my-namespace get <리소스타입> <이름> -o yaml | sed -n '1,120p'

부득이하게 finalizer를 제거해야 한다면 영향도를 먼저 평가한 뒤 진행하세요.

5) 실전 트러블슈팅 플로우(요약)

운영 중에는 아래 순서로 보면 시간이 가장 덜 듭니다.

  1. argocd app get / UI에서 실패 리소스와 메시지 확인
  2. 메시지가 forbidden이면 kubectl auth can-i로 RBAC 증명
  3. 메시지가 no matches for kindkubectl get crdkubectl api-resources로 CRD/버전 확인
  4. 반복 OutOfSyncargocd app diff로 필드 확인 후 managedFields로 변경 주체 추적
  5. HPA/웹훅/오퍼레이터가 바꾸는 필드는 ignoreDifferences로 최소 범위 무시
  6. immutable 변경은 replace/재생성 전략으로 설계 변경

6) 부록: 자주 쓰는 커맨드 모음

# 앱 상태/이벤트
argocd app get myapp
argocd app diff myapp
argocd app history myapp

# 컨트롤러 로그
kubectl -n argocd logs deploy/argocd-application-controller --tail=200
kubectl -n argocd logs deploy/argocd-repo-server --tail=200

# 권한 확인
kubectl auth can-i patch deployments.apps -n my-namespace \
  --as system:serviceaccount:argocd:argocd-application-controller

# CRD/리소스 확인
kubectl get crd | head
kubectl api-resources | head

# 리소스 변경 주체(ManagedFields) 확인
kubectl -n my-namespace get deploy my-deploy -o yaml | sed -n '1,220p'

Argo CD Sync 실패는 “도구가 불안정해서”라기보다, 대부분 권한/스키마/상태 불일치 중 하나로 귀결됩니다. 위의 분류-검증 루틴을 팀 런북으로 고정해두면, 장애 대응 시 원인 규명 시간을 크게 줄일 수 있습니다.