Pular para o conteúdo principal

Desafio 29: Automação de deploy de banco de dados

Habilidades do exame mapeadas

  • Implementar um deploy que inclua tarefas de banco de dados

Cenário

Os deploys da API da Contoso Ltd frequentemente quebram porque as alterações no esquema do banco de dados não são coordenadas com as alterações no código. Na semana passada, um desenvolvedor implantou uma nova versão da API que esperava uma tabela CustomerPreferences que ainda não existia, causando erros 500 por 45 minutos até que um DBA executou manualmente a migração. A equipe precisa de um pipeline de deploy consciente do banco de dados que garanta que as alterações de esquema sejam aplicadas na ordem correta em relação aos deploys da aplicação.

Detalhes do ambiente:

  • Azure SQL Database: sql-contoso-prod.database.windows.net
  • Banco de dados: ContosoWebDB
  • Aplicação: .NET 8 Web API usando Entity Framework Core
  • Resource group: rg-contoso-data
  • Região: East US

Tarefa 1: Implementar migrações EF Core no pipeline CI/CD

Gerar uma migração localmente

# Add a new migration
dotnet ef migrations add AddCustomerPreferences \
--project src/ContosoApi \
--startup-project src/ContosoApi \
--output-dir Data/Migrations

# Generate an idempotent SQL script (recommended for CI/CD)
dotnet ef migrations script \
--project src/ContosoApi \
--startup-project src/ContosoApi \
--idempotent \
--output migrations.sql

Workflow do GitHub Actions com migrações EF Core

Crie .github/workflows/deploy-with-migrations.yml:

name: Deploy with database migrations

on:
push:
branches: [main]
paths:
- 'src/ContosoApi/**'

env:
AZURE_WEBAPP_NAME: app-contoso-api
RESOURCE_GROUP: rg-contoso-data
SQL_SERVER: sql-contoso-prod.database.windows.net
SQL_DATABASE: ContosoWebDB
DOTNET_VERSION: '8.0.x'

jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: ${{ env.DOTNET_VERSION }}

- name: Install EF Core tools
run: dotnet tool install --global dotnet-ef

- name: Build application
run: |
dotnet restore src/ContosoApi/ContosoApi.csproj
dotnet publish src/ContosoApi/ContosoApi.csproj \
--configuration Release \
--output ./publish

- name: Generate idempotent migration script
run: |
dotnet ef migrations script \
--project src/ContosoApi \
--startup-project src/ContosoApi \
--idempotent \
--output ./publish/migrations.sql

- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
name: deploy-package
path: ./publish

deploy-database:
runs-on: ubuntu-latest
needs: build
environment: production
steps:
- name: Download artifacts
uses: actions/download-artifact@v4
with:
name: deploy-package
path: ./publish

- name: Login to Azure
uses: azure/login@v2
with:
creds: ${{ secrets.AZURE_CREDENTIALS }}

- name: Run database migrations
uses: azure/sql-action@v2.3
with:
connection-string: ${{ secrets.SQL_CONNECTION_STRING }}
path: ./publish/migrations.sql

- name: Verify migration success
run: |
az sql db show \
--name ${{ env.SQL_DATABASE }} \
--server sql-contoso-prod \
--resource-group ${{ env.RESOURCE_GROUP }} \
--query "status" -o tsv

deploy-application:
runs-on: ubuntu-latest
needs: deploy-database
environment: production
steps:
- name: Download artifacts
uses: actions/download-artifact@v4
with:
name: deploy-package
path: ./publish

- name: Login to Azure
uses: azure/login@v2
with:
creds: ${{ secrets.AZURE_CREDENTIALS }}

- name: Deploy to App Service
uses: azure/webapps-deploy@v3
with:
app-name: ${{ env.AZURE_WEBAPP_NAME }}
slot-name: staging
package: ./publish

- name: Validate staging
run: |
STAGING_URL="https://${{ env.AZURE_WEBAPP_NAME }}-staging.azurewebsites.net/health"
STATUS=$(curl -s -o /dev/null -w "%{http_code}" "$STAGING_URL")
if [ "$STATUS" != "200" ]; then
echo "Staging validation failed with status $STATUS"
exit 1
fi

- name: Swap to production
run: |
az webapp deployment slot swap \
--name ${{ env.AZURE_WEBAPP_NAME }} \
--resource-group ${{ env.RESOURCE_GROUP }} \
--slot staging \
--target-slot production

Tarefa 2: Implementar migrações Flyway para SQL Server

Estrutura do projeto Flyway

db/
flyway.toml
sql/
V001__Create_customers_table.sql
V002__Add_customer_preferences.sql
V003__Add_orders_indexes.sql
R__Create_reporting_views.sql

Configuração do Flyway (flyway.toml)

[environments.production]
url = "jdbc:sqlserver://sql-contoso-prod.database.windows.net:1433;database=ContosoWebDB;encrypt=true;trustServerCertificate=false"
user = "${FLYWAY_USER}"
password = "${FLYWAY_PASSWORD}"
schemas = ["dbo"]

[flyway]
locations = ["filesystem:sql"]
baselineOnMigrate = true
outOfOrder = false
validateOnMigrate = true

Arquivos de migração de exemplo

V001__Create_customers_table.sql:

CREATE TABLE dbo.Customers (
Id UNIQUEIDENTIFIER PRIMARY KEY DEFAULT NEWSEQUENTIALID(),
Email NVARCHAR(256) NOT NULL,
FullName NVARCHAR(512) NOT NULL,
CreatedAt DATETIME2 NOT NULL DEFAULT GETUTCDATE(),
UpdatedAt DATETIME2 NOT NULL DEFAULT GETUTCDATE(),
CONSTRAINT UQ_Customers_Email UNIQUE (Email)
);

CREATE INDEX IX_Customers_Email ON dbo.Customers (Email);

V002__Add_customer_preferences.sql:

CREATE TABLE dbo.CustomerPreferences (
Id UNIQUEIDENTIFIER PRIMARY KEY DEFAULT NEWSEQUENTIALID(),
CustomerId UNIQUEIDENTIFIER NOT NULL,
PreferenceKey NVARCHAR(128) NOT NULL,
PreferenceValue NVARCHAR(MAX),
CONSTRAINT FK_CustomerPreferences_Customers
FOREIGN KEY (CustomerId) REFERENCES dbo.Customers(Id)
);

CREATE INDEX IX_CustomerPreferences_CustomerId
ON dbo.CustomerPreferences (CustomerId);

Flyway no GitHub Actions

run-flyway-migrations:
runs-on: ubuntu-latest
needs: build
environment: production
steps:
- uses: actions/checkout@v4

- name: Run Flyway migrations
uses: flyway/flyway-action@v1
with:
url: jdbc:sqlserver://sql-contoso-prod.database.windows.net:1433;database=ContosoWebDB;encrypt=true
user: ${{ secrets.SQL_ADMIN_USER }}
password: ${{ secrets.SQL_ADMIN_PASSWORD }}
locations: filesystem:./db/sql
command: migrate

- name: Verify migration info
uses: flyway/flyway-action@v1
with:
url: jdbc:sqlserver://sql-contoso-prod.database.windows.net:1433;database=ContosoWebDB;encrypt=true
user: ${{ secrets.SQL_ADMIN_USER }}
password: ${{ secrets.SQL_ADMIN_PASSWORD }}
locations: filesystem:./db/sql
command: info

Tarefa 3: Ordenação de deploy de banco de dados (esquema primeiro, depois aplicação)

Diagrama de sequência de deploy

1. Run database migrations (additive/non-breaking)
2. Verify migrations applied successfully
3. Deploy new application version to staging slot
4. Validate staging with new schema
5. Swap staging to production
6. (Optional) Remove deprecated columns/tables in next release cycle

Azure Pipelines com ordenação estrita

trigger:
branches:
include:
- main

pool:
vmImage: 'ubuntu-latest'

variables:
azureSubscription: 'contoso-production-connection'
resourceGroup: 'rg-contoso-data'
sqlServer: 'sql-contoso-prod'
sqlDatabase: 'ContosoWebDB'
appName: 'app-contoso-api'

stages:
- stage: Build
jobs:
- job: BuildApp
steps:
- task: UseDotNet@2
inputs:
packageType: 'sdk'
version: '8.0.x'

- script: |
dotnet tool install --global dotnet-ef
dotnet restore src/ContosoApi/ContosoApi.csproj
dotnet publish src/ContosoApi/ContosoApi.csproj -c Release -o $(Build.ArtifactStagingDirectory)/app
dotnet ef migrations script --project src/ContosoApi --startup-project src/ContosoApi --idempotent --output $(Build.ArtifactStagingDirectory)/migrations.sql
displayName: 'Build app and generate migration script'

- task: PublishBuildArtifacts@1
inputs:
PathtoPublish: '$(Build.ArtifactStagingDirectory)'
ArtifactName: 'drop'

- stage: DeployDatabase
displayName: 'Deploy database schema'
dependsOn: Build
jobs:
- deployment: MigrateDatabase
environment: 'production-db'
strategy:
runOnce:
deploy:
steps:
- task: SqlAzureDacpacDeployment@1
displayName: 'Run SQL migrations'
inputs:
azureSubscription: $(azureSubscription)
authenticationType: 'server'
serverName: '$(sqlServer).database.windows.net'
databaseName: $(sqlDatabase)
sqlUsername: '$(SQL_ADMIN_USER)'
sqlPassword: '$(SQL_ADMIN_PASSWORD)'
deployType: 'SqlTask'
sqlFile: '$(Pipeline.Workspace)/drop/migrations.sql'

- stage: DeployApplication
displayName: 'Deploy application'
dependsOn: DeployDatabase
jobs:
- deployment: DeployApp
environment: 'production'
strategy:
runOnce:
deploy:
steps:
- task: AzureWebApp@1
inputs:
azureSubscription: $(azureSubscription)
appType: 'webAppLinux'
appName: $(appName)
deployToSlotOrASE: true
resourceGroupName: $(resourceGroup)
slotName: 'staging'
package: '$(Pipeline.Workspace)/drop/app'

- task: AzureAppServiceManage@0
displayName: 'Swap to production'
inputs:
azureSubscription: $(azureSubscription)
action: 'Swap Slots'
webAppName: $(appName)
resourceGroupName: $(resourceGroup)
sourceSlot: 'staging'

Tarefa 4: Estratégias de rollback para alterações de banco de dados

Estratégia 1: Forward-fix com migração compensatória

# If V003 introduced a bug, create V004 to fix it (preferred in production)
dotnet ef migrations add FixOrdersIndexes \
--project src/ContosoApi \
--startup-project src/ContosoApi

Estratégia 2: Restauração point-in-time para falhas catastróficas

# Restore database to a point before the migration
az sql db restore \
--dest-name ContosoWebDB-restored \
--resource-group $RESOURCE_GROUP \
--server sql-contoso-prod \
--name ContosoWebDB \
--time "2025-01-15T10:00:00Z"

# After verification, swap the restored database
az sql db rename \
--name ContosoWebDB \
--resource-group $RESOURCE_GROUP \
--server sql-contoso-prod \
--new-name ContosoWebDB-old

az sql db rename \
--name ContosoWebDB-restored \
--resource-group $RESOURCE_GROUP \
--server sql-contoso-prod \
--new-name ContosoWebDB

Estratégia 3: Backup pré-migração

backup-before-migration:
runs-on: ubuntu-latest
steps:
- name: Login to Azure
uses: azure/login@v2
with:
creds: ${{ secrets.AZURE_CREDENTIALS }}

- name: Create database copy as backup
run: |
TIMESTAMP=$(date +%Y%m%d%H%M%S)
az sql db copy \
--dest-name "ContosoWebDB-backup-${TIMESTAMP}" \
--resource-group ${{ env.RESOURCE_GROUP }} \
--server sql-contoso-prod \
--name ContosoWebDB \
--dest-server sql-contoso-prod
echo "Backup created: ContosoWebDB-backup-${TIMESTAMP}"

Tarefa 5: Blue-green com banco de dados (padrão expand-contract)

O padrão expand-contract permite migrações seguras de banco de dados em conjunto com deploys blue-green.

Fase 1: Expand (adicionar novo esquema, manter antigo)

-- V005__Expand_add_full_name_column.sql
-- Add new column (nullable, no breaking change)
ALTER TABLE dbo.Customers ADD FullName NVARCHAR(512) NULL;

-- Backfill data from existing columns
UPDATE dbo.Customers
SET FullName = FirstName + ' ' + LastName
WHERE FullName IS NULL;

Fase 2: Migrate (aplicação usa tanto o antigo quanto o novo)

// Application code handles both schemas during transition
public class Customer
{
public string? FirstName { get; set; } // Old column (still read)
public string? LastName { get; set; } // Old column (still read)
public string? FullName { get; set; } // New column (write here)

public string GetDisplayName()
{
// Prefer new column, fall back to old
return FullName ?? $"{FirstName} {LastName}";
}
}

Fase 3: Contract (remover esquema antigo na próxima versão)

-- V006__Contract_remove_name_columns.sql
-- Only run after ALL application instances use FullName
ALTER TABLE dbo.Customers DROP COLUMN FirstName;
ALTER TABLE dbo.Customers DROP COLUMN LastName;

-- Make the new column non-nullable
ALTER TABLE dbo.Customers ALTER COLUMN FullName NVARCHAR(512) NOT NULL;

Cronograma de deploy

VersãoAlteração no banco de dadosAlteração na aplicação
v2.1Expand: Adicionar coluna FullNameEscrever em ambas as colunas antiga e nova
v2.2NenhumaLer apenas da nova coluna, parar de escrever na antiga
v2.3Contract: Remover colunas antigasRemover referências às colunas antigas

Tarefa 6: Workflow do GitHub Actions com etapa de migração

Workflow completo demonstrando o padrão completo de deploy consciente do banco de dados:

name: Database-aware deployment

on:
push:
branches: [main]

env:
RESOURCE_GROUP: rg-contoso-data
APP_NAME: app-contoso-api
SQL_SERVER: sql-contoso-prod

jobs:
build:
runs-on: ubuntu-latest
outputs:
has_migrations: ${{ steps.check-migrations.outputs.has_migrations }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 2

- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: '8.0.x'

- name: Install EF Core tools
run: dotnet tool install --global dotnet-ef

- name: Check for new migrations
id: check-migrations
run: |
CHANGED=$(git diff --name-only HEAD~1 HEAD -- 'src/ContosoApi/Data/Migrations/')
if [ -n "$CHANGED" ]; then
echo "has_migrations=true" >> $GITHUB_OUTPUT
echo "New migrations detected: $CHANGED"
else
echo "has_migrations=false" >> $GITHUB_OUTPUT
echo "No new migrations"
fi

- name: Build and publish
run: |
dotnet publish src/ContosoApi/ContosoApi.csproj \
-c Release -o ./publish

- name: Generate migration script
if: steps.check-migrations.outputs.has_migrations == 'true'
run: |
dotnet ef migrations script \
--project src/ContosoApi \
--startup-project src/ContosoApi \
--idempotent \
--output ./publish/migrations.sql

- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
name: deploy-package
path: ./publish

migrate-database:
runs-on: ubuntu-latest
needs: build
if: needs.build.outputs.has_migrations == 'true'
environment: production
steps:
- name: Download artifacts
uses: actions/download-artifact@v4
with:
name: deploy-package
path: ./publish

- name: Login to Azure
uses: azure/login@v2
with:
creds: ${{ secrets.AZURE_CREDENTIALS }}

- name: Create pre-migration backup
run: |
TIMESTAMP=$(date +%Y%m%d%H%M%S)
az sql db copy \
--dest-name "ContosoWebDB-pre-migration-${TIMESTAMP}" \
--resource-group ${{ env.RESOURCE_GROUP }} \
--server ${{ env.SQL_SERVER }} \
--name ContosoWebDB \
--dest-server ${{ env.SQL_SERVER }}

- name: Apply migrations
uses: azure/sql-action@v2.3
with:
connection-string: ${{ secrets.SQL_CONNECTION_STRING }}
path: ./publish/migrations.sql

deploy-application:
runs-on: ubuntu-latest
needs: [build, migrate-database]
if: always() && needs.build.result == 'success' && (needs.migrate-database.result == 'success' || needs.migrate-database.result == 'skipped')
environment: production
steps:
- name: Download artifacts
uses: actions/download-artifact@v4
with:
name: deploy-package
path: ./publish

- name: Login to Azure
uses: azure/login@v2
with:
creds: ${{ secrets.AZURE_CREDENTIALS }}

- name: Deploy to staging
uses: azure/webapps-deploy@v3
with:
app-name: ${{ env.APP_NAME }}
slot-name: staging
package: ./publish

- name: Swap to production
run: |
az webapp deployment slot swap \
--name ${{ env.APP_NAME }} \
--resource-group ${{ env.RESOURCE_GROUP }} \
--slot staging \
--target-slot production

Tarefa 7: Azure Pipelines com task SqlAzureDacpacDeployment

Deploy baseado em DACPAC

stages:
- stage: DeployDatabase
displayName: 'Deploy database with DACPAC'
jobs:
- deployment: DatabaseDeploy
environment: 'production-db'
strategy:
runOnce:
deploy:
steps:
- task: SqlAzureDacpacDeployment@1
displayName: 'Deploy DACPAC to Azure SQL'
inputs:
azureSubscription: 'contoso-production-connection'
authenticationType: 'servicePrincipal'
serverName: 'sql-contoso-prod.database.windows.net'
databaseName: 'ContosoWebDB'
deployType: 'DacpacTask'
deploymentAction: 'Publish'
dacpacFile: '$(Pipeline.Workspace)/drop/ContosoApi.dacpac'
additionalArguments: '/p:BlockOnPossibleDataLoss=true /p:DropObjectsNotInSource=false'

- stage: DeploySqlScript
displayName: 'Deploy using SQL script'
jobs:
- deployment: SqlScriptDeploy
environment: 'production-db'
strategy:
runOnce:
deploy:
steps:
- task: SqlAzureDacpacDeployment@1
displayName: 'Run migration SQL script'
inputs:
azureSubscription: 'contoso-production-connection'
authenticationType: 'servicePrincipal'
serverName: 'sql-contoso-prod.database.windows.net'
databaseName: 'ContosoWebDB'
deployType: 'SqlTask'
sqlFile: '$(Pipeline.Workspace)/drop/migrations.sql'

Exercícios de quebra e conserto

Exercício 1: Migração falha por erro de permissão

Sintoma: A etapa azure/sql-action falha com The server principal is not able to access the database under the current security context.

Investigar:

# Check if the managed identity has access to the database
az sql db show \
--name ContosoWebDB \
--server sql-contoso-prod \
--resource-group $RESOURCE_GROUP \
--query "status"
Mostrar solução

Causa raiz: O service principal usado pelo pipeline não possui o role db_ddladmin no banco de dados de destino.

Correção:

-- Run as server admin to grant DDL permissions to the pipeline identity
CREATE USER [contoso-pipeline-sp] FROM EXTERNAL PROVIDER;
ALTER ROLE db_ddladmin ADD MEMBER [contoso-pipeline-sp];
ALTER ROLE db_datareader ADD MEMBER [contoso-pipeline-sp];
ALTER ROLE db_datawriter ADD MEMBER [contoso-pipeline-sp];

Exercício 2: Aplicação implantada antes da migração ser concluída

Sintoma: A aplicação lança SqlException: Invalid object name 'CustomerPreferences' porque o estágio de migração ainda estava em execução quando o deploy da aplicação iniciou.

Mostrar solução

Causa raiz: Os estágios do pipeline não tinham configuração adequada de dependsOn, permitindo que fossem executados em paralelo.

Correção: Garanta a ordenação estrita no pipeline:

stages:
- stage: DeployDatabase
dependsOn: Build
# ...

- stage: DeployApplication
dependsOn: DeployDatabase # Must wait for DB migration
condition: succeeded('DeployDatabase')
# ...

Exercício 3: Deploy de DACPAC bloqueado por perda de dados

Sintoma: O publish do DACPAC falha com Rows were detected. The schema update is terminating because data loss might occur.

Mostrar solução

Causa raiz: O DACPAC detecta que uma coluna sendo removida contém dados. A flag /p:BlockOnPossibleDataLoss=true impede a operação.

Correção: Use o padrão expand-contract em vez de remover colunas diretamente:

# Option 1: Override for this deployment (use with caution)
# /p:BlockOnPossibleDataLoss=false

# Option 2 (recommended): Use a pre/post deployment script to handle the data first
# Pre-script: migrate data to new column
# DACPAC: schema changes
# Post-script: cleanup (in next release)

Verificação de conhecimento

1. A Contoso implanta uma nova versão da API que requer uma nova tabela no banco de dados. O pipeline implanta a aplicação primeiro e depois executa as migrações. Os usuários veem erros 500 por 2 minutos. Qual é a ordem correta de deploy?

2. A Contoso usa o padrão expand-contract para uma alteração de esquema que renomeia uma coluna de FirstName + LastName para FullName. Quantas versões esse padrão normalmente requer?

3. Qual task do Azure Pipelines deve ser usada para implantar um script SQL no Azure SQL Database?

4. Um pipeline gera um script de migração idempotente do EF Core usando dotnet ef migrations script --idempotent. O que a flag --idempotent garante?


Limpeza

# Delete backup databases
az sql db list \
--server sql-contoso-prod \
--resource-group $RESOURCE_GROUP \
--query "[?contains(name, 'backup') || contains(name, 'pre-migration')].name" -o tsv | \
xargs -I {} az sql db delete \
--name {} \
--server sql-contoso-prod \
--resource-group $RESOURCE_GROUP \
--yes

# Delete the resource group
az group delete --name rg-contoso-data --yes --no-wait