Multi-Stage Deployments in Azure Pipelines

What are Multi-Stage Deployments?

Multi-stage deployments organize CI/CD pipelines into distinct phases, such as build, test, and deploy across multiple environments. Each stage represents a logical boundary in the software delivery process, enabling controlled progression from code commit to production deployment.

This approach ensures code quality through automated gates while providing visibility into the deployment pipeline status.


Why Multi-Stage Pipelines?


Multi-Stage Pipeline Architecture

graph LR A["Source Code"] -->B["Build Stage"] B -->C["Test Stage"] C -->D["Deploy Dev"] C -->E["Deploy Staging"] E -->F{Approval
Gate} F -->|Approved| G["Deploy Production"] F -->|Rejected| H["Pipeline Stopped"] style B fill:#e1f5ff style C fill:#fff3e0 style D fill:#e8f5e9 style E fill:#e8f5e9 style G fill:#ccffcc style H fill:#ffcccc

Basic Multi-Stage Pipeline

trigger: - main stages: - stage: Build displayName: 'Build Application' jobs: - job: BuildJob pool: vmImage: 'ubuntu-latest' steps: - script: echo "Building application" - script: dotnet build --configuration Release - task: PublishBuildArtifacts@1 inputs: pathToPublish: '$(Build.ArtifactStagingDirectory)' artifactName: 'drop' - stage: Test displayName: 'Run Tests' dependsOn: Build jobs: - job: TestJob steps: - script: echo "Running tests" - script: dotnet test - stage: DeployDev displayName: 'Deploy to Development' dependsOn: Test jobs: - deployment: DeployDev environment: development strategy: runOnce: deploy: steps: - script: echo "Deploying to dev" - stage: DeployProd displayName: 'Deploy to Production' dependsOn: DeployDev jobs: - deployment: DeployProd environment: production strategy: runOnce: deploy: steps: - script: echo "Deploying to production"

Stage Dependencies

Sequential Stages

stages: - stage: Build jobs: - job: BuildJob steps: - script: echo "Building" - stage: Test dependsOn: Build jobs: - job: TestJob steps: - script: echo "Testing" - stage: Deploy dependsOn: Test jobs: - job: DeployJob steps: - script: echo "Deploying"

Parallel Stages

stages: - stage: Build jobs: - job: BuildJob steps: - script: echo "Building" - stage: TestUnit dependsOn: Build jobs: - job: UnitTests steps: - script: echo "Unit tests" - stage: TestIntegration dependsOn: Build jobs: - job: IntegrationTests steps: - script: echo "Integration tests" - stage: Deploy dependsOn: - TestUnit - TestIntegration jobs: - job: DeployJob steps: - script: echo "Deploying"

Parallel Stage Execution

graph TD A["Build Stage"] -->B["Unit Tests"] A -->C["Integration Tests"] A -->D["Security Scan"] B -->E["Deploy Stage"] C -->E D -->E style A fill:#e1f5ff style B fill:#fff3e0 style C fill:#fff3e0 style D fill:#fff3e0 style E fill:#e8f5e9

Environments and Approvals

Environments provide deployment tracking, approvals, and security controls.

Creating Environments

Navigate to Pipelines > Environments in Azure DevOps to create and configure environments.

Deployment with Environment

stages: - stage: DeployProduction jobs: - deployment: DeployProd displayName: 'Deploy to Production' environment: production pool: vmImage: 'ubuntu-latest' strategy: runOnce: deploy: steps: - download: current artifact: drop - task: AzureWebApp@1 inputs: azureSubscription: 'AzureConnection' appName: 'myapp-prod' package: '$(Pipeline.Workspace)/drop/**/*.zip'

Manual Approval Gates

Configure approvals in environment settings:


Approval Flow

sequenceDiagram participant Pipeline as Pipeline participant Env as Environment participant Approver as Approver participant Deploy as Deployment Pipeline->>Env: Request deployment Env->>Approver: Send approval request Approver->>Approver: Review changes Approver->>Env: Approve/Reject alt Approved Env->>Deploy: Start deployment Deploy->>Deploy: Execute steps else Rejected Env->>Pipeline: Stop pipeline end

Deployment Strategies

RunOnce Strategy

Simple deployment that runs once.

strategy: runOnce: preDeploy: steps: - script: echo "Pre-deployment tasks" deploy: steps: - script: echo "Deployment tasks" routeTraffic: steps: - script: echo "Route traffic to new version" postRouteTraffic: steps: - script: echo "Post-deployment validation" on: failure: steps: - script: echo "Rollback on failure" success: steps: - script: echo "Cleanup on success"

Rolling Strategy

Gradual deployment across multiple instances.

strategy: rolling: maxParallel: 2 preDeploy: steps: - script: echo "Pre-deploy validation" deploy: steps: - script: echo "Deploy to instance" postRouteTraffic: steps: - script: echo "Health check" on: failure: steps: - script: echo "Rollback instance"

Canary Strategy

Gradually increase traffic to new version.

strategy: canary: increments: [10, 25, 50, 100] preDeploy: steps: - script: echo "Pre-deploy tasks" deploy: steps: - script: echo "Deploy canary version" routeTraffic: steps: - script: echo "Route $(strategy.increment)% traffic" postRouteTraffic: steps: - script: echo "Monitor metrics" - script: sleep 300 # Wait 5 minutes on: failure: steps: - script: echo "Rollback canary"

Deployment Strategy Comparison

graph TD A["Deployment Strategies"] -->B["RunOnce
All at once"] A -->C["Rolling
Gradual instance update"] A -->D["Canary
Gradual traffic shift"] B -->E["Fast deployment
Higher risk"] C -->F["Controlled rollout
Medium risk"] D -->G["Safest approach
Slower rollout"] style B fill:#ffcccc style C fill:#fff3e0 style D fill:#ccffcc

Conditional Deployments

Deploy to specific environments based on conditions.

stages: - stage: Build jobs: - job: BuildJob steps: - script: dotnet build - stage: DeployDev displayName: 'Deploy to Development' dependsOn: Build condition: | and( succeeded(), or( eq(variables['Build.SourceBranch'], 'refs/heads/develop'), eq(variables['Build.Reason'], 'PullRequest') ) ) jobs: - deployment: DeployDev environment: development strategy: runOnce: deploy: steps: - script: echo "Deploy to dev" - stage: DeployStaging displayName: 'Deploy to Staging' dependsOn: Build condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main')) jobs: - deployment: DeployStaging environment: staging strategy: runOnce: deploy: steps: - script: echo "Deploy to staging" - stage: DeployProduction displayName: 'Deploy to Production' dependsOn: DeployStaging condition: succeeded() jobs: - deployment: DeployProduction environment: production strategy: runOnce: deploy: steps: - script: echo "Deploy to production"

Artifact Management

Publishing Artifacts

stages: - stage: Build jobs: - job: BuildJob steps: - task: DotNetCoreCLI@2 displayName: 'Build and publish' inputs: command: 'publish' publishWebProjects: true arguments: '--configuration Release --output $(Build.ArtifactStagingDirectory)' - task: PublishBuildArtifacts@1 displayName: 'Publish artifacts' inputs: pathToPublish: '$(Build.ArtifactStagingDirectory)' artifactName: 'webapp' publishLocation: 'Container'

Downloading Artifacts

stages: - stage: Deploy jobs: - deployment: DeployJob environment: production strategy: runOnce: deploy: steps: - download: current artifact: webapp - script: | echo "Artifact location: $(Pipeline.Workspace)/webapp" ls -la $(Pipeline.Workspace)/webapp

Stage Variables

stages: - stage: DeployDev variables: environmentName: 'development' appServiceName: 'myapp-dev' azureSubscription: 'AzureDev' jobs: - deployment: Deploy environment: $(environmentName) strategy: runOnce: deploy: steps: - task: AzureWebApp@1 inputs: azureSubscription: $(azureSubscription) appName: $(appServiceName) - stage: DeployProd variables: environmentName: 'production' appServiceName: 'myapp-prod' azureSubscription: 'AzureProd' jobs: - deployment: Deploy environment: $(environmentName) strategy: runOnce: deploy: steps: - task: AzureWebApp@1 inputs: azureSubscription: $(azureSubscription) appName: $(appServiceName)

Environment Checks

Configure automated checks for environments.

Branch Protection

Restrict deployments to specific branches:

# Configure in Environment > Approvals and checks # Add "Branch control" check # Specify allowed branches: refs/heads/main, refs/heads/release/*

Business Hours

Deploy only during business hours:

# Configure in Environment > Approvals and checks # Add "Business hours" check # Set allowed time window: Monday-Friday, 9 AM - 5 PM

Required Template

Enforce pipeline templates for deployments:

# Configure in Environment > Approvals and checks # Add "Required template" check # Specify template repository and path

Real-World Multi-Environment Pipeline

trigger: branches: include: - main - develop - release/* variables: - group: 'shared-variables' - name: buildConfiguration value: 'Release' stages: - stage: Build displayName: 'Build and Test' jobs: - job: BuildJob pool: vmImage: 'ubuntu-latest' steps: - task: UseDotNet@2 displayName: 'Install .NET SDK' inputs: version: '7.x' - task: DotNetCoreCLI@2 displayName: 'Restore packages' inputs: command: 'restore' - task: DotNetCoreCLI@2 displayName: 'Build' inputs: command: 'build' arguments: '--configuration $(buildConfiguration) --no-restore' - task: DotNetCoreCLI@2 displayName: 'Run unit tests' inputs: command: 'test' arguments: '--configuration $(buildConfiguration) --no-build --filter Category=Unit' - task: DotNetCoreCLI@2 displayName: 'Publish' inputs: command: 'publish' publishWebProjects: true arguments: '--configuration $(buildConfiguration) --output $(Build.ArtifactStagingDirectory)' - task: PublishBuildArtifacts@1 displayName: 'Publish artifacts' inputs: pathToPublish: '$(Build.ArtifactStagingDirectory)' artifactName: 'webapp' - stage: SecurityScan displayName: 'Security Scanning' dependsOn: Build jobs: - job: SecurityJob steps: - script: echo "Running security scan" - script: echo "SAST and dependency scanning" - stage: DeployDev displayName: 'Deploy to Development' dependsOn: - Build - SecurityScan condition: | and( succeeded(), or( eq(variables['Build.SourceBranch'], 'refs/heads/develop'), startsWith(variables['Build.SourceBranch'], 'refs/heads/feature/') ) ) variables: environmentName: 'development' appServiceName: 'myapp-dev' jobs: - deployment: DeployDevJob environment: $(environmentName) pool: vmImage: 'ubuntu-latest' strategy: runOnce: deploy: steps: - download: current artifact: webapp - task: AzureWebApp@1 displayName: 'Deploy to Azure Web App' inputs: azureSubscription: 'AzureDev' appName: $(appServiceName) package: '$(Pipeline.Workspace)/webapp/**/*.zip' - task: PowerShell@2 displayName: 'Smoke test' inputs: targetType: 'inline' script: | $url = "https://$(appServiceName).azurewebsites.net/health" $response = Invoke-WebRequest -Uri $url -UseBasicParsing if ($response.StatusCode -ne 200) { throw "Health check failed with status $($response.StatusCode)" } Write-Host "Health check passed" - stage: DeployStaging displayName: 'Deploy to Staging' dependsOn: - Build - SecurityScan condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main')) variables: environmentName: 'staging' appServiceName: 'myapp-staging' jobs: - deployment: DeployStagingJob environment: $(environmentName) pool: vmImage: 'ubuntu-latest' strategy: runOnce: preDeploy: steps: - script: echo "Validating pre-deployment conditions" deploy: steps: - download: current artifact: webapp - task: AzureWebApp@1 inputs: azureSubscription: 'AzureStaging' appName: $(appServiceName) package: '$(Pipeline.Workspace)/webapp/**/*.zip' postRouteTraffic: steps: - task: PowerShell@2 displayName: 'Run smoke tests' inputs: targetType: 'inline' script: | Write-Host "Running smoke tests against staging" # Add smoke test logic here on: failure: steps: - script: echo "Deployment failed, sending alerts" success: steps: - script: echo "Deployment successful" - stage: IntegrationTests displayName: 'Integration Tests' dependsOn: DeployStaging condition: succeeded() jobs: - job: IntegrationTestsJob pool: vmImage: 'ubuntu-latest' steps: - task: DotNetCoreCLI@2 displayName: 'Run integration tests' inputs: command: 'test' arguments: '--filter Category=Integration' env: TEST_URL: https://myapp-staging.azurewebsites.net - stage: DeployProduction displayName: 'Deploy to Production' dependsOn: - DeployStaging - IntegrationTests condition: succeeded() variables: environmentName: 'production' appServiceName: 'myapp-prod' jobs: - deployment: DeployProductionJob environment: $(environmentName) pool: vmImage: 'ubuntu-latest' strategy: canary: increments: [25, 50, 100] preDeploy: steps: - script: echo "Preparing production deployment" - script: echo "Creating deployment backup" deploy: steps: - download: current artifact: webapp - task: AzureWebApp@1 inputs: azureSubscription: 'AzureProduction' appName: $(appServiceName) package: '$(Pipeline.Workspace)/webapp/**/*.zip' deploymentMethod: 'zipDeploy' routeTraffic: steps: - script: echo "Routing $(strategy.increment)% traffic to new version" postRouteTraffic: steps: - task: PowerShell@2 displayName: 'Monitor metrics' inputs: targetType: 'inline' script: | Write-Host "Monitoring application metrics" Write-Host "Current traffic: $(strategy.increment)%" Start-Sleep -Seconds 300 # Wait 5 minutes - task: PowerShell@2 displayName: 'Validate deployment' inputs: targetType: 'inline' script: | $url = "https://$(appServiceName).azurewebsites.net/health" $response = Invoke-WebRequest -Uri $url if ($response.StatusCode -ne 200) { throw "Health check failed" } on: failure: steps: - script: echo "Rolling back deployment" - script: echo "Sending failure notifications" success: steps: - script: echo "Deployment completed successfully" - script: echo "Sending success notifications"

Monitoring and Troubleshooting

View Stage Status

Navigate to Pipelines > Select run > View stages with status indicators.

Logs and Diagnostics

steps: - script: | echo "##[debug]Debug message" echo "##[warning]Warning message" echo "##[error]Error message"

Pipeline Analytics

Azure DevOps provides analytics on:


Key Takeaways

Next Steps: Implement multi-stage pipelines with approval gates, configure environment-specific variables, and adopt canary or rolling deployment strategies for production releases.