CI/CD for Migrations — Complete Guide
In this tutorial, you will learn about CI/CD for Migrations. We cover key concepts, practical examples, and best practices to help you master this topic.
Learn CI/CD integration for database migrations: automate Migration execution in deployment pipelines, test migrations in CI, manage migration state across environments, and implement safe deployment strategies with automated rollback on failure.
What You Learn
You will learn how to integrate database migrations into CI/CD pipelines: running migrations as a pipeline stage, testing migrations in CI with fresh databases, managing migration state across dev/staging/production environments, implementing safe deployment strategies with automated rollback, and monitoring migration execution in production.
Why It Matters
Manual migration execution is error-prone and slow. Developers forget to run migrations before deploying. Production schema drifts from staging. Rollbacks require manual commands under pressure. Automating migrations in CI/CD ensures consistent, safe, and auditable schema changes across all environments.
Real-World Use
DodaTech runs migrations as an automated stage in GitLab CI. The pipeline creates a fresh database, applies all migrations, runs the test suite, and verifies rollback. When a migration fails in production, the pipeline automatically rolls back and notifies the team. Zero manual migration execution in the last 18 months.
Pipeline Architecture
graph LR
Commit[Code Commit] --> CI[CI Pipeline]
CI --> Build[Build Stage]
CI --> MigrationTest[Migration Test]
CI --> UnitTest[Unit Tests]
Build --> Deploy[Deploy Stage]
MigrationTest --> Deploy
UnitTest --> Deploy
Deploy --> Migrate[Run Migrations]
Deploy --> AppDeploy[Deploy Application]
Migrate --> Verify[Health Check]
AppDeploy --> Verify
Verify --> Success[Deploy Success]
Verify --> Rollback[Auto Rollback]
The CI/CD pipeline has two main phases: CI (test migrations) and CD (execute migrations and deploy). Migration testing in CI catches issues before they reach production.
CI Pipeline: Migration Testing
# .github/workflows/ci-migrations.yml
name: CI - Migration Tests
on:
pull_request:
paths:
- 'migrations/**'
- 'models/**'
- 'prisma/schema.prisma'
jobs:
migration-tests:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:16
env:
POSTGRES_PASSWORD: postgres
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: Install dependencies
run: pip install -r requirements-dev.txt
- name: Create test database
run: createdb -h localhost -U postgres ci_test
- name: Apply all migrations from scratch
run: alembic upgrade head
env:
DATABASE_URL: postgresql://postgres:postgres@localhost/ci_test
- name: Verify schema
run: python -m scripts.verify_schema
env:
DATABASE_URL: postgresql://postgres:postgres@localhost/ci_test
- name: Test rollback
run: |
alembic downgrade -1
alembic upgrade head
env:
DATABASE_URL: postgresql://postgres:postgres@localhost/ci_test
- name: Run application tests
run: pytest tests/
env:
DATABASE_URL: postgresql://postgres:postgres@localhost/ci_test
Expected output: CI pipeline runs migration tests on every PR that changes migrations or models. It creates a fresh database, applies all migrations, verifies schema, tests rollback, and runs the application test suite.
CD Pipeline: Migration Execution
# .gitlab-ci.yml with migration stages
stages:
- build
- migrate
- deploy
- verify
variables:
MIGRATION_IMAGE: $CI_REGISTRY_IMAGE/migration:$CI_COMMIT_SHA
build-migration-image:
stage: build
script:
- docker build -t $MIGRATION_IMAGE -f Dockerfile.migration .
- docker push $MIGRATION_IMAGE
only:
- main
run-migrations:
stage: migrate
image: $MIGRATION_IMAGE
script:
- echo "Running migrations..."
- alembic upgrade head
- echo "Migrations completed"
environment:
name: production
only:
- main
when: manual # Require manual approval for production migrations
allow_failure: false
deploy-application:
stage: deploy
script:
- kubectl set image deployment/app app=$CI_REGISTRY_IMAGE/app:$CI_COMMIT_SHA
- kubectl rollout status deployment/app
environment:
name: production
needs: ["run-migrations"]
only:
- main
verify-deployment:
stage: verify
script:
- python scripts/health_check.py
- python scripts/verify_schema.py
- python scripts/smoke_tests.py
environment:
name: production
needs: ["deploy-application"]
only:
- main
auto-rollback:
stage: verify
script:
- python scripts/rollback.py
- kubectl rollout undo deployment/app
environment:
name: production
needs: ["deploy-application"]
when: on_failure # Triggered when verify fails
only:
- main
Expected output: CD pipeline runs migrations as a separate stage before deployment. If migrations fail, the deployment is skipped. If verification fails after deployment, automated rollback reverts both schema and application.
Multi-Environment Migration Strategy
# Environment-specific migration configuration
# .env.development
DATABASE_URL=postgresql://dev:dev@localhost/dev_db
AUTO_MIGRATE=true # Auto-run migrations on dev startup
# .env.staging
DATABASE_URL=postgresql://staging:staging@staging-host/staging_db
AUTO_MIGRATE=true # Auto-run migrations in staging CI
# .env.production
DATABASE_URL=postgresql://prod:prod@prod-host/prod_db
AUTO_MIGRATE=false # Require manual approval for production
#!/bin/bash
# scripts/run_migrations.sh
set -euo pipefail
ENVIRONMENT=${1:-development}
echo "Running migrations for $ENVIRONMENT environment..."
case $ENVIRONMENT in
development)
# Auto-run migrations on dev startup
alembic upgrade head
echo "Development migrations applied"
;;
staging)
# Auto-run in staging CI/CD
alembic upgrade head
echo "Staging migrations applied"
;;
production)
# Require confirmation for production
echo "WARNING: Running migrations on PRODUCTION"
echo "Current migration state:"
alembic current
read -p "Continue with production migration? (yes/no): " confirmation
if [ "$confirmation" != "yes" ]; then
echo "Migration cancelled"
exit 1
fi
# Take a backup before migrating
echo "Taking database backup..."
pg_dump $DATABASE_URL > "backup_$(date +%Y%m%d_%H%M%S).sql"
# Run migrations
alembic upgrade head
echo "Production migrations applied"
# Verify
python scripts/verify_schema.py
echo "Schema verification passed"
;;
esac
Expected output: Migration script adapts to each environment. Development and staging auto-run migrations. Production requires manual confirmation and takes a backup before migrating.
Migration Status Monitoring
# scripts/monitor_migrations.py
"""Monitor migration execution and report status."""
import os
import time
import requests
from datetime import datetime
def monitor_migration():
"""Monitor migration execution and report to team."""
webhook_url = os.environ.get('SLACK_WEBHOOK_URL')
start_time = time.time()
def report(message, status='info'):
if webhook_url:
requests.post(webhook_url, json={
'text': f'[Migration] {message}',
'attachments': [{
'color': {
'info': '#3498db',
'success': '#2ecc71',
'error': '#e74c3c',
}.get(status, '#3498db'),
'fields': [
{'title': 'Environment', 'value': os.environ.get('ENV', 'unknown'), 'short': True},
{'title': 'Timestamp', 'value': datetime.now().isoformat(), 'short': True},
{'title': 'Duration', 'value': f'{time.time() - start_time:.1f}s', 'short': True},
]
}]
})
try:
report('Migration starting...')
# Run migration
result = subprocess.run(
['alembic', 'upgrade', 'head'],
capture_output=True, text=True, check=True,
timeout=300
)
report(f'Migration completed in {time.time() - start_time:.1f}s', 'success')
return True
except subprocess.CalledProcessError as e:
report(f'Migration FAILED: {e.stderr[:500]}', 'error')
return False
except subprocess.TimeoutExpired:
report('Migration TIMEOUT after 5 minutes', 'error')
return False
Expected output: Migration monitor reports start, completion, and failure to the team chat. Includes environment, timestamp, and duration. Failures trigger alerts for immediate investigation.
Safe Deployment Strategies
# Deployment strategies for different migration risk levels
# Strategy 1: Blue-Green Deployment (safe for most migrations)
blue-green:
- Run migrations on both blue and green databases
- Switch traffic after both are migrated
- Rollback: switch traffic back
# Strategy 2: Expand-Contract Pattern (for breaking changes)
expand-contract:
- Phase 1: Add new columns/tables (non-breaking)
- Phase 2: Update application to use new schema
- Phase 3: Remove old columns/tables (in a later release)
- Rollback: Revert application code, keep old schema
# Strategy 3: Feature Flags
feature-flags:
- Add column with feature flag gating
- Deploy with flag disabled
- Enable flag after verifying migration
- Rollback: Disable flag
# scripts/expand_contract_migration.py
"""Example of expand-contract pattern."""
def phase1_expand():
"""Add new columns alongside old ones."""
op.add_column('users', sa.Column('email_new', sa.String(255)))
op.add_column('users', sa.Column('email_verified_new', sa.Boolean()))
def phase2_migrate_data():
"""Backfill new columns from old ones."""
op.execute("""
UPDATE users
SET email_new = email,
email_verified_new = email_verified
""")
def phase3_contract():
"""Remove old columns (in a future release)."""
op.drop_column('users', 'email')
op.drop_column('users', 'email_verified')
op.rename_column('users', 'email_new', 'email')
op.rename_column('users', 'email_verified_new', 'email_verified')
Expected output: Expand-contract pattern adds new columns without removing old ones. The application is updated to use new columns. Old columns are removed in a later release. This allows safe rollback at any phase.
Common Mistakes
1. Running Migrations in the Same Stage as Application Deploy
Migrations and application deploy should be separate stages. If the deploy fails after migration, you need a rollback. Separate stages allow independent failure handling.
2. No Manual Approval Gate for Production
Auto-running migrations on production without approval is risky. Require manual approval before production migration execution. Use environment protection rules in CI/CD.
3. Not Testing Migrations in CI
Merging migration PRs without testing in CI causes production failures. Run migration tests in CI: apply from scratch, verify schema, test rollback. Gate the merge on test pass.
4. Ignoring Migration Failures in Pipeline
A failed migration that is ignored leaves the database in an unknown state. Fail the pipeline on migration failure. Do not proceed with deployment. Investigate and fix before retrying.
5. No Rollback Automation
A deployment pipeline without automated rollback requires manual intervention during incidents. Implement automated rollback triggered by verification failure. Test the rollback path regularly.
6. Different Migration Order Across Environments
Migrations should be applied in the same order across all environments. Use the same migration tool and configuration. Verify migration state consistency across environments.
Practice Questions
1. Why should migrations be a separate stage in CI/CD?
Separate stages allow independent failure handling. If migrations fail, the deployment is skipped. If verification fails after deploy, rollback reverts both schema and application. Stages provide clear failure boundaries.
2. What is the expand-contract pattern?
A safe deployment strategy for breaking schema changes. Phase 1 adds new columns alongside old ones. Phase 2 migrates data. Phase 3 removes old columns in a later release. Rollback is possible at any phase.
3. When should you require manual approval for migrations?
Production migrations should require manual approval. Development and staging can auto-run. Manual approval prevents accidental production schema changes and ensures a human reviews the migration plan.
4. How do you verify migration success after deployment?
Run health checks that verify schema correctness. Check that expected tables, columns, and constraints exist. Run smoke tests that exercise the new schema. Verify application starts without errors.
Challenge
Build a complete CI/CD migration pipeline that: runs migration tests in CI on every PR (fresh database, apply, verify, rollback), deploys with separate migrate/deploy/verify stages, requires manual approval for production migrations, takes a database backup before production migration, runs health checks after migration, triggers automated rollback on verification failure, and notifies the team via Slack with migration status.
FAQ
Mini Project: CI/CD Migration Pipeline
Build a complete CI/CD pipeline for database migrations: CI pipeline that tests migrations against a fresh database (apply all, verify schema, test rollback, run app tests), CD pipeline with separate migrate/deploy/verify stages, manual approval gate for production, automated backup before production migration, health check verification after migration, automated rollback on failure, and Slack notifications for migration status.
What's Next
Now that you understand CI/CD for migrations, learn about Zero-Downtime Migrations for schema changes that do not affect application availability.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro