Argo CD sync waves en hooks: migraties veilig uitrollen

Coding.nl
9 min leestijd
Inhoud van dit artikel
  1. Phases en waves: hoe Argo CD de volgorde bepaalt
  2. De hooks die je nodig hebt
  3. Laravel: migrate als PreSync Job
  4. ExternalSecret en ConfigMap
  5. web-Deployment (php-fpm/nginx of Octane)
  6. Horizon en scheduler
  7. Expand en contract: de echte veiligheidsregel
  8. Magento: setup:upgrade als hook
  9. Helm-hooks in Argo CD
  10. Als de sync mislukt
  11. Wanneer is dit nodig?

Draai je database-migratie in Argo CD als Kubernetes Job met de annotatie argocd.argoproj.io/hook: PreSync. Argo CD voert die Job uit vóór de nieuwe Deployments, en stopt de sync als de migratie faalt: de oude versie blijft dan gewoon draaien. Met sync waves bepaal je daarna de volgorde binnen de sync, zoals eerst secrets, dan web, dan workers. Dit werkt voor Laravel en Magento, met per platform een paar eigen valkuilen.

Phases en waves: hoe Argo CD de volgorde bepaalt

Argo CD past resources niet allemaal tegelijk toe. Elke sync loopt door phases, en binnen een phase door waves. De volgorde is volgens de documentatie:

  1. Phase: PreSync, Sync, PostSync.
  2. Wave: laagste nummer eerst. Default is wave 0, negatieve waves mogen.
  3. Kind: namespaces eerst, dan andere Kubernetes-resources, dan custom resources.
  4. Naam: alfabetisch.

Argo CD gaat pas naar de volgende wave als alle resources in de huidige wave healthy zijn. Voor een Deployment betekent healthy: de rollout is klaar. Voor een Job: succesvol afgerond. Tussen waves wacht Argo CD standaard twee seconden, instelbaar met de omgevingsvariabele ARGOCD_SYNC_WAVE_DELAY.

Een wave zet je met één annotatie:

yaml
metadata:
  annotations:
    argocd.argoproj.io/sync-wave: "-1"

Bij het opruimen (prune) draait de volgorde om: hoogste wave eerst.

De hooks die je nodig hebt

Hooks zijn gewone Kubernetes-resources, meestal Jobs, met een hook-annotatie. De phases die je voor deploys gebruikt:

HookWanneer
PreSyncVoordat de manifests worden toegepast
SyncTegelijk met de manifests, na de PreSync hooks
PostSyncAls alles synced en healthy is
SyncFailAls de sync mislukt

Daarnaast bestaan PreDelete en PostDelete voor het verwijderen van een hele Application, en Skip om een resource over te slaan. Voor migraties zijn die niet relevant.

Met argocd.argoproj.io/hook-delete-policy bepaal je wanneer Argo CD de hook-resource opruimt: HookSucceeded, HookFailed of BeforeHookCreation. Zet je niets, dan geldt BeforeHookCreation: de vorige Job wordt verwijderd vlak voordat de nieuwe wordt aangemaakt. Dat is meestal wat je wilt, want dan kun je de logs van de laatste migratie nog teruglezen.

Laravel: migrate als PreSync Job

yaml
apiVersion: batch/v1
kind: Job
metadata:
  name: shop-migrate
  annotations:
    argocd.argoproj.io/hook: PreSync
    argocd.argoproj.io/hook-delete-policy: BeforeHookCreation
spec:
  backoffLimit: 0
  activeDeadlineSeconds: 900
  template:
    spec:
      restartPolicy: Never
      containers:
        - name: migrate
          image: registry.example.nl/shop:1.42.0
          command: ["php", "artisan", "migrate", "--force", "--isolated"]
          envFrom:
            - configMapRef:
                name: shop-env
            - secretRef:
                name: shop-secrets

De details die ertoe doen:

  • Hetzelfde image-tag als de Deployments. Zet het tag op één plek, in je Helm values of Kustomize images:, zodat migratie en code nooit uit elkaar lopen.
  • --force omdat Laravel in productie anders om bevestiging vraagt en de Job blijft hangen.
  • --isolated pakt een atomic lock via de cache-driver. Een tweede migrate tegelijk wordt dan een no-op met exit code 0. Dat beschermt je als iemand handmatig een sync start terwijl er al een loopt. Het vraagt een gedeelde cache, zoals Redis.
  • backoffLimit: 0 zodat een mislukte migratie niet automatisch drie keer opnieuw draait. Een halve migratie die opnieuw start, maakt de schade vaak groter.
  • activeDeadlineSeconds als bovengrens. Een migratie die op een tabel-lock wacht, wil je niet eindeloos laten hangen.

Faalt de Job, dan markeert Argo CD de sync als mislukt en past het de nieuwe Deployments niet toe. Je webpods en Horizon draaien door op de vorige release. Dat is precies het vangnet dat je wilt.

De volgorde in de Sync-phase

Na de migratie wil je niet alles tegelijk uitrollen. Een voorbeeld:

yaml
# ExternalSecret en ConfigMap
argocd.argoproj.io/sync-wave: "-1"

# web-Deployment (php-fpm/nginx of Octane)
argocd.argoproj.io/sync-wave: "0"

# Horizon en scheduler
argocd.argoproj.io/sync-wave: "1"

Web gaat eerst. Pas als de nieuwe webpods healthy zijn, rollen de workers uit. Faalt de webrollout op een readiness probe, dan starten de nieuwe workers niet en dispatchen ze ook geen jobs met nieuwe code.

Een PostSync hook gebruik je voor alles wat pas zinvol is als de nieuwe versie draait: een smoke test, het opwarmen van caches, of een melding in Slack.

yaml
apiVersion: batch/v1
kind: Job
metadata:
  name: shop-smoke-test
  annotations:
    argocd.argoproj.io/hook: PostSync
    argocd.argoproj.io/hook-delete-policy: BeforeHookCreation
spec:
  backoffLimit: 0
  template:
    spec:
      restartPolicy: Never
      containers:
        - name: smoke
          image: curlimages/curl:8.10.1
          args: ["--fail", "--max-time", "10", "http://shop-web/up"]

Laravel heeft standaard een /up-healthroute. Die zegt niet alles, maar vangt een kapotte boot of ontbrekende env-variabele direct af.

Valkuil: de PreSync Job mist zijn Secret

Hier lopen veel teams tegenaan bij de eerste installatie. De PreSync-phase draait vóór de Sync-phase. Staan je ConfigMap en Secret in de Sync-phase, ook in wave -1, dan bestaan ze nog niet als de migratie-Job start. De Job blijft hangen op CreateContainerConfigError.

Bij volgende deploys merk je het niet, want dan bestaan ze al. Tot je een nieuwe env-variabele toevoegt die de migratie nodig heeft. Dan draait de migratie met de oude ConfigMap.

Twee oplossingen:

  1. Maak de config zelf ook PreSync, met een lagere wave dan de migratie. Bij een ExternalSecret moet de bijbehorende Secret dan wel gesynchroniseerd zijn voordat de Job start. Zie External Secrets Operator op Kubernetes.
  2. Maak de migratie geen hook maar een gewone resource in de Sync-phase, in wave 0, met secrets in wave -1 en Deployments in wave 1. Een Job is immutable, dus je hebt per release een unieke naam nodig (bijvoorbeeld met het versienummer erin), of de sync-option Replace=true.

Optie 2 is robuuster als de config vaak wijzigt. Optie 1 past als de config stabiel is en je de hook-semantiek wilt houden.

Expand en contract: de echte veiligheidsregel

Een PreSync migratie betekent: de nieuwe database draait even onder de oude code. Bij een rolling update draaien oude en nieuwe pods daarna nog een tijdje naast elkaar. Je schema moet dus met beide versies werken.

De vuistregel heet expand and contract:

  • Release N (expand): voeg kolommen en tabellen toe, nullable of met default. Schrijf naar oud en nieuw.
  • Release N+1: code leest alleen nog uit de nieuwe structuur.
  • Release N+2 (contract): verwijder de oude kolom.

Een renameColumn in één release is de klassieke fout. De oude pods zoeken de oude naam, en vallen om tot de rollout klaar is. Hetzelfde geldt voor een kolom die NOT NULL wordt zonder default: oude code die nog inserts doet zonder die kolom, krijgt errors.

Argo CD kan je deploy terugdraaien naar een vorige Git-revisie. Je database draait het niet terug. Een rollback in Argo CD is dus alleen veilig als je migratie backwards compatible was. Dat is de belangrijkste reden om expand and contract te volgen, ook als je de rest van dit artikel overslaat.

Magento: setup:upgrade als hook

Bij Magento is het principe hetzelfde, maar het platform is gevoeliger. Een Magento-image bouw je compleet: setup:di:compile en setup:static-content:deploy gebeuren in de CI-build, niet bij het opstarten en niet in een hook. Hoe dat image eruitziet, staat op Magento 2 op Kubernetes.

De hook doet alleen het database-deel. Magento heeft daarvoor een handig commando: setup:db:status. Volgens de Adobe-documentatie betekent exit code 0 dat alles in orde is, en exit code 2 dat setup:upgrade nodig is.

yaml
apiVersion: batch/v1
kind: Job
metadata:
  name: magento-upgrade
  annotations:
    argocd.argoproj.io/hook: PreSync
    argocd.argoproj.io/hook-delete-policy: BeforeHookCreation
spec:
  backoffLimit: 0
  activeDeadlineSeconds: 1800
  template:
    spec:
      restartPolicy: Never
      containers:
        - name: upgrade
          image: registry.example.nl/magento:2026.10.3
          command: ["/bin/sh", "-c"]
          args:
            - |
              set -u
              php bin/magento setup:db:status
              status=$?
              if [ "$status" -eq 0 ]; then
                echo "Geen schemawijzigingen, alleen config importeren"
                php bin/magento app:config:import --no-interaction
              elif [ "$status" -eq 2 ]; then
                echo "Schemawijzigingen gevonden, setup:upgrade"
                php bin/magento setup:upgrade --keep-generated --no-interaction
              else
                echo "Onverwachte status $status"
                exit "$status"
              fi
          envFrom:
            - secretRef:
                name: magento-env

--keep-generated is hier essentieel. Zonder die optie gooit setup:upgrade de gegenereerde code en static files weg die je net in je image hebt gebouwd. In een container is dat zinloos werk, en met een read-only bestandssysteem faalt het zelfs.

Maintenance mode werkt anders in een cluster

Op een enkele server zet bin/magento maintenance:enable een flag-bestand in var/. In Kubernetes heeft elke pod zijn eigen bestandssysteem. Een maintenance-flag in de migratie-Job ziet geen enkele webpod.

Je opties:

  • Maintenance aan de rand regelen. Een maintenance-pagina in Varnish of op je Gateway API-route, aan te zetten via een ConfigMap of een aparte route. Dat werkt voor alle pods tegelijk.
  • Deploys zonder schemawijziging zonder maintenance. Met de setup:db:status-check hierboven weet je per release of er überhaupt iets aan de database verandert. Vaak is dat niet zo.
  • De blue-green-instelling van Magento. Sinds 2.4.4 kent Magento de optie deployment/blue_green/enabled in env.php. Die is bedoeld om oude code te laten doorwerken terwijl de database al is bijgewerkt. Test het gedrag in jouw versie en met jouw extensies, want het hangt af van wat die extensies in hun data patches doen.

De volledige deploy-flow, inclusief Varnish en cache-invalidatie, beschrijven we in Magento deployen op Kubernetes zonder downtime.

Helm-hooks in Argo CD

Gebruik je een Helm-chart met helm.sh/hook-annotaties, dan vertaalt Argo CD die naar eigen hooks. Argo CD gebruikt Helm alleen om de chart te renderen met helm template. Het voert geen helm install of helm upgrade uit.

Dat heeft een gevolg dat in de documentatie staat maar vaak gemist wordt: Argo CD weet niet of een sync een eerste installatie of een upgrade is. pre-install en pre-upgrade worden allebei een PreSync hook en draaien dus bij elke sync. Heb je een pre-install Job die een database aanmaakt, dan draait die bij elke deploy opnieuw. Maak zulke Jobs idempotent, of gebruik direct de Argo CD-annotaties.

Een Job met helm.sh/hook: pre-install,pre-upgrade werkt in Argo CD dus prima voor migraties. Hoe je die Job netjes in een chart opneemt, staat in een Helm-chart voor je Laravel-app.

Als de sync mislukt

Een mislukte migratie is geen ramp als je het goed hebt opgezet. Wat je wel wilt:

  • Een melding. Via Argo CD Notifications op on-sync-failed, of met een SyncFail hook die een webhook aanroept.
  • De logs. Door BeforeHookCreation blijft de mislukte Job staan tot de volgende sync. kubectl logs job/shop-migrate laat zien wat er misging.
  • Geen automatische retry op migraties. Met syncPolicy.retry probeert Argo CD de hele sync opnieuw, inclusief je PreSync hook. Voor Laravel is dat meestal onschuldig, want uitgevoerde migraties staan in de migrations-tabel. Een migratie die halverwege faalde op MySQL kan wel half zijn toegepast, want DDL draait in MySQL niet in een transactie.

Na een fout los je het probleem op in Git en commit je een fix. Handmatig in de database rommelen en daarna opnieuw syncen kan, maar leg dan vast wat je deed.

Wanneer is dit nodig?

Voor een kleine Laravel-app op een enkele server is een deployscript met php artisan migrate --force voldoende. Sync waves en hooks lossen een probleem op dat pas ontstaat met meerdere pods, rolling updates en GitOps. Draai je dat al, of ga je ernaartoe, dan is dit de manier om migraties voorspelbaar te maken.

Wil je naast veilige migraties ook het verkeer geleidelijk naar een nieuwe versie sturen, kijk dan naar canary-deploys met Argo Rollouts. Het grotere plaatje van GitOps en image-updates staat op GitOps met Argo CD en CI/CD naar Kubernetes.

Veelgestelde vragen

Wat is het verschil tussen een sync wave en een hook?+

Een hook bepaalt in welke phase een resource draait: vóór, tijdens of na de sync. Een sync wave bepaalt de volgorde binnen een phase. Je combineert ze: een migratie-Job als PreSync hook, en daarna web in wave 0 en workers in wave 1.

Draait Argo CD een PreSync hook bij elke sync?+

Ja, bij elke sync-operatie, ook als er niets aan je database verandert. Daarom moet je migratie-Job idempotent zijn. php artisan migrate en Magento's setup:db:status-check zijn dat al.

Wat gebeurt er als de migratie-Job faalt?+

De sync stopt en de nieuwe Deployments worden niet toegepast. Je draait door op de vorige release. Lees de logs van de Job, fix in Git en sync opnieuw.

Kan ik een migratie terugdraaien met een Argo CD-rollback?+

Nee. Argo CD zet je manifests terug, niet je database. Een rollback is alleen veilig als de migratie backwards compatible was, volgens expand and contract.

Werken Helm-hooks in Argo CD?+

Grotendeels. pre-install en pre-upgrade worden allebei een PreSync hook en draaien bij elke sync, omdat Argo CD geen onderscheid maakt tussen installatie en upgrade. Niet-ondersteunde hooks worden genegeerd.

Ruthger Idema

Geschreven door Ruthger Idema

Eigenaar van Coding.nl. 15+ jaar e-commerce met Magento, Shopify en Laravel, samen met het vaste team in Emmen.

Wil je dit voor jouw shop laten uitzoeken? Plan een half uur

Een half uur via video met de mensen die het werk ook echt doen. Kies hieronder een moment dat jou past.

  • Direct met het vaste team, geen tussenlagen
  • Concrete feedback op je huidige situatie of plannen
  • Eerlijk advies, ook als het antwoord "niet doen" is
  • Indicatie van kosten en tijdlijn
Liever bellen? 0591-239 299
Beschikbaar deze week

Plan een gratis Tech Check

Vrijblijvend jouw uitdagingen bespreken

Of bel direct: 0591-239 299