# ArgoCD ignoreDifferences가 동작하지 않는 세 가지 이유

GitOps로 Helm 차트를 배포하다 보면 아무것도 바꾸지 않았는데 Application이 계속 OutOfSync로 남는 상황을 만나게 됩니다. 대개는 차트가 렌더한 매니페스트와 클러스터에 실제로 저장된 객체가 미세하게 다르기 때문이고, ArgoCD는 이런 필드를 무시하도록 spec.ignoreDifferences를 제공합니다.

그런데 ignoreDifferences를 넣었는데도 OutOfSync가 사라지지 않는 경우가 흔합니다. 이 글에서는 영구 diff가 생기는 원인, ignoreDifferences 매처가 조용히 빗나가는 조건, 그리고 이 설정이 어디까지 적용되는지(진단에서 가장 많이 오해하는 부분)를 정리합니다.

# 1. 왜 "바꾼 게 없는데" diff가 생기는가

ArgoCD의 diff는 Git에서 렌더한 desired state와 클러스터의 live state를 비교합니다. 두 값이 논리적으로 같아도 표현이 다르면 diff로 잡힙니다. 대표적인 세 가지 패턴입니다.

패턴 원인
빈 컬렉션 차트가 labels: {}를 렌더하지만 API 서버는 빈 맵을 저장하지 않음 → desired에는 있고 live에는 없음 CRD metadata.labels, metadata.annotations
컨트롤러가 채우는 필드 다른 컨트롤러가 런타임에 값을 주입 webhook clientConfig.caBundle, Secret의 tls.crt
워크로드가 바꾸는 필드 오토스케일러/오퍼레이터가 replicas를 조정 HPA가 관리하는 spec.replicas

첫 번째가 가장 헷갈립니다. 차트에 labels: {} 같은 빈 맵이 들어 있으면 API 서버는 그것을 저장하지 않으므로 live 객체에는 해당 키가 아예 없습니다. 즉 기능에는 아무 영향이 없는데 diff는 영구히 남습니다. 다음처럼 확인할 수 있습니다.

# desired: 차트가 무엇을 렌더하는지
helm template my-release <chart> | yq 'select(.kind == "CustomResourceDefinition") | .metadata.labels'
# → {}

# live: 클러스터에 실제로 저장된 값
kubectl get crd policies.kyverno.io -o json | jq '.metadata.labels'
# → null

# 2. 첫 번째 함정 - group을 생략하면 규칙이 매칭되지 않는다

ignoreDifferences 항목은 group + kind(옵션으로 name, namespace)로 대상 리소스를 고릅니다. 공식 문서는 group을 "버전을 제외한 Kubernetes API 그룹"으로 정의합니다. 여기서 중요한 것은 group을 생략하면 빈 문자열, 즉 core 그룹으로 해석된다는 점입니다.

# ❌ 동작하지 않음 - core 그룹의 CustomResourceDefinition을 찾는다(존재하지 않음)
ignoreDifferences:
  - kind: CustomResourceDefinition
    jqPathExpressions:
      - .metadata.labels

# ✅ CRD의 실제 API 그룹을 명시
ignoreDifferences:
  - group: apiextensions.k8s.io
    kind: CustomResourceDefinition
    jqPathExpressions:
      - .metadata.labels
      - .metadata.annotations

이 실수는 오류가 나지 않아서 특히 잡기 어렵습니다. 매처가 아무것도 고르지 않아도 ArgoCD는 조용히 넘어가고, 화면에는 여전히 OutOfSync만 보입니다. Secret, ConfigMap, Service처럼 core 그룹 리소스는 group을 생략해도 동작하기 때문에, 같은 파일 안에서 어떤 항목은 되고 어떤 항목은 안 되는 상황이 생깁니다.

kubectl api-resources로 그룹을 확인하는 습관이 안전합니다.

kubectl api-resources | grep -i customresourcedefinition
# customresourcedefinitions   crd,crds   apiextensions.k8s.io/v1   false   CustomResourceDefinition

# 3. 두 번째 함정 - 경로 지정 방식 선택

세 가지 방식이 있고 용도가 다릅니다.

방식 표기 적합한 경우
jsonPointers RFC 6902 JSON Pointer (/spec/replicas) 경로가 고정된 단일 필드
jqPathExpressions jq 경로식 (.webhooks[]?.clientConfig.caBundle) 배열 요소, 내용 기반 선택
managedFieldsManagers field manager 이름 특정 컨트롤러가 소유한 필드 전체

배열이 끼면 jsonPointers는 인덱스를 고정해야 하므로 깨지기 쉽습니다. webhook 목록처럼 순서·개수가 변하는 대상은 jqPathExpressions가 맞습니다. ?를 붙여 해당 키가 없는 요소에서 에러가 나지 않게 하는 것도 실무에서 필요합니다.

ignoreDifferences:
  - group: admissionregistration.k8s.io
    kind: ValidatingWebhookConfiguration
    jqPathExpressions:
      - .webhooks[]?.clientConfig.caBundle

세 번째 방식은 관점이 다릅니다. "어떤 경로를 무시할지"가 아니라 "누가 쓴 필드를 무시할지"를 지정합니다. 값이 어디에 들어올지 예측하기 어렵고 소유자는 분명할 때 유용합니다.

ignoreDifferences:
  - group: apps
    kind: Deployment
    managedFieldsManagers:
      - kube-controller-manager

# 4. 세 번째 함정 - ignoreDifferences는 sync를 막지 않는다

가장 많이 오해하는 부분입니다. 공식 문서는 기본 동작을 이렇게 설명합니다.

By default, ignoreDifferences only affects whether an application appears synced or out-of-sync. During actual synchronization, the desired state is applied as-is using a 3-way merge.

즉 기본값에서 ignoreDifferences판정용입니다. Application이 Synced로 보이게 만들 뿐, sync가 실제로 실행될 때는 desired state가 그대로 적용되므로 무시하기로 한 필드도 덮어써집니다. replicas를 무시 목록에 넣어 두고 "오토스케일러 값이 보존된다"고 믿었다가, 다른 이유로 sync가 한 번 돌면서 replicas가 차트 기본값으로 되돌아가는 사고가 여기서 나옵니다.

apply 단계까지 반영하려면 RespectIgnoreDifferences=true가 필요합니다.

spec:
  ignoreDifferences:
    - group: apps
      kind: Deployment
      jsonPointers:
        - /spec/replicas
  syncPolicy:
    syncOptions:
      - RespectIgnoreDifferences=true

이 옵션은 desired state를 적용하기 전에 무시 대상 필드를 live 값으로 미리 패치(pre-patch) 합니다. 단서가 하나 있습니다.

Note that the RespectIgnoreDifferences sync option is only effective when the resource is already created in the cluster. If the Application is being created and no live state exists, the desired state is applied as-is.

최초 생성 시점에는 live 값이 없으므로 보호되지 않습니다. 첫 배포 때는 차트 값이 그대로 들어간다는 뜻입니다.

# 4-1. 두 설정의 역할 구분

정리하면 이렇습니다.

목적 필요한 것
OutOfSync 표시를 없애고 싶다 ignoreDifferences + 정확한 group
sync가 그 필드를 덮어쓰지 않게 하고 싶다 위 + RespectIgnoreDifferences=true

OutOfSync가 안 사라지는 문제에 RespectIgnoreDifferences=true를 먼저 넣어 보는 경우가 많은데, 그건 apply 동작을 바꾸는 옵션이라 표시 문제의 해법이 아닙니다. 표시가 안 고쳐진다면 대개 원인은 2절의 매처 문제입니다. 두 설정은 목적이 다르므로 증상에 맞는 쪽을 고쳐야 합니다.

# 5. selfHeal과의 상호작용

자동 동기화에서 selfHeal: true는 live가 desired에서 벗어났을 때 되돌리는 기능입니다. 무시된 필드는 Synced로 판정되므로 selfHeal을 유발하지 않습니다. 그래서 "무시했으니 안전하다"고 느끼기 쉽지만, 다른 변경으로 sync가 트리거되는 순간 4절의 3-way merge가 그 필드에도 적용됩니다.

syncPolicy:
  automated:
    prune: true
    selfHeal: true
  syncOptions:
    - CreateNamespace=true
    - ServerSideApply=true
    - RespectIgnoreDifferences=true

ServerSideApply=true를 함께 쓰는 경우가 많습니다. 262144바이트 애노테이션 제한을 넘는 대형 CRD를 다룰 때, 그리고 ArgoCD가 전적으로 소유하지 않은 리소스를 패치할 때 필요합니다. 다만 Replace=trueServerSideApply=true보다 우선한다는 점은 기억해야 합니다.

# 6. Application 단위 vs 시스템 단위

같은 무시 규칙을 여러 Application에 반복해서 넣게 되면 argocd-cmresource.customizations로 올리는 편이 낫습니다. 키는 <group>_<kind> 형태입니다.

data:
  resource.customizations.ignoreDifferences.admissionregistration.k8s.io_MutatingWebhookConfiguration: |
    jqPathExpressions:
      - .webhooks[]?.clientConfig.caBundle
  # 모든 리소스에 공통 적용
  resource.customizations.ignoreDifferences.all: |
    managedFieldsManagers:
      - kube-controller-manager

CRD 빈 맵처럼 차트 구현에서 비롯한 일반적인 잡음은 시스템 단위가 적절하고, 특정 워크로드의 replicas처럼 그 앱에만 해당하는 예외는 Application 단위에 두는 것이 원칙입니다. ApplicationSet으로 여러 차트를 배포한다면 템플릿 쪽에 공통 항목을 두어 한 번에 적용할 수 있습니다.

# 7. 진단 순서

OutOfSync가 사라지지 않을 때 아래 순서로 좁히면 대부분 잡힙니다.

# 1) 실제로 어떤 필드가 다른지 확인
argocd app diff <app-name>

# 2) 해당 리소스의 API 그룹 확인 → ignoreDifferences의 group과 일치하는가
kubectl api-resources | grep -i <kind>

# 3) live에 필드가 존재하는지 (빈 맵/누락 판별)
kubectl get <kind> <name> -o json | jq '.metadata.labels'

# 4) 무시 규칙이 적용된 뒤에도 남는 diff인지
argocd app get <app-name> --refresh
증상 원인 후보 조치
ignoreDifferences 추가 후에도 diff 그대로 group 누락/오기 정확한 API 그룹 명시
배열 안 필드가 계속 diff jsonPointers 인덱스 고정 jqPathExpressions로 전환
Synced인데 값이 되돌아감 apply 단계는 무시하지 않음 RespectIgnoreDifferences=true
최초 배포에서만 값이 덮어써짐 live 없음 → 옵션 무효 차트 기본값 자체를 조정
여러 앱에서 같은 잡음 반복 Application 단위 중복 resource.customizations로 이동

# 8. 마무리

  • ignoreDifferences가 안 먹는 첫 번째 원인은 거의 항상 매처입니다. group을 생략하면 core 그룹으로 해석되어 조용히 아무것도 매칭하지 않습니다.
  • 기본 동작은 판정까지입니다. sync가 그 필드를 덮어쓰지 않게 하려면 RespectIgnoreDifferences=true가 필요하고, 이 옵션은 리소스가 이미 존재할 때만 유효합니다.
  • 잡음의 성격을 구분해야 합니다. 차트 구현에서 오는 것은 시스템 단위로, 워크로드 고유 예외는 Application 단위로 두면 관리 비용이 낮습니다.

# 참고