skip to content

An Argo CD Application is permanently OutOfSync on a Deployment that nobody has edited, and the diff shows a field written by another controller. How do you diagnose that, and what does spec.ignoreDifferences do about it?

level: seniorimportance: should knowfreq 46%

answer

  1. read the diff before changing anything
  2. managedFields names the writer
  3. some fields you should not own at all
  4. ignoring the diff is not yielding the field
  5. a second setting governs the sync itself

basics

~20 s

Inspect the actual diff with argocd app diff to find which field differs and who writes it — usually a mutating admission webhook, a defaulting controller or an autoscaler. Then declare that field under spec.ignoreDifferences so Argo CD stops counting it as drift.

solid answer

~50 s

Start from the diff, not from the status: `argocd app diff <app>` (or the UI's diff view, with **Compare with live manifest** rather than the desired manifest) shows exactly which field differs. Then work out who writes it. The usual culprits are a mutating admission webhook injecting a sidecar or annotations, a Kubernetes controller defaulting a field, or an HPA owning `spec.replicas`. Argo CD is diffing your rendered manifest against a live object that something else has legitimately modified, so it will report `OutOfSync` forever. `spec.ignoreDifferences` tells the diff engine to exclude specific fields on specific resources, selected by `group`/`kind`/`name`/`namespace` plus `jsonPointers`, `jqPathExpressions`, or `managedFieldsManagers` (which ignores whatever a named server-side-apply field manager owns). Crucially, ignoring a difference only affects the *diff* — a sync still applies your value unless you also add the `RespectIgnoreDifferences=true` sync option, which matters a lot when self-heal is on.

code

yaml · 26 lines
yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: checkout
  namespace: argocd
spec:
  project: payments
  source:
    repoURL: https://github.com/acme/manifests.git
    path: apps/checkout/overlays/prod
    targetRevision: v1.8.3
  destination:
    server: https://kubernetes.default.svc
    namespace: checkout
  ignoreDifferences:
    - group: apps
      kind: Deployment
      name: checkout
      jsonPointers:
        - /spec/replicas
  syncPolicy:
    automated:
      prune: true
      selfHeal: true
    syncOptions:
      - RespectIgnoreDifferences=true

go deeper

for a junior

Recognise that a difference between the manifest in Git and the live object makes the app OutOfSync, and that other cluster components — not only people — can create such differences.

for a middle

Be able to read a diff, name the usual writers (mutating webhooks, defaulting, autoscalers), and write an ignoreDifferences entry with jsonPointers or jqPathExpressions.

for a senior

Diagnose from managedFields to the owning component, argue for removing the field from Git over silencing it, and know that RespectIgnoreDifferences is what stops the sync re-applying an ignored field.

for a principal

Decide where these exceptions live — per Application versus cluster-wide resource customizations — so a mesh or policy engine rolled out fleet-wide does not leave every team debugging the same permanent drift.

## Why this happens at all Argo CD's sync status is a diff between the manifests it renders from the source and the objects that are live in the cluster. Kubernetes, however, is full of things that legitimately write to objects after you create them: mutating admission webhooks (service meshes injecting sidecars, policy engines adding annotations), the API server's own defaulting, and controllers such as the HorizontalPodAutoscaler that own a single field. Every one of those produces a live object that differs from what you committed, and Argo CD dutifully reports it. The symptom is characteristic: an app that is `OutOfSync` but perfectly `Healthy`, that goes `Synced` for a moment right after a sync and drifts straight back, and that nobody can point to a human cause for. ## Diagnose before you silence 1. `argocd app diff <app>` prints the field-level difference. In the UI, the same view is available per resource — be sure you are comparing against the **live** manifest, since the desired-manifest view will not show the mutation. 2. Identify the writer. `kubectl get <kind>/<name> -o yaml --show-managed-fields` shows `metadata.managedFields`, which names the field manager that owns each field — `kube-controller-manager`, an autoscaler, a webhook's manager name. This is the single most useful command here, because it turns "something changes it" into a name. 3. Decide whether the field belongs in your manifest at all. If an HPA owns `replicas`, the honest fix is to delete `replicas` from Git rather than to ignore it. Ignoring should be for fields you genuinely cannot own — an injected sidecar, a webhook-added CA bundle, a defaulted value you have no opinion about. ## Declaring the exception ```yaml spec: ignoreDifferences: - group: apps kind: Deployment name: checkout jsonPointers: - /spec/replicas - group: admissionregistration.k8s.io kind: ValidatingWebhookConfiguration jqPathExpressions: - .webhooks[]?.clientConfig.caBundle - group: apps kind: Deployment managedFieldsManagers: - kube-controller-manager ``` - **`jsonPointers`** — RFC 6901 pointers to exact paths. Simple and precise, but awkward for list elements whose index is not stable. - **`jqPathExpressions`** — jq-style expressions, which handle lists and conditionals; the right tool for "every webhook's caBundle". - **`managedFieldsManagers`** — ignore whatever the named server-side-apply field manager owns, without you enumerating the paths. Powerful when a webhook's exact mutations vary, and the closest thing to expressing intent ("that component owns whatever it owns"). The same exceptions can be declared cluster-wide in the `argocd-cm` ConfigMap under `resource.customizations.ignoreDifferences.<group_kind>`, which is how you handle a mutation that affects every app rather than one. ## The trap: ignoring is not yielding By default `ignoreDifferences` affects only the **diff**. The field stops making the app `OutOfSync`, but when a sync does run — for any reason — Argo CD still applies your manifest, including the value you said you were ignoring. With `selfHeal: true` and an HPA in the picture, that means the app stops *reporting* the fight while still *having* it: replicas get stomped on every sync. The fix is the sync option: ```yaml spec: syncPolicy: syncOptions: - RespectIgnoreDifferences=true ``` which makes the sync itself leave the ignored fields alone. Candidates who know only half of this pair usually learned it the hard way. ## Alternatives worth naming - **Remove the field from Git.** Always the first choice when another component is the rightful owner. - **`ServerSideApply=true`** as a sync option makes Argo CD apply with server-side apply and its own field manager, which cooperates with other field owners rather than overwriting the whole object, and also helps with large CRDs that blow the client-side-apply annotation size limit. - **A per-resource `argocd.argoproj.io/compare-options: IgnoreExtraneous`** annotation for objects that a controller generates and that should never count as drift. ## What a strong answer sounds like A weak candidate says "add ignoreDifferences". A strong one says: find who owns the field, prefer to stop owning it yourself, use `managedFieldsManagers` when the writer is a component rather than a path, and remember that ignoring the diff does not stop the sync from writing unless you add `RespectIgnoreDifferences=true`.

  • Why can an Argo CD Application be OutOfSync and Healthy at the same time?
    Because they measure different things. Sync status compares rendered manifests with live objects; health reflects whether the running resources are working. A webhook-injected sidecar makes the object differ from Git while the workload serves traffic perfectly, so the app is legitimately OutOfSync and Healthy at once.
  • When would you use managedFieldsManagers instead of jsonPointers?
    When the writer is a component whose exact mutations you cannot enumerate — a service-mesh injector, a policy engine — and the list of touched paths would be long or version-dependent. Naming the field manager expresses the intent directly: whatever that manager owns, Argo CD does not diff. jsonPointers stays better for one known, stable field.
  • What does enabling the ServerSideApply sync option change about this situation?
    Argo CD applies with server-side apply under its own field manager, so the API server merges rather than replacing the whole object, and fields owned by other managers survive. It reduces ownership fights generally and avoids the client-side-apply last-applied annotation size limit on large CRDs, though it does not by itself stop Argo CD writing a field your manifest still declares.

saying these in an interview costs you the question

  • Adding ignoreDifferences without finding who writes the field
  • Assuming ignoreDifferences also stops the sync from applying the value
  • Keeping spec.replicas in Git alongside an HPA
  • Treating OutOfSync as a health problem
  • Believing any diff means someone edited the cluster by hand

context