GitOps with Argo CD›06 · Sync policies, hooks & waves
Learning Hub / Delivery & Infrastructure as Code / GitOps with Argo CD

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.

Practitioner → Advanced
Key wordssync wavesresource hooksPreSyncPostSyncSyncFailhook delete policysync optionsignoreDifferencesretrysync windowscustom 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.retry with limit and exponential backoff handles transient errors (a CRD not yet established).
  • Sync windows (in the AppProject): deny syncs to prod during business hours, or allow only 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 stuck Progressing forever.

Try it: order and safety

  1. Add a ConfigMap in wave -1, a Deployment in wave 0 and a PostSync Job that curls the Service. Sync and watch the order in the UI.
  2. Make the PostSync Job fail (a wrong URL) and see the sync result. Add a SyncFail hook that echoes a message.
  3. Add a PVC with Prune=false, delete it from Git, sync with prune, and confirm the PVC survives (the app shows it as extraneous).
  4. Add an HPA to the app and watch Argo CD fight it; fix it with ignoreDifferences or by removing replicas.
  5. Add a deny sync 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=true on the CRs.
  • Big platform apps benefit from ServerSideApply=true and 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, ServerSideApply add 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.