Ask any developer what keeps them awake at night during a major release, and nine times out of ten the answer is schema migrations. Dropping a column, renaming a table, or altering a foreign key constraint on a live production database while traffic is hitting your API can cause cascading failures and catastrophic downtime.
For SMBs running applications on Kubernetes, the knee-jerk reaction is often to run migrations as a Kubernetes Job during an ArgoCD rollout. If the migration fails or locks a table, your entire deployment pipeline stalls.
1. The Expand-Contract Pattern
Zero-downtime migrations rely on the Expand-Contract (or Expand-Migrate-Contract) pattern. You never perform a destructive change in a single deployment. Instead, you break the migration into backward-compatible phases:
- Expand: Add the new column or table alongside the old one. Update your application code to write to both (or handle both schemas).
- Migrate: Backfill historical data from the old column to the new column asynchronously.
- Contract: Once all running pods use the new schema, remove the old column in a subsequent release.
2. Managing Kubernetes Rollouts with ArgoCD
When orchestrating schema changes in Kubernetes, ensure your database migration Job runs as a pre-sync hook in ArgoCD, but design your application pods to tolerate both old and new schema states simultaneously during the rollout window.
apiVersion: batch/v1
kind: Job
metadata:
name: db-migration-expand
annotations:
argocd.argoproj.io/hook: PreSync
argocd.argoproj.io/hook-delete-policy: HookSucceeded
spec:
template:
spec:
containers:
- name: migrator
image: myapp:v2.1.0
command: ["npm", "run", "db:migrate:expand"]
restartPolicy: Never
Conclusion
Database migrations don’t have to be a high-stress event. By decoupling schema expansion from data contraction and structuring your Kubernetes rollouts defensively, you can achieve true zero-downtime deployments.