DEV Community

secopslog for SecOpsLog

Posted on • Originally published at secopslog.com

Argo CD sync policies & self-heal, explained

Auto-sync, prune, self-heal.

A syncPolicy is the thermostat setting for an Argo CD Application. Argo CD continuously compares Git against the live cluster to detect drift; the sync policy decides what it does about it — whether it waits for a human to press sync, or acts on its own. Left at the default, an OutOfSync app just sits there flagged until someone runs argocd app sync. Turn on automation and that same detection becomes continuous delivery: every commit to the tracked revision rolls out by itself, and the cluster is dragged back toward Git whenever it strays. Three switches shape how far that goes.

Automated, prune, and self-heal

spec.syncPolicy.automated is the master switch: with it set, Argo CD syncs automatically after any reconcile where Git and the cluster differ, with no manual step. Inside it, two booleans decide how aggressive that is. prune governs deletions — by default an automated sync only creates and updates, so a manifest you delete from Git leaves its live object orphaned in the cluster until you set prune: true, at which point a removal in Git becomes a deletion in the cluster. selfHeal governs the other direction: with it off, drift you introduce out-of-band — an edited replica count, a hand-patched env var — is reported but left alone; with it on, Argo CD overwrites the live object back to what Git declares on the next pass. automated alone tracks Git forward; add prune and selfHeal and the cluster becomes a strict mirror of the repo.

spec:
  syncPolicy:
    automated:
      prune: true        # delete resources removed from Git
      selfHeal: true     # revert out-of-band drift back to Git
      allowEmpty: false  # refuse to prune everything (the default)
# equivalent via the CLI:
# argocd app set payments-api --sync-policy automated --auto-prune --self-heal
Enter fullscreen mode Exit fullscreen mode

Sync options and retry

Beyond the three switches, syncOptions tune the mechanics of each sync. CreateNamespace=true makes Argo CD create the destination namespace instead of failing when it is absent. ApplyOutOfSyncOnly=true skips resources that already match, so large apps reconcile faster. PruneLast=true defers deletions until everything else is healthy, so a rename removes the old object only after its replacement is up. PrunePropagationPolicy=foreground makes a deletion block on its dependents rather than orphaning them. And retry decides what happens when a sync fails: limit caps the attempts while backoff grows the wait between them, so a transient failure — an image not yet pushed, a webhook briefly down — is retried with increasing delay instead of either giving up or hammering the API. These options apply to manual and automated syncs alike; they describe how a sync runs, not when.

spec:
  syncPolicy:
    syncOptions:
      - CreateNamespace=true       # create the namespace if it is missing
      - ApplyOutOfSyncOnly=true    # skip resources already in sync
      - PruneLast=true             # delete only after the rest is healthy
      - PrunePropagationPolicy=foreground
    retry:
      limit: 5
      backoff: { duration: 5s, factor: 2, maxDuration: 3m }
Enter fullscreen mode Exit fullscreen mode

A posture for each environment

The right policy is not one setting but a spectrum you choose per environment. Dev and preview environments usually run fully automated with prune and selfHeal for hands-off, always-fresh clusters — mistakes there are cheap and self-correcting. Production often runs automated without selfHeal, or manual sync entirely, so a human still confirms the rollout and can pause it mid-incident; the review gate then lives in branch protection on the Git repo rather than at deploy time, which is where GitOps wants it. Keep allowEmpty at its default of false everywhere: it stops an automated sync from pruning every resource when a bad Helm render or an empty path briefly makes Git look like it declares nothing — the difference between a harmless no-op and deleting your entire app. Individual resources can also opt out of prune with a sync-options annotation, which is worth doing for anything stateful.

# prod: automated apply, but no self-heal — a human still owns live drift
$ argocd app set payments-api --sync-policy automated --auto-prune

# revert prod to fully manual (the default when there is no automated block)
$ argocd app set payments-api --sync-policy none

# shield one resource from prune, in its own manifest:
#   metadata:
#     annotations:
#       argocd.argoproj.io/sync-options: Prune=false
Enter fullscreen mode Exit fullscreen mode

⚠️ Self-heal fights any controller that owns the same field: selfHeal rewrites the live object to match Git on every pass, so if another controller legitimately writes a field Argo CD manages, the two fight forever. The classic case is a HorizontalPodAutoscaler adjusting spec.replicas while Argo CD keeps resetting it to the number baked into the manifest — the app flaps between Synced and OutOfSync and pods churn on every reconcile. The fix is not to switch self-heal off but to stop Argo CD from diffing that one field: add a spec.ignoreDifferences entry targeting /spec/replicas so Git owns the manifest and the HPA owns its one field (or, cleaner, drop replicas from the manifest entirely so there is nothing to reconcile). Argo CD ships no default ignore for this — you configure it yourself. Before enabling self-heal cluster-wide, audit what else mutates your resources — admission webhooks, service meshes, cert-manager injecting CA bundles — because each of those is a candidate for the same tug-of-war.


This lesson is part of the free *Argo CD** course at SecOpsLog — hands-on, command-first DevSecOps tutorials. Read the original with the full interactive version →*

Top comments (0)