On Kubernetes
Web, cron and consumer deployments, probes that use health_check.php, shared media, and the schema change as a gated Job.
readinessProbe: httpGet: {path: /health_check.php, port: 8080}On Kubernetes a Magento release is three kinds of workload and one job. The split matters because they fail differently: web pods must be replaceable at any moment, the scheduler must never be duplicated, and the schema change must happen exactly once per release.
The workloads
- A web Deployment, nginx in front of php-fpm, scaled to traffic. The nginx container needs the built
pub/static, so it runs an image from the same release. - A cron Deployment with
replicas: 1andstrategy: Recreate. Two schedulers write competing rows intocron_schedule. - One consumer Deployment per queue, running
bin/magento queue:consumers:start <name>. A consumer is a PHP process that reads one queue; it exits when--max-messagesis reached and the kubelet restarts it.
Probes
Magento ships pub/health_check.php. It boots the application, connects to every configured database, tests every cache backend, and returns 500 if any of that fails. That makes it the right readiness probe and the wrong liveness probe: it is not cheap, so give it periodSeconds: 15 or more and use a TCP check on the php-fpm port for liveness.
Media and migrations
pub/media is written at runtime, so it is a ReadWriteMany volume mounted into the web and cron pods, or it moves off the filesystem entirely with bin/magento setup:config:set --remote-storage-driver=aws-s3. The schema change runs as its own Job, from the new image, gated on bin/magento setup:db:status: exit 2 means an upgrade is needed, exit 0 means it is not. Start the rollout only after the Job succeeds.
Example
# k8s/web-deployment.yaml apply with: kubectl apply -f k8s/web-deployment.yaml
apiVersionapps/v1
kindDeployment
metadata
namemagento-web
namespacestore
spec
replicas4
selector
matchLabelsappmagento roleweb
template
metadata
labelsappmagento roleweb
spec
containers
namenginx
imagestore/web1.42 # same release: nginx plus pub/static
portscontainerPort8080
readinessProbe
httpGetpath/health_check.php port8080
periodSeconds15
timeoutSeconds5
volumeMounts
namemedia mountPath/app/pub/media
namephp-fpm
imagestore/app1.42
livenessProbe
tcpSocketport9000
periodSeconds20
volumeMounts
namemedia mountPath/app/pub/media
nameenv mountPath/app/app/etc/env.php
subPathenv.php readOnlytrue
volumes
namemedia
persistentVolumeClaimclaimNamemagento-media # ReadWriteMany
nameenv
secretsecretNamemagento-env-phpWhen to use it
- Run a Magento store on an existing Kubernetes cluster without the web pods fighting over cron.
- Keep a release from serving new code against the old database schema.
- Choose a readiness probe that notices a lost database rather than only a dead port.
- Share merchandiser-uploaded media across several web pods.
More examples
The schema change as a gated Job
One Job per release, named after the release, so a second apply is a no-op rather than a second migration. The exit code from setup:db:status decides whether anything runs at all, and backoffLimit: 0 stops Kubernetes from retrying a schema change that failed halfway. Use this instead of putting setup:upgrade in a web pod's entrypoint, where every replica would run it at once.
# k8s/migrate-job.yaml kubectl apply -f k8s/migrate-job.yaml
apiVersion: batch/v1
kind: Job
metadata:
name: magento-migrate-1-42
namespace: store
spec:
backoffLimit: 0 # never retry a half-applied schema change
ttlSecondsAfterFinished: 3600
template:
spec:
restartPolicy: Never
containers:
- name: migrate
image: store/app:1.42 # the release being deployed
command: ["sh", "-c"]
args:
- |
bin/magento setup:db:status; status=$?
if [ "$status" -eq 0 ]; then echo "schema current"; exit 0; fi
if [ "$status" -ne 2 ]; then echo "db:status failed"; exit 1; fi
bin/magento setup:upgrade --keep-generated
volumeMounts:
- {name: env, mountPath: /app/app/etc/env.php,
subPath: env.php, readOnly: true}
volumes:
- name: env
secret: {secretName: magento-env-php}Cron and consumer deployments
Recreate on the cron Deployment is the point: the default rolling update briefly runs two schedulers, which double-schedules jobs. The consumer takes replicas because several processes may read the same queue, and --max-messages makes each pod exit and be replaced before its memory use matters. Volume and secret mounts are omitted here for length; they match the web Deployment.
# k8s/workers.yaml kubectl apply -f k8s/workers.yaml
apiVersion: apps/v1
kind: Deployment
metadata: {name: magento-cron, namespace: store}
spec:
replicas: 1 # exactly one scheduler, always
strategy: {type: Recreate} # never two during a rollout
selector:
matchLabels: {app: magento, role: cron}
template:
metadata:
labels: {app: magento, role: cron}
spec:
containers:
- name: cron
image: store/app:1.42
command: ["sh", "-c", "while true; do bin/magento cron:run; sleep 60; done"]
---
apiVersion: apps/v1
kind: Deployment
metadata: {name: magento-consumer-async, namespace: store}
spec:
replicas: 2
selector:
matchLabels: {app: magento, role: consumer-async}
template:
metadata:
labels: {app: magento, role: consumer-async}
spec:
containers:
- name: consumer
image: store/app:1.42
command: ["bin/magento", "queue:consumers:start",
"async.operations.all", "--max-messages=1000"]The order of one release
Each step waits for the previous one, which is the whole difference between a deploy and an outage. Stopping consumers and cron first prevents an old-code process from writing through a schema it does not know. Keep this as a script or a pipeline stage so nobody performs the steps from memory at 22:00.
# Workers stop first so no old-code process touches the new schema.
kubectl -n store scale deploy/magento-cron --replicas=0
kubectl -n store scale deploy/magento-consumer-async --replicas=0
kubectl -n store apply -f k8s/migrate-job.yaml
kubectl -n store wait --for=condition=complete \
job/magento-migrate-1-42 --timeout=20m
kubectl -n store set image deploy/magento-web \
nginx=store/web:1.42 php-fpm=store/app:1.42
kubectl -n store rollout status deploy/magento-web
# Workers come back on the new image only after the web pods are ready.
kubectl -n store scale deploy/magento-cron --replicas=1
kubectl -n store scale deploy/magento-consumer-async --replicas=2
# If the rollout stalls:
kubectl -n store rollout undo deploy/magento-web
Discussion