Migrations in a deploy
After this you can answer: When do database migrations run, and what happens if one fails halfway?
Picture it
Section titled “Picture it”flowchart TB A["phase A: migration task<br/>definition → NEW image"] --> R["run-migrations.sh<br/>one task, FARGATE"] R --> M["node dist/migrate.js"] M -->|"pending SQL files,<br/>one transaction"| DB[("RDS Postgres")] M -->|"exit 0"| B["phase B: services roll"] M -->|"exit ≠ 0"| S["deploy stops<br/>old app keeps running"]How it works
Section titled “How it works”- Its own task definition.
minitickets-dev-migratehas its own image setting, so phase A can point it at the NEW image while the services keep the current one. - One task.
scripts/run-migrations.shstarts it with launch typeFARGATE, not Spot: a Spot interruption must never stop a migration halfway. It runs in the api’s subnets with the api security group, which the database allows on TCP 5432. - Apply.
node dist/migrate.jsruns every SQL file that has not run yet. Drizzle records the finished ones in a table, and applies all pending files inside ONE transaction. - Report. The script waits for the task to stop, prints its last 20 log lines, and exits with the container’s exit code. No exit code at all means the container never ran; the task’s stopped reason says why.
In MiniTickets
Section titled “In MiniTickets”The task uses only the database settings (DB_HOST, DB_PORT, DB_NAME, and the user and password
from the RDS master secret). It borrows the api execution role to read that secret and has no task
role: a migration calls no AWS API. Its logs go to /minitickets/dev/api with the stream prefix
migrate. The same script runs by hand as pnpm infra:migrate.
Because Postgres can change tables inside a transaction, a failing SQL file rolls back every file of that run. Infrastructure changes from phase A stay applied.
| From → to | What happens | What allows it |
|---|---|---|
| Migration task definition → RDS Postgresonly when computeEnabled = true | runs node dist/migrate.js against the database |
|
| Migration task definition → API task execution roleonly when computeEnabled = true | the migration borrows the api execution role (no task role) |
|
| CD deploy job → Migration task definitiononly when computeEnabled = true | runs the migration task (scripts/run-migrations.sh) with launch type FARGATE |
|
See it in AWS
Section titled “See it in AWS”ECS → Task definitions → minitickets-dev-migrate; the run’s lines in CloudWatch →
/minitickets/dev/api, streams starting with migrate/.
aws ecs describe-task-definition --task-definition minitickets-dev-migrate
infra/src/compute/migration-task.ts → createMigrationTask and
scripts/run-migrations.sh.
Break it
Section titled “Break it”| Symptom | Cause | Where to look |
|---|---|---|
| CD stops at “Run the database migration”; the old app still runs | A SQL statement failed; phase B never ran | The step’s last 20 log lines |
| The task stops with no exit code | The image could not be pulled, or the secret not read | The task’s stopped reason |
| Someone proposes Spot for migrations | An interruption could stop it halfway | run-migrations.sh uses FARGATE on purpose |
Say it in an interview
Section titled “Say it in an interview”Follow-ups
Section titled “Follow-ups”Why must migrations be backward compatible?
The old code runs against the new schema until phase B finishes, and a later app rollback runs old code against it too. So add first (expand), switch the code, and remove later (contract).
Why not run migrations when the api starts?
Several api tasks starting together would all migrate at once, and a failed migration would crash every new task instead of stopping the deploy cleanly.
What cannot run inside the transaction?
Some statements, such as CREATE INDEX CONCURRENTLY, refuse to run in a transaction. They would
need their own migration step, planned separately.
Advanced: Details: why the api execution role
Reusing the api execution role means the migration could also read the Redis and revalidate secrets, which it does not need. The trade-off was accepted to avoid one more role. The task asks for 0.25 vCPU and 512 MB, on ARM64.
Checkpoint
Section titled “Checkpoint”Answer these without scrolling up. If one feels hard, reread that section.
- Why does the migration have its own task definition?
- Why FARGATE and not Spot?
- What happens to the running app when a migration fails?
- How many transactions does one migration run use?
- Why must a migration stay compatible with the old code?
Redraw it from memory: phase A (migration task def → new image) → run-migrations.sh (FARGATE, api SG) → migrate.js → Postgres → exit 0: phase B / else: stop
Try this: on the map, select the migration task definition and find which security group lets it reach Postgres
Read this book online: https://book.theversehub.in/