Lesson 06 of 7 · Modules
Sync policies, hooks & waves
Control how and when Argo CD applies changes: ordering with sync waves, jobs that run before or after a sync with hooks, per-resource sync options for safety, ignoring fields other controllers own, retries, sync windows, and custom health checks.
Why ordering matters
Kubernetes is eventually consistent: applying everything at once usually works. Sometimes it doesn't. A CRD must exist before its custom resources; a database migration must finish before the new app version starts; a smoke test should run after a rollout. Argo CD gives you phases, waves and hooks.
Building a LEGO house: the floor goes first, then the walls, then the roof (waves). Before you start, you check you have all the bricks (a PreSync hook). When you finish, you take a photo to prove it's done (a PostSync hook). If something goes wrong, you call for help (SyncFail).
Phases, waves and hooks
A sync runs in phases: PreSync → Sync → PostSync (and SyncFail if it fails). Inside each phase, resources apply in waves: lowest sync-wave first (default 0), and Argo CD waits until a wave is healthy before starting the next.
apiVersion: batch/v1
kind: Job
metadata:
name: db-migrate
annotations:
argocd.argoproj.io/hook: PreSync
argocd.argoproj.io/hook-delete-policy: BeforeHookCreation
spec:
backoffLimit: 1
template:
spec:
restartPolicy: Never
containers:
- name: migrate
image: registry.example.com/shop-migrations:1.4.2
args: [ "migrate", "up" ]
apiVersion: v1
kind: ConfigMap
metadata:
name: shop-config
annotations:
argocd.argoproj.io/sync-wave: "-1" # before the Deployment in wave 0
data:
FEATURE_X: "on"
| Hook | Runs |
|---|---|
PreSync |
Before applying manifests (migrations, checks) |
Sync |
Alongside the main apply |
PostSync |
After all resources are healthy (smoke tests, notifications) |
SyncFail |
When the sync fails (cleanup, alert) |
PostDelete |
After the Application's resources are deleted (recent versions) |
Hook delete policies: HookSucceeded, HookFailed, BeforeHookCreation (the default when none is set: delete the old hook just before creating the new one, which leaves the last run available for inspection).
Migrations must be backwards compatible with the old version for the time both run. Hooks give ordering, not magic rollbacks.
Sync options
App-level (spec.syncPolicy.syncOptions) or per resource (annotation argocd.argoproj.io/sync-options):
| Option | Use |
|---|---|
CreateNamespace=true |
Create the destination namespace |
ServerSideApply=true |
Big objects (CRDs), and cooperation with other field managers |
PruneLast=true |
Prune removed resources after everything else is healthy |
ApplyOutOfSyncOnly=true |
Only apply changed resources (large apps) |
Prune=false (resource) |
Never prune this object (e.g. a PVC) |
Delete=false (resource) |
Keep it even when the Application is deleted |
Prune=confirm (resource) |
Require manual confirmation to prune it |
Don't fight other controllers
spec:
ignoreDifferences:
- group: apps
kind: Deployment
jsonPointers:
- /spec/replicas # the HPA owns this
syncPolicy:
syncOptions:
- RespectIgnoreDifferences=true
Common cases: HPA replicas, CA bundles injected into webhooks, fields defaulted by mutating webhooks. Omitting replicas from the manifest is often the simplest fix for HPAs.
Retries, windows and health
- Retry:
syncPolicy.retrywithlimitand exponentialbackoffhandles transient errors (a CRD not yet established). - Sync windows (in the AppProject):
denysyncs to prod during business hours, orallowonly in a change window; manual syncs can be permitted separately. - Custom health checks: Argo CD knows built-in kinds; for your CRDs, add Lua health checks in
argocd-cm(resource.customizations.health.<group>_<kind>) so apps aren't stuckProgressingforever.
Try it: order and safety
- Add a ConfigMap in wave
-1, a Deployment in wave0and a PostSync Job thatcurls the Service. Sync and watch the order in the UI. - Make the PostSync Job fail (a wrong URL) and see the sync result. Add a SyncFail hook that echoes a message.
- Add a PVC with
Prune=false, delete it from Git, sync with prune, and confirm the PVC survives (the app shows it as extraneous). - Add an HPA to the app and watch Argo CD fight it; fix it with
ignoreDifferencesor by removingreplicas. - Add a
denysync window to the project for the next hour and try an automated sync.
Going deeper: robust syncs
- Keep hooks idempotent: they can run again on every sync.
- CRDs with custom resources in the same app: put CRDs in an earlier wave, or use
SkipDryRunOnMissingResource=trueon the CRs. - Big platform apps benefit from
ServerSideApply=trueand splitting into several Applications with their own waves (App-of-Apps children can have sync waves too). - For progressive delivery inside an app (canary, blue/green with analysis), add Argo Rollouts; Argo CD syncs the Rollout object and Rollouts manages traffic shifting.
Recap
- Phases PreSync → Sync → PostSync (+ SyncFail); waves order within a phase and wait for health.
- Hooks run Jobs for migrations and tests; choose a delete policy.
- Sync options like
Prune=false,Delete=false,PruneLast,ServerSideApplyadd safety. - ignoreDifferences stops fights with HPAs and webhooks; retries, sync windows and custom health complete the picture.
This site is a public version of my personal engineering knowledge hub. It intentionally excludes confidential company information and internal operational details.