Inhoud van dit artikel
- Phases en waves: hoe Argo CD de volgorde bepaalt
- De hooks die je nodig hebt
- Laravel: migrate als PreSync Job
- ExternalSecret en ConfigMap
- web-Deployment (php-fpm/nginx of Octane)
- Horizon en scheduler
- Expand en contract: de echte veiligheidsregel
- Magento: setup:upgrade als hook
- Helm-hooks in Argo CD
- Als de sync mislukt
- 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:
- Phase: PreSync, Sync, PostSync.
- Wave: laagste nummer eerst. Default is wave 0, negatieve waves mogen.
- Kind: namespaces eerst, dan andere Kubernetes-resources, dan custom resources.
- 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:
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:
| Hook | Wanneer |
|---|---|
PreSync | Voordat de manifests worden toegepast |
Sync | Tegelijk met de manifests, na de PreSync hooks |
PostSync | Als alles synced en healthy is |
SyncFail | Als 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
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. --forceomdat Laravel in productie anders om bevestiging vraagt en de Job blijft hangen.--isolatedpakt een atomic lock via de cache-driver. Een tweedemigratetegelijk 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: 0zodat een mislukte migratie niet automatisch drie keer opnieuw draait. Een halve migratie die opnieuw start, maakt de schade vaak groter.activeDeadlineSecondsals 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:
# 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.
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:
- 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.
- 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.
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/enabledinenv.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 eenSyncFailhook die een webhook aanroept. - De logs. Door
BeforeHookCreationblijft de mislukte Job staan tot de volgende sync.kubectl logs job/shop-migratelaat zien wat er misging. - Geen automatische retry op migraties. Met
syncPolicy.retryprobeert Argo CD de hele sync opnieuw, inclusief je PreSync hook. Voor Laravel is dat meestal onschuldig, want uitgevoerde migraties staan in demigrations-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.




