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:
- Infrastructure creates/updates Azure SQL Database
- Migrations run automatically
- 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. │
└─────────────────────────────────────────────────────────────┘
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)
What it does:
- ✅ Build migration project
- ✅ 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;
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
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)
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!
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! ✅
The result:
- ✅ Schema changes are code-reviewed
- ✅ Deployments are repeatable
- ✅ VersionInfo prevents duplicates
- ✅ Safe, auditable, automated
Happy automating!
Top comments (0)