db-init creates the schema; later releases change it with numbered SQL files, applied by a Job named after the release (a Job's template is immutable) before the new API rolls out. The first indexes the genre filter:
CREATE INDEX IF NOT EXISTS books_genre_idx ON books (genre);apiVersion: batch/v1
kind: Job
metadata: { name: db-migrate-1-5, labels: { app: booknest } }
spec:
activeDeadlineSeconds: 300
ttlSecondsAfterFinished: 86400
template:
spec:
restartPolicy: Never
containers:
- name: migrate
image: localhost:33500/postgres:18
command: [sh, -c, 'for f in /migrations/*.sql; do psql -v ON_ERROR_STOP=1 -f "$f"; done']
envFrom: [{ configMapRef: { name: api-config } }]
env:
- name: PGUSER
valueFrom: { secretKeyRef: { name: db-credentials, key: username } }
- name: PGPASSWORD
valueFrom: { secretKeyRef: { name: db-credentials, key: password } }
volumeMounts: [{ name: migrations, mountPath: /migrations }]
volumes: [{ name: migrations, configMap: { name: db-migrations } }]kubectl create configmap db-migrations --from-file=db/migrations/
kubectl apply -f k8s/db-migrate-job.yaml
kubectl wait --for=condition=Complete job/db-migrate-1-5 --timeout=120s
kubectl logs job/db-migrate-1-5
git add db/migrations k8s/db-migrate-job.yaml
git commit -qm "Add a migration Job and a genre index"Output
configmap/db-migrations created job.batch/db-migrate-1-5 created job.batch/db-migrate-1-5 condition met CREATE INDEX
IF NOT EXISTS makes a retry harmless. Migration tools such as node-pg-migrate 1,487 , Flyway 14,820 , Liquibase 51,363 and Atlas also record which files have run. What matters most is order: the Job must finish before new API Pods start, whether a pipeline runs it first (Jenkins), Helm 29,435 as a pre-upgrade hook (Hooks and Dependencies) or Argo CD 126 as a PreSync hook (GitOps with Argo CD). Keep migrations backward-compatible so old Pods work during the rollout.