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?
- Separation of Concerns - Distinct stages for build, test, and deployment
- Environment Progression - Gradual rollout from dev to staging to production
- Approval Gates - Manual or automated approvals between stages
- Parallel Execution - Run independent stages simultaneously
- Visibility - Clear view of pipeline progress and status
- Rollback Capability - Identify and revert problematic deployments
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:
- Navigate to Environments > Select environment
- Click on "Approvals and checks"
- Add "Approvals" check
- Specify required approvers
- Set timeout and approval instructions
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:
- Stage success rates
- Average stage duration
- Failure patterns
- Deployment frequency
Key Takeaways
- Multi-stage pipelines organize CI/CD into logical phases like build, test, and deploy
- Stages can run sequentially or in parallel using dependency declarations
- Environments provide deployment tracking, approvals, and security controls
- Manual approval gates enable controlled production deployments
- Deployment strategies include runOnce, rolling, and canary approaches
- Conditional stages deploy to specific environments based on branch or trigger
- Artifacts transfer build outputs between stages efficiently
- Environment checks enforce business hours, branch protection, and required templates
- Canary deployments gradually shift traffic to minimize risk
- Stage variables enable environment-specific configuration
- Pipeline visibility shows clear progress through deployment phases
Next Steps: Implement multi-stage pipelines with approval gates, configure environment-specific variables, and adopt canary or rolling deployment strategies for production releases.