ArgoCD Automation With Native Tools: Sync Policies, Health Checks, and Safe Reconciliation
Two booleans in an ArgoCD Application spec decide whether the controller is a safety net or a chainsaw: prune and selfHeal. Get them right and drift heals itself while orphaned resources disappear cleanly. Get them wrong and a teammate's emergency kubectl edit vanishes ten seconds later, or a mislabeled resource survives a chart refactor forever. This is the ArgoCD automation article that treats those flags with the caution they deserve.
In part one of this series we walked the failure patterns in ArgoCD-run GitOps: apps parked OutOfSync until the status lost meaning, manual hotfixes silently reverted (or silently kept), Degraded health nobody drills into, sync waves failing halfway, and app-of-apps sprawl. This article is the hands-on tuning pass — everything you can do with ArgoCD's own features and the argocd CLI, no third-party tooling. We'll go feature by feature, and close honestly with the one question none of it can answer.
Everything here uses the official Argo CD documentation syntax. Test against a non-production app first; several of these settings change reconciliation behavior fleet-wide.
Sync policies: automated vs manual, done deliberately
An Application either reconciles on its own or waits for a human. The choice, and the two flags attached to it, is the single highest-leverage decision in your setup.
Manual sync (the conservative default) leaves the app OutOfSync until someone runs a sync. Automated sync reconciles whenever Git and live state diverge:
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: payments-api
namespace: argocd
spec:
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true
Now the two flags, and their blast radius (sync policy docs):
prune: true lets ArgoCD delete resources that no longer exist in Git. Without it, removing a Deployment from your manifests leaves the live Deployment orphaned — quiet waste that accumulates. With it, a bad chart refactor that drops a resource by accident deletes it from the cluster on the next sync. Prune is correct for most apps, but it is the flag most worth pairing with a PruneLast=true sync option and a review discipline on manifest deletions.
selfHeal: true reverts any live change that diverges from Git, continuously. This is what silently undoes a manual kubectl scale during an incident. Turn it on and you are declaring "Git is the only way to change this app" — which is the point of GitOps, but only once your team actually pushes hotfixes through Git. Turn it on too early and you train people to fight the controller.
A sane rollout: start automated with prune: true and selfHeal: false. Watch what drifts. Once manual changes have moved to Git, enable selfHeal.
Sync windows: when reconciliation is allowed
Auto-sync at 2 a.m. on a Saturday is how you turn a merged-but-not-ready PR into a weekend page. Sync windows constrain when automated syncs run, scoped by app, namespace, or label, and configured on the AppProject (sync windows docs):
apiVersion: argoproj.io/v1alpha1
kind: AppProject
metadata:
name: production
namespace: argocd
spec:
syncWindows:
- kind: deny
schedule: '0 0 * * SAT,SUN'
duration: 48h
applications:
- 'payments-*'
manualSync: true
A deny window blocks automated syncs over the weekend while manualSync: true still lets an on-call engineer sync deliberately. Inspect active windows with the CLI:
argocd proj windows list production
Health checks, including custom Lua for CRDs
ArgoCD ships health assessments for built-in kinds — a Deployment is Healthy when its rollout completes, Progressing while it rolls, Degraded when it fails. The gap is your CRDs. A custom resource with no health logic reports Healthy the moment it exists, even if its controller never reconciled it — exactly the false-green that hides real breakage.
You close that gap with a Lua health check in the argocd-cm ConfigMap (custom health docs):
apiVersion: v1
kind: ConfigMap
metadata:
name: argocd-cm
namespace: argocd
data:
resource.customizations.health.example.com_Database: |
hs = {}
if obj.status ~= nil and obj.status.conditions ~= nil then
for i, condition in ipairs(obj.status.conditions) do
if condition.type == "Ready" and condition.status == "True" then
hs.status = "Healthy"
hs.message = condition.message
return hs
end
end
end
hs.status = "Progressing"
hs.message = "Waiting for Database to become Ready"
return hs
Now that CRD only reports Healthy once its own controller sets a Ready condition — the health rollup on the parent app becomes trustworthy.
argocd CLI triage: get, diff, history, rollback
When an app is OutOfSync or Degraded, four commands tell you what happened and let you act (CLI reference).
Start with the summary — sync status, health, and per-resource state:
argocd app get payments-api
Then see the actual drift. app diff compares live cluster state against the target Git revision, field by field:
argocd app diff payments-api
This is the command that answers "what is out of sync." A diff showing a replica count changed from 3 to 6 is a manual scale someone forgot to commit; a diff showing an injected sidecar annotation is a mutating webhook, not drift you should revert.
Review deployment history and roll back if a bad revision shipped:
argocd app history payments-api
# roll back to a specific history ID (disable auto-sync first, or it re-syncs forward)
argocd app rollback payments-api 42
Rollback with automated sync enabled will be immediately undone by the controller — set syncPolicy.automated to null or use argocd app set payments-api --sync-policy none before rolling back.
Resource hooks and sync waves: ordering the rollout
Half-applied syncs usually trace back to ordering. Sync waves run resources in ascending order; a wave doesn't start until the previous wave's resources are Healthy. Hooks run jobs at defined phases — PreSync for migrations, PostSync for smoke tests, SyncFail for cleanup (hooks docs, sync waves docs):
apiVersion: batch/v1
kind: Job
metadata:
name: db-migrate
annotations:
argocd.argoproj.io/hook: PreSync
argocd.argoproj.io/hook-delete-policy: HookSucceeded
argocd.argoproj.io/sync-wave: '-1'
spec:
template:
spec:
containers:
- name: migrate
image: registry.example.com/payments-migrate:latest
restartPolicy: Never
A PreSync migration in wave -1 runs and must succeed before the app's Deployments (wave 0) roll. If the migration Job fails, the sync stops there — that's the "failing halfway" symptom, and it's working as designed. Set hook-delete-policy: HookSucceeded so completed hooks don't linger as false drift.
Notifications: triggers and templates to Slack or webhooks
Toil accumulates when a Degraded app waits for someone to notice. Argo CD Notifications pushes state changes to Slack, webhooks, or email via triggers (when to fire) and templates (what to say), configured in argocd-notifications-cm (notifications docs):
apiVersion: v1
kind: ConfigMap
metadata:
name: argocd-notifications-cm
namespace: argocd
data:
trigger.on-health-degraded: |
- when: app.status.health.status == 'Degraded'
send: [app-degraded]
template.app-degraded: |
message: |
Application {{.app.metadata.name}} is Degraded.
Sync: {{.app.status.sync.status}}
Subscribe an app by annotation:
metadata:
annotations:
notifications.argoproj.io/subscribe.on-health-degraded.slack: platform-alerts
Fire on on-sync-failed and on-health-degraded at minimum. Skip on-sync-running — it's noise.
ignoreDifferences: silencing known-noisy fields
Some fields are mutated by the cluster, not by you: a HPA rewriting replicas, an admission controller injecting fields, cert-manager rotating a secret. Left alone, they keep an app permanently OutOfSync and desensitize everyone to the status. ignoreDifferences tells ArgoCD to stop diffing specific paths (diffing docs):
spec:
ignoreDifferences:
- group: apps
kind: Deployment
jsonPointers:
- /spec/replicas
- group: '*'
kind: '*'
managedFieldsManagers:
- kube-controller-manager
Ignoring /spec/replicas on a HPA-managed Deployment is correct. Ignoring it everywhere would hide the manual scale you do want to catch — scope every rule as narrowly as the noise requires, and no wider.
The honest part: where native automation stops
You should configure all of this. It converts a fleet of ambiguous OutOfSync apps into ones where every remaining diff means something. But native ArgoCD automation has a hard ceiling, and it's worth naming plainly.
Auto-sync reconciles state; it cannot judge intent. When app diff shows a live replica count that doesn't match Git, ArgoCD knows that they differ. It does not know why. It cannot tell you whether that drift was a deliberate hotfix an on-call engineer applied at 3 a.m. to survive a traffic spike — a change worth keeping and committing back to Git — or an accidental kubectl edit on the wrong context that should be reverted immediately. selfHeal: true reverts both. selfHeal: false keeps both. Neither is investigation.
Health is a status, not a diagnosis. A custom Lua check tells you a CRD is Degraded. It doesn't pull the resource's actual Kubernetes events, correlate them with the failing Job's logs, or tell you the migration failed because a column already exists. That drill-down is still a human at a terminal.
Notifications move toil, they don't remove it. A Slack message about a Degraded app is a page, not a resolution. Every alert still lands on an engineer who has to run the argocd app get, read the diff, decide, and act.
So the DIY setup is real and worth the effort — it's the difference between a fleet that pages meaningfully and one that cries wolf. But it stops at the exact moment judgment is required: is this drift a hotfix worth keeping, or an accident worth reverting? Every OutOfSync app in your fleet is still a manual investigation.
What comes next
That investigation — diff Git against live, classify the drift, check the Degraded resource's real Kubernetes state, and propose the safe action — is what part three of this series automates. CloudThinker agents connect read-only to your ArgoCD API server, watch sync and health across the whole fleet, triage each OutOfSync app, and propose keep-and-commit, revert, or escalate under autonomy you control.
Or see your own fleet's drift triaged today: Try CloudThinker free — 100 premium credits, no card required.
