DEV Community

Daniel Jonathan
Daniel Jonathan

Posted on

Automating Database Migrations with CI/CD

Automated migrations bring your database into the same repeatable, auditable, and safe process your application code already enjoys. In this guide, you’ll learn how to integrate FluentMigrator cleanly into your Azure CI/CD pipeline.


The Goal

Automate database migrations so they run automatically in your deployment pipeline.

When you deploy:

  1. Infrastructure creates/updates Azure SQL Database
  2. Migrations run automatically
  3. API deploys (only if migrations succeed)

Zero manual steps. Zero forgotten migrations. Zero production surprises.


The Pattern: Infrastructure + Migrations

Here's how it works conceptually:

┌─────────────────────────────────────────────────────────────┐
│  Infrastructure Pipeline (sql-infra.yml)                    │
│  • Runs independently when infrastructure changes           │
│  • Deploys: Azure SQL Server + Database (Bicep/ARM)         │
│  • Triggered by: Changes to AzIaC/AzSqlIaC/*                │
└─────────────────────────────────────────────────────────────┘

                        SEPARATE

┌─────────────────────────────────────────────────────────────┐
│  Migration Pipeline (sql-migrations.yml)                    │
│  • Manual trigger only                                      │
│  • Gets connection string from Variable Group               │
│  • Runs FluentMigrator migrations                           │
│  • That's it! Simple and clean.                             │
└─────────────────────────────────────────────────────────────┘
Enter fullscreen mode Exit fullscreen mode

Key concepts:

  • Pipelines are completely independent
  • Connection strings in Variable Groups
  • Firewall rules configured at SQL Server level (not in pipeline)

The Migration Pipeline (Simplified!)

File: Pipelines/sql-migrations.yml

name: 'ProductDB_Migrations_$(Date:yyyyMMdd)$(Rev:.r)'

trigger: none  # Manual only

pool:
  vmImage: ubuntu-latest

parameters:
- name: environment
  displayName: Environment to deploy
  type: string
  default: dev
  values:
  - dev
  - prod

variables:
  projectPath: 'src/ProductWebAPI.Database/ProductWebAPI.Database.csproj'

stages:
- stage: Migrate
  displayName: 'Run Database Migrations'
  variables:
  - group: 'ProductAPI-${{ parameters.environment }}'  # Has sqlConnectionString

  jobs:
  - job: RunMigrations
    displayName: 'Execute FluentMigrator'
    steps:
    - checkout: self

    - task: UseDotNet@2
      displayName: 'Install .NET 8.0'
      inputs:
        version: '8.x'

    - task: DotNetCoreCLI@2
      displayName: 'Restore NuGet Packages'
      inputs:
        command: 'restore'
        projects: '$(projectPath)'

    - task: DotNetCoreCLI@2
      displayName: 'Build Migration Project'
      inputs:
        command: 'build'
        projects: '$(projectPath)'
        arguments: '--configuration Release --no-restore'

    # Run migrations - that's it!
    - task: DotNetCoreCLI@2
      displayName: 'Run Migrations'
      inputs:
        command: 'run'
        projects: '$(projectPath)'
        arguments: '--configuration Release'
      env:
        ConnectionStrings__DefaultConnection: $(sqlConnectionString)
Enter fullscreen mode Exit fullscreen mode

What it does:

  1. ✅ Build migration project
  2. ✅ Run migrations with connection string from Variable Group

That's it! No firewall management, no SQL Server discovery. Simple and clean.


Variable Groups Setup

Create variable groups in Azure DevOps:

Pipelines → Library → Variable groups → + Variable group

Name: ProductAPI-dev
Variables:
  - sqlConnectionString (secret)
    Value: Server=tcp:yourserver.database.windows.net,1433;Database=ProductDB;User ID=app-user;Password=xxx;Encrypt=True;

Name: ProductAPI-prod
Variables:
  - sqlConnectionString (secret)
    Value: Server=tcp:prod-server.database.windows.net,1433;Database=ProductDB;User ID=app-user;Password=xxx;Encrypt=True;
Enter fullscreen mode Exit fullscreen mode

How to Use

First Time Setup

# 1. Infrastructure team runs (once)
Run pipeline: sql-infra.yml
  Environment: dev
  → Creates Azure SQL Server + Database

# 2. Add connection string to Variable Group
Pipelines → Library → Variable groups
  → ProductAPI-dev
  → sqlConnectionString: Server=...;Database=ProductDB;...

# 3. Run migrations
Run pipeline: sql-migrations.yml
  Environment: dev
Enter fullscreen mode Exit fullscreen mode

Daily Usage (Schema Changes)

# Developer adds new migration M004_AddOrdersTable.cs
# Then triggers migration pipeline:

Run pipeline: sql-migrations.yml
  Environment: dev
  → Applies only new migrations (M004)
Enter fullscreen mode Exit fullscreen mode

Real-World Deployment Flow

1. Developer commits new migration M004_AddOrdersTable.cs
    ↓
2. Manually trigger sql-migrations.yml (Dev)
    ↓
3. Build migration project
    • Restore NuGet packages
    • Build ProductWebAPI.Database
    ↓
4. Run migrations
    • Connection string from Variable Group: ProductAPI-dev
    • FluentMigrator checks VersionInfo table
    • 20251128001: CreateInitialSchema (already applied, skipped)
    • 20251128002: SeedInitialData (already applied, skipped)
    • 20251128003: CreateViews (already applied, skipped)
    • 20251129001: AddOrdersTable ✓ (NEW - applied!)
    ↓
✅ Dev database updated!
Enter fullscreen mode Exit fullscreen mode

Best Practices

1. Pipeline Independence

  • ✅ Infrastructure and migrations are separate pipelines
  • ✅ Connection strings stored in Variable Groups, not passed between pipelines
  • ✅ Each pipeline runs independently

2. Infrastructure Pipeline (Conceptual)

  • ✅ Runs rarely (only when infrastructure changes)
  • ✅ Managed by infrastructure/DevOps team
  • ✅ Deploys Azure SQL Server + Database via Bicep

3. Migration Pipeline (Your Focus)

  • ✅ Manual trigger only (trigger: none)
  • ✅ Gets connection string from Variable Group
  • ✅ Firewall configured at SQL Server level (not in pipeline)
  • ✅ Simple: Build → Run

4. Safety & Infrastructure

  • ✅ Never auto-trigger migrations to production
  • ✅ Test in dev first
  • ✅ Firewall rules: Configure at SQL Server level (allow Azure DevOps service tag or agent IPs)

Key Takeaways

Independent pipelines:

  • Infrastructure pipeline runs separately (when infrastructure changes)
  • Migration pipeline runs independently (when schema changes)
  • Connection strings in Variable Groups, not shared between pipelines

Migration pipeline features:

  • Super simple: just 2 steps (Build → Run)
  • Manual trigger only (safety first)
  • Connection string from Variable Group

Simple workflow:

1. Write migration (M004_AddOrdersTable.cs)
2. Trigger pipeline (sql-migrations.yml)
3. Choose environment (dev/prod)
4. Done! ✅
Enter fullscreen mode Exit fullscreen mode

The result:

  • ✅ Schema changes are code-reviewed
  • ✅ Deployments are repeatable
  • ✅ VersionInfo prevents duplicates
  • ✅ Safe, auditable, automated

Happy automating!

Top comments (0)