Magento deployen op Kubernetes zonder downtime

Coding.nl
8 min leestijd
Inhoud van dit artikel
  1. Waarom het op Kubernetes anders werkt
  2. Stap 1: alles in het image bouwen
  3. syntax=docker/dockerfile:1
  4. Stap 2: taggen, pushen en de deploy-repo bijwerken
  5. overlays/productie/kustomization.yaml
  6. Stap 3: detecteren of setup:upgrade nodig is
  7. Stap 4: maintenance alleen als het moet
  8. Stap 5: de rolling update
  9. Stap 6: terugdraaien
  10. De checklist per deploy

Magento zonder downtime deployen op Kubernetes komt neer op vier dingen: alles bouwen in het image, vóór de uitrol met setup:db:status detecteren of er een databasewijziging is, alleen bij een echte schemawijziging setup:upgrade draaien in een Job, en de pods daarna rollend vervangen. Maintenance mode heb je alleen nodig bij wijzigingen die de oude code breken. Terugdraaien is een Git-revert, maar alleen als je databasewijzigingen achterwaarts compatibel houdt.

Waarom het op Kubernetes anders werkt

Op een klassieke server werkt een Magento-deploy vaak zo: code ophalen, maintenance:enable, setup:upgrade, compileren, static content deployen, maintenance:disable. De shop ligt dan minuten plat, elke deploy.

Op Kubernetes kun je dat niet een-op-een overnemen:

  • Pods zijn wegwerpbaar. Compileren in een draaiende pod is zinloos; de volgende pod begint weer bij nul.
  • De maintenance-flag is een bestand. bin/magento maintenance:enable schrijft var/.maintenance.flag. Met tien pods zonder gedeelde var/ zet je maintenance aan op precies één pod.
  • Oude en nieuwe pods draaien tegelijk. Tijdens een rolling update bedienen beide versies bezoekers, tegen dezelfde database.

Dat laatste punt bepaalt de hele aanpak. Je deploy moet zo ingericht zijn dat oude code korte tijd tegen het nieuwe databaseschema kan draaien.

Stap 1: alles in het image bouwen

Het image bevat de complete, gecompileerde applicatie. Niets wordt gebouwd bij het opstarten van een pod. Magento kan de build-stap draaien zonder databaseverbinding, zolang app/etc/config.php de modules, scopes en thema's bevat.

Die configuratie zet je eenmalig vast en commit je:

bash
bin/magento app:config:dump scopes themes i18n

Een multi-stage Dockerfile:

dockerfile
# syntax=docker/dockerfile:1
FROM registry.example.nl/base/magento-php:8.4 AS build
WORKDIR /var/www/html
COPY composer.json composer.lock ./
RUN --mount=type=secret,id=composer_auth,target=/var/www/html/auth.json \
    composer install --no-dev --prefer-dist --no-interaction
COPY . .

FROM node:22 AS theme
WORKDIR /var/www/html
COPY --from=build /var/www/html ./
RUN cd app/design/frontend/Voorbeeld/shop/web/tailwind \
 && npm ci && npm run build

FROM build AS compile
COPY --from=theme /var/www/html/app/design/frontend/Voorbeeld/shop/web/css/ \
     app/design/frontend/Voorbeeld/shop/web/css/
RUN bin/magento setup:di:compile \
 && composer dump-autoload --no-dev --optimize \
 && bin/magento setup:static-content:deploy -f --jobs=4 nl_NL en_US

FROM registry.example.nl/base/magento-php:8.4 AS runtime
WORKDIR /var/www/html
COPY --from=compile --chown=www-data:www-data /var/www/html /var/www/html
USER www-data

Wat hier gebeurt:

  • De Composer-credentials voor repo.magento.com komen binnen als build secret. Ze staan niet in een image-laag.
  • Het Hyvä-thema bouwt in een aparte Node-stage, met de vendor-map erbij, omdat Tailwind ook de templates van modules scant. Commit app/etc/hyva-themes.json (gegenereerd met bin/magento hyva:config:generate) of genereer het in de build-stage. In de runtime zit geen Node. Paden en scriptnamen verschillen per thema en Hyvä-versie; pas ze aan op je eigen structuur.
  • DI en static content worden één keer gebouwd. Elke pod die start, is direct klaar.
  • De PHP-versie van het basisimage volgt je Magento-versie. Volgens de systeemeisen van Adobe is dat PHP 8.3 of 8.4 voor 2.4.8 en PHP 8.5 voor 2.4.9.

env.php zit niet in het image. Die komt bij het draaien uit een Secret, met daarin ook 'MAGE_MODE' => 'production'. Hoe het image in de rest van de architectuur past, lees je in Magento 2 op Kubernetes: de architectuur per component.

Stap 2: taggen, pushen en de deploy-repo bijwerken

Elk image krijgt een onveranderlijke tag, meestal de commit-hash. Nooit latest. Met latest weet je niet wat er draait en kun je niet terug.

In CI ziet dat er ongeveer zo uit:

bash
docker buildx build \
  --secret id=composer_auth,src=auth.json \
  -t registry.example.nl/shop/magento:${GIT_SHA} \
  --push .

Daarna wordt de nieuwe tag in de deploy-repository gezet, bijvoorbeeld in een Kustomize-overlay:

yaml
# overlays/productie/kustomization.yaml
resources:
  - ../../base
images:
  - name: registry.example.nl/shop/magento
    newTag: 1f3c9a2

Die commit kan je pipeline doen, of Argo CD Image Updater. Argo CD ziet de wijziging en synchroniseert. Een complete pipeline met GitHub Actions beschrijven we in Magento 2 CI/CD pipeline opzetten met GitHub Actions. Hoe wij builds, registry en promotie tussen omgevingen inrichten, staat op CI/CD naar Kubernetes.

Stap 3: detecteren of setup:upgrade nodig is

Niet elke deploy wijzigt de database. Een template-aanpassing of een bugfix in een controller raakt het schema niet. Dan is setup:upgrade overbodig en alleen maar risico.

Magento heeft daar een commando voor. bin/magento setup:db:status geeft exit code 0 als alles bijgewerkt is, en exit code 2 als setup:upgrade nodig is. Exit code 1 betekent dat de code en de database op een andere manier niet overeenkomen; dan stop je en kijk je wat er mis is.

Wij draaien die controle in een Job die Argo CD vóór de uitrol uitvoert, met het nieuwe image:

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
  template:
    spec:
      restartPolicy: Never
      containers:
        - name: upgrade
          image: registry.example.nl/shop/magento:1f3c9a2
          command: ["/bin/sh", "-c"]
          args:
            - |
              set +e
              bin/magento setup:db:status
              STATUS=$?
              set -e
              if [ "$STATUS" -eq 0 ]; then
                bin/magento app:config:import --no-interaction
              elif [ "$STATUS" -eq 2 ]; then
                bin/magento setup:upgrade --keep-generated --no-interaction
              else
                exit "$STATUS"
              fi
              bin/magento cache:clean config

De keuzes:

  • PreSync: als deze Job faalt, stopt Argo CD de hele sync. De oude pods blijven draaien. Niemand merkt iets.
  • --keep-generated: de gegenereerde code staat al in het image. Zonder deze vlag gooit setup:upgrade die weg.
  • cache:clean config in plaats van cache:flush. Een volledige flush laat al je pods tegelijk een koude cache opbouwen.

Een bekende valkuil: bij sommige versies meldt setup:db:status een verschil dat er niet echt is, bijvoorbeeld rond JSON-kolommen of lege comments in db_schema.xml. Dan draait setup:upgrade bij elke deploy. Controleer na een deploy of de status weer 0 is. Zo niet, zoek uit welk verschil blijft hangen.

Hoe je zulke Jobs en de volgorde van resources regelt met sync waves, werken we uit in Argo CD sync waves voor migraties. De basis van onze GitOps-opzet staat op GitOps met Argo CD.

Stap 4: maintenance alleen als het moet

Tijdens setup:upgrade draaien de oude pods door. Dat werkt alleen als de oude code de nieuwe database accepteert. Magento gooit normaal een "Please upgrade your database"-fout zodra de moduleversies in de database nieuwer zijn dan de code verwacht.

Sinds Magento 2.4.4 kun je die controle uitzetten in env.php:

php
'deployment' => [
    'blue_green' => [
        'enabled' => true,
    ],
],

Daarmee blijven de oude pods bedienen terwijl de upgrade draait. Belangrijk: deze vlag maakt destructieve schemawijzigingen niet veilig. Hij haalt alleen de blokkade weg.

Wij onderscheiden daarom drie soorten deploys:

  1. Geen databasewijziging. Rolling update, geen maintenance. Dit is verreweg de meeste deploys.
  2. Achterwaarts compatibele wijziging. Een nieuwe kolom, een nieuwe tabel, een nieuwe index. setup:upgrade met de blue/green-vlag, oude pods draaien door, geen maintenance.
  3. Brekende wijziging. Een kolom die verdwijnt of van type verandert, een grote datamigratie. Hier plannen we maintenance in.

Voor type 3 werkt maintenance:enable niet, om de reden die we bovenaan noemden. Maintenance regel je dan op het netwerkniveau. Bijvoorbeeld door de HTTPRoute tijdelijk naar een simpele maintenance-pagina te sturen:

yaml
rules:
  - backendRefs:
      - name: maintenance-page
        port: 80

Dat is een commit in Git, zichtbaar en terug te draaien. Pauzeer ook cron tijdens zo'n upgrade, zodat oude cron-code niet op een half gemigreerde database draait:

bash
kubectl -n shop-productie patch cronjob magento-cron \
  -p '{"spec":{"suspend":true}}'

Het beste is: zorg dat type 3 zelden voorkomt. Splits een brekende wijziging op in twee releases. Eerst de nieuwe kolom toevoegen en beide kolommen vullen. Pas in een volgende release de oude kolom weghalen. Dit heet expand en contract, en het is de basis van deploys zonder downtime.

Stap 5: de rolling update

Na een geslaagde PreSync-Job vervangt Kubernetes de pods. Met deze instellingen gaat er geen capaciteit verloren:

yaml
spec:
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 0
  template:
    spec:
      terminationGracePeriodSeconds: 30
      containers:
        - name: nginx
          readinessProbe:
            httpGet:
              path: /health_check.php
              port: 8080
          lifecycle:
            preStop:
              exec:
                command: ["sleep", "10"]
  • maxUnavailable: 0: een oude pod gaat pas weg als een nieuwe pod ready is.
  • Readiness probe: een nieuwe pod krijgt pas verkeer als Magento antwoord geeft.
  • preStop: de pod blijft nog even draaien terwijl de load balancer hem uit rotatie haalt, zodat lopende requests afmaken.

Queue-consumers en eventuele cron-Deployments rollen gewoon mee met het nieuwe image. Geef consumers een terminationGracePeriodSeconds die past bij hun langste bericht, zodat een import niet halverwege wordt afgebroken.

Static content krijgt in Magento een versienummer in de URL. De standaard Nginx-configuratie van Magento haalt dat versienummer er weer uit bij het serveren. Oude HTML in Varnish laadt na de uitrol dus de nieuwe CSS en JavaScript. Meestal is dat geen probleem. Is de markup flink veranderd, ververs dan de page cache met cache:clean full_page, niet met een volledige flush.

Wil je nog voorzichtiger uitrollen, met eerst een klein deel van het verkeer op de nieuwe versie, dan kun je de Deployment vervangen door een Argo Rollout met een canary-strategie. Dat werken we uit in canary deploys met Argo Rollouts.

Stap 6: terugdraaien

Een rollback van de code is simpel. Zet de vorige image-tag terug in Git:

bash
git revert HEAD
git push

Argo CD synchroniseert en de pods rollen terug naar het vorige image. Omdat het image alles bevat, is de oude versie binnen de tijd van een rolling update terug.

De database draait niet mee terug. Magento heeft geen "setup:downgrade". Daarom is expand en contract zo belangrijk: na een type-2-wijziging werkt de oude code nog steeds tegen het nieuwe schema. Een rollback is dan veilig.

Na een type-3-wijziging ligt dat anders. Daar hoort een database-snapshot vlak voor de upgrade bij, en een bewuste keuze: terug naar de snapshot met verlies van tussentijdse bestellingen, of vooruit met een fix. Die keuze maak je vóór de deploy, niet tijdens het incident.

De checklist per deploy

  • Image gebouwd met onveranderlijke tag, inclusief DI en static content.
  • config.php bijgewerkt en gecommit als er modules zijn bijgekomen.
  • Databasewijziging beoordeeld: geen, compatibel of brekend.
  • Bij brekend: snapshot gemaakt, maintenance-route klaar, cron gepauzeerd.
  • PreSync-Job geslaagd, setup:db:status daarna op 0.
  • Rolling update afgerond, alle pods ready.
  • Steekproef in de shop: homepage, categorie, product, winkelwagen, checkout.

Zo inrichten doen we voor elke shop die we op Kubernetes zetten. Meer over onze aanpak: Magento 2 op Kubernetes.

Veelgestelde vragen

Kan ik bin/magento maintenance:enable gebruiken op Kubernetes?+

Alleen als var/ gedeeld is tussen alle pods, en dat raden we af. Regel maintenance liever op de Gateway of in Varnish, zodat het voor alle pods tegelijk geldt.

Moet setup:upgrade bij elke deploy draaien?+

Nee. Laat setup:db:status bepalen of het nodig is. Bij exit code 0 volstaat app:config:import en blijft de database ongemoeid.

Wat doet de blue_green-instelling in env.php precies?+

Hij voorkomt dat Magento een "upgrade je database"-fout geeft als de database nieuwer is dan de code. Oude pods kunnen dan doordraaien tijdens een upgrade. Hij maakt brekende schemawijzigingen niet veilig.

Hoe snel kan ik terugdraaien?+

Zo snel als een rolling update duurt, omdat het vorige image compleet is. Voorwaarde is dat de databasewijziging van de nieuwe release achterwaarts compatibel was.

Werkt dit ook voor security patches?+

Ja. Een patch is een nieuwe image-build en een normale uitrol. Veel patches raken het schema niet, waardoor het een deploy zonder databasestap wordt.

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