Introduction
Continuous Integration and Continuous Deployment (CI/CD) are fundamental practices in modern software development, enabling teams to deliver high-quality applications faster and more reliably. Azure DevOps Pipelines provides a robust, cloud-native platform for automating build, test, and deployment workflows across any platform or cloud provider.
In this comprehensive guide, we'll explore how to create production-ready CI/CD pipelines using Azure DevOps, covering YAML-based pipeline definitions, deployment strategies, security best practices, and integration with testing frameworks. Whether you're deploying web applications, APIs, or containerized services, this guide will help you establish efficient automated workflows.
Prerequisites
Before diving into Azure DevOps Pipelines, ensure you have:
- Azure DevOps Organization: Create a free account at dev.azure.com
- Azure Subscription: Active subscription for deploying resources
- Source Repository: Git repository (Azure Repos, GitHub, or Bitbucket)
- Basic YAML Knowledge: Understanding of YAML syntax
- Application to Deploy: Sample application or existing codebase
- Azure CLI Installed: For local testing and troubleshooting
Recommended Tools:
- Visual Studio Code with Azure Pipelines extension
- Git for version control
- Docker Desktop (if working with containers)
Understanding Azure Pipelines Architecture
Core Concepts
Pipeline: Automated workflow defining build and deployment processes
Stage: Logical boundary in pipeline (e.g., Build, Test, Deploy)
Job: Collection of steps running on an agent
Step: Individual task (script, task template, or command)
Agent: Compute resource executing pipeline jobs
Artifact: Build output stored for deployment stages
Pipeline Types
Build Pipeline (CI): Compiles code, runs tests, produces artifacts
Release Pipeline (CD): Deploys artifacts to target environments
Multi-Stage Pipeline: Combines CI and CD in single YAML definition
Creating Your First YAML Pipeline
Basic Pipeline Structure
Create a file named azure-pipelines.yml in your repository root:
trigger:
branches:
include:
- main
- develop
pool:
vmImage: 'ubuntu-latest'
variables:
buildConfiguration: 'Release'
dotnetVersion: '8.x'
stages:
- stage: Build
displayName: 'Build Stage'
jobs:
- job: BuildJob
displayName: 'Build Application'
steps:
- task: UseDotNet@2
displayName: 'Install .NET SDK'
inputs:
version: $(dotnetVersion)
packageType: sdk
- task: DotNetCoreCLI@2
displayName: 'Restore NuGet Packages'
inputs:
command: 'restore'
projects: '**/*.csproj'
- task: DotNetCoreCLI@2
displayName: 'Build Project'
inputs:
command: 'build'
projects: '**/*.csproj'
arguments: '--configuration $(buildConfiguration)'
- task: DotNetCoreCLI@2
displayName: 'Run Unit Tests'
inputs:
command: 'test'
projects: '**/*Tests.csproj'
arguments: '--configuration $(buildConfiguration) --collect:"XPlat Code Coverage"'
- task: DotNetCoreCLI@2
displayName: 'Publish Application'
inputs:
command: 'publish'
publishWebProjects: true
arguments: '--configuration $(buildConfiguration) --output $(Build.ArtifactStagingDirectory)'
zipAfterPublish: true
- task: PublishBuildArtifacts@1
displayName: 'Publish Artifacts'
inputs:
PathtoPublish: '$(Build.ArtifactStagingDirectory)'
ArtifactName: 'drop'
publishLocation: 'Container'
Multi-Stage Pipeline with Deployment
Extend the pipeline to include deployment stages:
trigger:
branches:
include:
- main
pool:
vmImage: 'ubuntu-latest'
variables:
buildConfiguration: 'Release'
azureSubscription: 'MyAzureSubscription'
webAppName: 'my-webapp-prod'
stages:
- stage: Build
displayName: 'Build'
jobs:
- job: BuildJob
displayName: 'Build and Test'
steps:
- task: UseDotNet@2
inputs:
version: '8.x'
- script: |
dotnet restore
dotnet build --configuration $(buildConfiguration)
dotnet test --configuration $(buildConfiguration) --logger trx
displayName: 'Build and Test'
- task: DotNetCoreCLI@2
displayName: 'Publish'
inputs:
command: 'publish'
publishWebProjects: true
arguments: '--configuration $(buildConfiguration) --output $(Build.ArtifactStagingDirectory)'
- publish: $(Build.ArtifactStagingDirectory)
artifact: drop
- stage: DeployDev
displayName: 'Deploy to Development'
dependsOn: Build
condition: succeeded()
jobs:
- deployment: DeployWeb
displayName: 'Deploy Web App'
environment: 'development'
strategy:
runOnce:
deploy:
steps:
- task: AzureWebApp@1
displayName: 'Deploy to Azure Web App'
inputs:
azureSubscription: $(azureSubscription)
appType: 'webApp'
appName: 'my-webapp-dev'
package: '$(Pipeline.Workspace)/drop/*.zip'
- stage: DeployProd
displayName: 'Deploy to Production'
dependsOn: DeployDev
condition: succeeded()
jobs:
- deployment: DeployWeb
displayName: 'Deploy to Production'
environment: 'production'
strategy:
runOnce:
deploy:
steps:
- task: AzureWebApp@1
inputs:
azureSubscription: $(azureSubscription)
appType: 'webApp'
appName: $(webAppName)
package: '$(Pipeline.Workspace)/drop/*.zip'
deploymentMethod: 'zipDeploy'
Advanced Deployment Strategies
Blue-Green Deployment
- stage: DeployProduction
jobs:
- deployment: BlueGreenDeploy
environment: 'production'
strategy:
runOnce:
deploy:
steps:
# Deploy to staging slot (green)
- task: AzureWebApp@1
displayName: 'Deploy to Staging Slot'
inputs:
azureSubscription: $(azureSubscription)
appName: $(webAppName)
slotName: 'staging'
package: '$(Pipeline.Workspace)/drop/*.zip'
# Run smoke tests against staging
- task: PowerShell@2
displayName: 'Run Smoke Tests'
inputs:
targetType: 'inline'
script: |
$response = Invoke-WebRequest -Uri "https://$(webAppName)-staging.azurewebsites.net/health" -UseBasicParsing
if ($response.StatusCode -ne 200) {
throw "Health check failed"
}
# Swap slots (blue-green switch)
- task: AzureAppServiceManage@0
displayName: 'Swap Slots'
inputs:
azureSubscription: $(azureSubscription)
action: 'Swap Slots'
webAppName: $(webAppName)
resourceGroupName: 'my-resource-group'
sourceSlot: 'staging'
Canary Deployment with Traffic Splitting
- stage: CanaryDeploy
jobs:
- deployment: CanaryRelease
environment: 'production'
strategy:
canary:
increments: [10, 25, 50, 100]
preDeploy:
steps:
- script: echo "Starting canary deployment at $(strategy.increment)%"
deploy:
steps:
- task: AzureWebApp@1
inputs:
azureSubscription: $(azureSubscription)
appName: $(webAppName)
package: '$(Pipeline.Workspace)/drop/*.zip'
- task: AzureCLI@2
displayName: 'Configure Traffic Split'
inputs:
azureSubscription: $(azureSubscription)
scriptType: 'bash'
scriptLocation: 'inlineScript'
inlineScript: |
az webapp traffic-routing set \
--resource-group my-resource-group \
--name $(webAppName) \
--distribution staging=$(strategy.increment)
postRouteTraffic:
steps:
- script: |
echo "Monitoring metrics for $(strategy.increment)% traffic"
sleep 300
displayName: 'Monitor Deployment'
on:
failure:
steps:
- task: AzureCLI@2
displayName: 'Rollback Traffic'
inputs:
azureSubscription: $(azureSubscription)
scriptType: 'bash'
scriptLocation: 'inlineScript'
inlineScript: |
az webapp traffic-routing clear \
--resource-group my-resource-group \
--name $(webAppName)
Testing Integration
Unit and Integration Tests
- stage: Test
jobs:
- job: TestJob
displayName: 'Run Tests'
steps:
- task: UseDotNet@2
inputs:
version: '8.x'
- task: DotNetCoreCLI@2
displayName: 'Run Unit Tests'
inputs:
command: 'test'
projects: '**/*UnitTests.csproj'
arguments: '--configuration $(buildConfiguration) --collect:"XPlat Code Coverage" --logger trx'
- task: DotNetCoreCLI@2
displayName: 'Run Integration Tests'
inputs:
command: 'test'
projects: '**/*IntegrationTests.csproj'
arguments: '--configuration $(buildConfiguration) --logger trx'
- task: PublishTestResults@2
displayName: 'Publish Test Results'
condition: succeededOrFailed()
inputs:
testResultsFormat: 'VSTest'
testResultsFiles: '**/*.trx'
mergeTestResults: true
- task: PublishCodeCoverageResults@1
displayName: 'Publish Code Coverage'
inputs:
codeCoverageTool: 'Cobertura'
summaryFileLocation: '$(Agent.TempDirectory)/**/coverage.cobertura.xml'
Security Scanning
- job: SecurityScan
displayName: 'Security Analysis'
steps:
- task: DotNetCoreCLI@2
displayName: 'Restore for Vulnerability Scan'
inputs:
command: 'restore'
- task: SonarCloudPrepare@1
displayName: 'Prepare SonarCloud Analysis'
inputs:
SonarCloud: 'SonarCloud Connection'
organization: 'my-org'
scannerMode: 'MSBuild'
projectKey: 'my-project-key'
- script: dotnet build --configuration $(buildConfiguration)
displayName: 'Build for Analysis'
- task: SonarCloudAnalyze@1
displayName: 'Run Code Analysis'
- task: SonarCloudPublish@1
displayName: 'Publish Quality Gate Result'
- task: WhiteSource@21
displayName: 'WhiteSource Scan'
inputs:
cwd: '$(System.DefaultWorkingDirectory)'
Pipeline Variables and Secrets
Variable Groups
Create variable groups in Azure DevOps Library:
variables:
- group: 'production-variables'
- group: 'shared-secrets'
- name: environment
value: 'production'
Using Azure Key Vault
variables:
- group: 'KeyVault-Secrets'
steps:
- task: AzureKeyVault@2
displayName: 'Get Secrets from Key Vault'
inputs:
azureSubscription: $(azureSubscription)
KeyVaultName: 'my-keyvault'
SecretsFilter: 'ConnectionString,ApiKey'
RunAsPreJob: true
- script: |
echo "Using secret: $(ConnectionString)"
displayName: 'Use Secret'
env:
SECRET_VALUE: $(ConnectionString)
Container-Based Pipelines
Docker Build and Push
trigger:
- main
pool:
vmImage: 'ubuntu-latest'
variables:
dockerRegistryServiceConnection: 'MyACR'
imageRepository: 'myapp'
containerRegistry: 'myregistry.azurecr.io'
dockerfilePath: '$(Build.SourcesDirectory)/Dockerfile'
tag: '$(Build.BuildId)'
stages:
- stage: Build
jobs:
- job: BuildAndPush
displayName: 'Build and Push Docker Image'
steps:
- task: Docker@2
displayName: 'Build Docker Image'
inputs:
command: 'build'
repository: $(imageRepository)
dockerfile: $(dockerfilePath)
containerRegistry: $(dockerRegistryServiceConnection)
tags: |
$(tag)
latest
- task: Docker@2
displayName: 'Push to Container Registry'
inputs:
command: 'push'
repository: $(imageRepository)
containerRegistry: $(dockerRegistryServiceConnection)
tags: |
$(tag)
latest
- stage: Deploy
dependsOn: Build
jobs:
- deployment: DeployContainer
environment: 'production'
strategy:
runOnce:
deploy:
steps:
- task: AzureWebAppContainer@1
displayName: 'Deploy to Azure App Service'
inputs:
azureSubscription: $(azureSubscription)
appName: 'my-containerized-app'
containers: $(containerRegistry)/$(imageRepository):$(tag)
Best Practices
1. Pipeline Organization
- Use templates for reusable pipeline components
- Separate build and deployment logic into stages
- Keep pipelines version-controlled with application code
- Use descriptive display names for all tasks
2. Security
- Never store secrets in YAML files
- Use Azure Key Vault for sensitive configuration
- Implement approval gates for production deployments
- Enable branch protection policies
- Use service connections with minimal permissions
3. Performance Optimization
- Cache dependencies between pipeline runs
- Use parallel jobs for independent tasks
- Choose appropriate agent pools (Microsoft-hosted vs. self-hosted)
- Publish artifacts only when necessary
4. Testing Strategy
- Run fast unit tests in every build
- Execute integration tests before deployment
- Implement smoke tests post-deployment
- Maintain high code coverage thresholds
5. Monitoring and Notifications
- Configure build failure notifications
- Integrate with Application Insights
- Set up deployment approval workflows
- Track deployment frequency and success rates
6. Versioning
- Use semantic versioning for releases
- Tag builds with version numbers
- Maintain changelog with deployment notes
- Implement rollback procedures
Troubleshooting Common Issues
Build Failures
Issue: NuGet restore fails
Solution: Clear package cache, verify feed permissions
- task: NuGetCommand@2
inputs:
command: 'restore'
restoreSolution: '**/*.sln'
feedsToUse: 'config'
nugetConfigPath: 'NuGet.config'
Issue: Timeout during deployment
Solution: Increase timeout, optimize artifact size
- task: AzureWebApp@1
timeoutInMinutes: 20
inputs:
# deployment configuration
Agent Issues
Issue: Agent pool queue is full
Solution: Scale out agents or use Microsoft-hosted agents
pool:
name: 'Default'
demands:
- agent.os -equals Linux
Deployment Failures
Issue: Slot swap fails
Solution: Verify app settings, check deployment slots
az webapp deployment slot list --name my-webapp --resource-group my-rg
Key Takeaways
- YAML-based pipelines provide version control, code review, and reusability for CI/CD workflows
- Multi-stage pipelines combine build and deployment in a single, auditable process
- Deployment strategies like blue-green and canary reduce risk in production releases
- Testing integration ensures quality gates throughout the pipeline
- Security practices protect secrets and credentials using Key Vault and service connections
- Monitoring and notifications provide visibility into pipeline health and deployment status
- Templates and reusability reduce duplication and standardize workflows across teams
Additional Resources
- Azure Pipelines Documentation
- YAML Schema Reference
- Pipeline Templates
- Deployment Strategies
- Security Best Practices
- Azure DevOps Labs
Call to Action
Ready to implement CI/CD for your projects? Start by creating a basic pipeline, gradually add stages for testing and deployment, and iterate based on your team's needs. Share your pipeline configurations and lessons learned with the community, and explore advanced features like self-hosted agents, pipeline decorators, and integration with other Azure services for a complete DevOps solution.
Have questions about Azure Pipelines or want to share your CI/CD journey? Let's discuss in the comments below!
Architecture Overview (ASCII Diagram)
┌────────────────────────────────────────────────────────────────────────────────────────────┐
│ Azure DevOps CI/CD Flow │
├────────────────────────────────────────────────────────────────────────────────────────────┤
│ │
│ Developer Commit / PR │
│ │ │
│ ▼ │
│ ┌──────────────┐ PR Validation Pipeline (lint, unit tests, SAST) │
│ │ Git Repo │──────────────────────────────────────────────────────────────────────┐ │
│ └──────────────┘ │ │
│ │ ▼ │
│ │ ┌────────────────┐
│ ▼ │ Build Stage │
│ Branch Policy (reviewers, checks) │ • Restore │
│ │ │ • Compile │
│ ▼ │ • Unit Tests │
│ Merge to main triggers Full Multi-Stage Pipeline │ • Static Scan │
│ │ │ • Publish Artfs │
│ ▼ └────────────────┘
│ ┌──────────────────────────────────────────────────────────────────────────────────────────┐ │
│ │ Multi-Stage YAML Pipeline │ │
│ └──────────────────────────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌────────────────┐ ┌────────────────┐ ┌────────────────┐ ┌──────────────────────────┐ │
│ │ Test Stage │→→│ Quality Stage │→→│ Security Stage │→→│ Package Stage (Artifacts)│ │
│ │ • Integration │ │ • Code Metrics │ │ • SCA / SAST │ │ • Image Build / Sign │ │
│ │ • API Tests │ │ • Coverage │ │ • IaC Scan │ │ • SBOM Generation │ │
│ │ • Contract │ │ • Lint │ │ • Secrets Scan │ │ • Push to Registry │ │
│ └────────────────┘ └────────────────┘ └────────────────┘ └──────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌────────────────┐ Canary Deploy (Slots / % Traffic) ┌──────────────────────────────────┐ │
│ │ Deploy Dev │────────────────────────────────────→│ Deploy Prod (Blue-Green Swap) │ │
│ │ Deploy QA │ └──────────────────────────────────┘ │
│ │ Deploy Stage │ │
│ └────────────────┘ │
│ │ │
│ ▼ │
│ Observability & Feedback: App Insights, Log Analytics, Alerts, Change Audits │
│ │
└────────────────────────────────────────────────────────────────────────────────────────────┘
Comprehensive Multi-Stage Pipeline (Production-Ready Example)
name: $(Date:yyyyMMdd)$(Rev:.r)
trigger:
branches:
include:
- main
paths:
exclude:
- docs/*
pr:
branches:
include:
- main
- develop
variables:
# Centralized variable expansion
buildConfiguration: Release
dotnetVersion: 8.x
imageRepository: myapp-api
acrName: contosoacr
containerRegistry: $(acrName).azurecr.io
k8sNamespace: prod-apps
helmChartPath: infra/charts/myapp
infraSubscription: My-Infra-Subscription
appSubscription: My-App-Subscription
artifactName: drop
enableFeatureX: false
stages:
- stage: PRValidation
displayName: PR Validation
condition: eq(variables['Build.Reason'], 'PullRequest')
jobs:
- job: LintAndTests
pool:
vmImage: ubuntu-latest
steps:
- task: UseDotNet@2
inputs:
version: $(dotnetVersion)
- script: dotnet tool restore
displayName: Restore .NET Tools
- script: dotnet format --verify-no-changes
displayName: Lint Formatting
- script: dotnet build --configuration Debug
displayName: Build
- script: dotnet test --configuration Debug --logger trx --collect:"XPlat Code Coverage"
displayName: Unit Tests
- task: PublishTestResults@2
inputs:
testResultsFormat: VSTest
testResultsFiles: '**/*.trx'
failTaskOnFailedTests: true
- stage: Build
displayName: Build & Package
jobs:
- job: BuildJob
pool:
vmImage: ubuntu-latest
steps:
- checkout: self
persistCredentials: true
- task: UseDotNet@2
inputs:
version: $(dotnetVersion)
- task: Cache@2
inputs:
key: 'nuget | "$(Agent.OS)" | **/*.csproj'
restoreKeys: 'nuget | "$(Agent.OS)"'
path: ~/.nuget/packages
- script: |
dotnet restore
dotnet build --configuration $(buildConfiguration) --no-restore
displayName: Restore & Compile
- script: |
dotnet test --configuration $(buildConfiguration) --logger trx --collect:"XPlat Code Coverage"
displayName: Run Tests
- task: DotNetCoreCLI@2
displayName: Publish App
inputs:
command: publish
publishWebProjects: true
arguments: '--configuration $(buildConfiguration) --output $(Build.ArtifactStagingDirectory)'
zipAfterPublish: true
- publish: $(Build.ArtifactStagingDirectory)
artifact: $(artifactName)
- script: sbom-tool generate -b $(Build.SourcesDirectory) -bc $(Build.SourcesDirectory) -pn Contoso.MyApp -pv 1.0.$(Build.BuildId) -nsb . -V true
displayName: Generate SBOM
- stage: SecurityQuality
dependsOn: Build
displayName: Security & Quality Gates
jobs:
- job: Scan
pool:
vmImage: ubuntu-latest
steps:
- task: SonarCloudPrepare@1
inputs:
SonarCloud: 'SonarCloud'
organization: 'contoso'
scannerMode: 'MSBuild'
projectKey: 'contoso-myapp'
projectName: 'Contoso MyApp'
- script: dotnet build --configuration $(buildConfiguration)
displayName: Build for Analysis
- task: SonarCloudAnalyze@1
- task: SonarCloudPublish@1
- task: Trivy@0
displayName: Container Vulnerability Scan
inputs:
dockerImageName: '$(containerRegistry)/$(imageRepository):$(Build.BuildId)'
- task: SnykSecurityScan@1
displayName: Dependency Scan
inputs:
serviceConnectionEndpoint: 'SnykServiceConnection'
testType: 'app'
- task: AzureCLI@2
displayName: Scan Bicep (Checkov)
inputs:
azureSubscription: $(infraSubscription)
scriptType: bash
scriptLocation: inlineScript
inlineScript: |
pip install checkov --quiet
checkov -d infra/bicep || true
- script: echo "Quality Stage Complete"
- stage: ContainerImage
dependsOn: Build
jobs:
- job: BuildContainer
pool:
vmImage: ubuntu-latest
steps:
- task: Docker@2
displayName: Build Image
inputs:
command: build
repository: $(imageRepository)
dockerfile: Dockerfile
containerRegistry: '$(acrName)'
tags: |
$(Build.BuildId)
latest
- task: Docker@2
displayName: Push Image
inputs:
command: push
repository: $(imageRepository)
containerRegistry: '$(acrName)'
tags: |
$(Build.BuildId)
latest
- task: PublishBuildArtifacts@1
inputs:
PathtoPublish: 'infra/charts'
ArtifactName: 'helm'
publishLocation: Container
- stage: InfraDeploy
displayName: Provision Infrastructure (Bicep)
dependsOn: SecurityQuality
jobs:
- deployment: DeployInfra
environment: 'infrastructure'
strategy:
runOnce:
deploy:
steps:
- task: AzureCLI@2
displayName: Deploy Bicep
inputs:
azureSubscription: $(infraSubscription)
scriptType: bash
scriptLocation: inlineScript
inlineScript: |
az group create -n rg-myapp-prod -l eastus
az deployment group create \
-g rg-myapp-prod \
-f infra/bicep/main.bicep \
-p environment=prod enableFeatureX=$(enableFeatureX)
- stage: DevDeploy
displayName: Deploy to Dev
dependsOn: ContainerImage
jobs:
- deployment: DeployDev
environment: 'dev'
strategy:
runOnce:
deploy:
steps:
- download: current
- task: HelmInstaller@1
inputs:
helmVersionToInstall: 'latest'
- task: AzureCLI@2
displayName: Helm Deploy Dev
inputs:
azureSubscription: $(appSubscription)
scriptType: bash
scriptLocation: inlineScript
inlineScript: |
az aks get-credentials -g rg-myapp-prod -n aks-prod --admin
helm upgrade --install myapp-dev $(helmChartPath) \
--namespace $(k8sNamespace) \
--set image.repository=$(containerRegistry)/$(imageRepository) \
--set image.tag=$(Build.BuildId) \
--set replicaCount=2
- stage: QaDeploy
displayName: Deploy to QA
dependsOn: DevDeploy
jobs:
- deployment: DeployQA
environment: 'qa'
strategy:
runOnce:
deploy:
steps:
- task: AzureCLI@2
displayName: Helm Deploy QA
inputs:
azureSubscription: $(appSubscription)
scriptType: bash
scriptLocation: inlineScript
inlineScript: |
az aks get-credentials -g rg-myapp-prod -n aks-prod --admin
helm upgrade --install myapp-qa $(helmChartPath) \
--namespace $(k8sNamespace) \
--set image.repository=$(containerRegistry)/$(imageRepository) \
--set image.tag=$(Build.BuildId) \
--set replicaCount=3
- script: curl -f https://qa.myapp.contoso/health || exit 1
displayName: Health Check QA
- stage: ProdCanary
displayName: Canary Production Deployment
dependsOn: QaDeploy
jobs:
- deployment: Canary
environment: 'production'
strategy:
canary:
increments: [10,25,50,100]
preDeploy:
steps:
- script: echo "Starting canary $(strategy.increment)%"
deploy:
steps:
- task: AzureCLI@2
displayName: Helm Deploy Canary
inputs:
azureSubscription: $(appSubscription)
scriptType: bash
scriptLocation: inlineScript
inlineScript: |
az aks get-credentials -g rg-myapp-prod -n aks-prod --admin
helm upgrade --install myapp-prod $(helmChartPath) \
--namespace $(k8sNamespace) \
--set image.repository=$(containerRegistry)/$(imageRepository) \
--set image.tag=$(Build.BuildId) \
--set replicaCount=5 \
--set canaryIncrement=$(strategy.increment)
postRouteTraffic:
steps:
- script: echo "Monitoring $(strategy.increment)% traffic" && sleep 120
displayName: Monitor
on:
failure:
steps:
- script: echo "Rollback initiated"; helm rollback myapp-prod 0
displayName: Rollback
- stage: Observability
displayName: Observability & Post-Deployment
dependsOn: ProdCanary
jobs:
- job: Telemetry
pool:
vmImage: ubuntu-latest
steps:
- task: AzureCLI@2
displayName: Query Live Metrics
inputs:
azureSubscription: $(appSubscription)
scriptType: bash
scriptLocation: inlineScript
inlineScript: |
az monitor metrics list \
--resource myapp-prod-appinsights \
--metric requests/duration --interval PT10M || true
- script: echo "Deployment Complete" > summary.txt
displayName: Generate Summary
- publish: summary.txt
artifact: deployment-summary
Reusable Pipeline Templates
Create a .azure-pipelines/templates folder and factor shared logic:
templates/dotnet-build.yml
parameters:
solution: '**/*.sln'
configuration: 'Release'
steps:
- task: UseDotNet@2
inputs:
version: 8.x
- script: dotnet restore
displayName: Restore
- script: dotnet build --configuration ${{ parameters.configuration }} --no-restore
displayName: Build
- script: dotnet test --configuration ${{ parameters.configuration }} --logger trx
displayName: Test
Usage:
jobs:
- job: Build
steps:
- template: templates/dotnet-build.yml
parameters:
configuration: 'Debug'
Parameterization & Runtime Control
Use parameters for controlled variations:
parameters:
- name: deployEnvironment
displayName: Environment
type: string
default: dev
values:
- dev
- qa
- prod
Conditional stage:
stages:
- stage: ProdOnly
condition: eq('${{ parameters.deployEnvironment }}','prod')
Self-Hosted Agents Setup (Windows Example)
Install script placeholder (reference web picture):
Invoke-WebRequest -Uri https://vstsagentpackage.azureedge.net/agent/3.0.2/vsts-agent-win-x64-3.0.2.zip -OutFile agent.zip
Expand-Archive agent.zip -DestinationPath .\agent
cd agent
./config.cmd --url https://dev.azure.com/contoso --auth pat --token $Env:AZDO_PAT --pool SelfHosted --agent ProdAgent01 --work _work --runAsService
./run.cmd

Caching Strategies
- task: Cache@2
inputs:
key: 'npm | $(Agent.OS) | package-lock.json'
restoreKeys: 'npm | $(Agent.OS)'
path: 'node_modules'
Infrastructure as Code (Bicep Example)
infra/bicep/main.bicep
param environment string
param enableFeatureX bool = false
resource appPlan 'Microsoft.Web/serverfarms@2023-01-01' = {
name: 'asp-myapp-${environment}'
location: resourceGroup().location
sku: {
name: 'P1v3'
tier: 'PremiumV3'
capacity: 1
}
}
resource webApp 'Microsoft.Web/sites@2023-01-01' = {
name: 'myapp-${environment}'
location: resourceGroup().location
properties: {
httpsOnly: true
}
identity: {
type: 'SystemAssigned'
}
kind: 'app'
sku: {
name: 'P1v3'
tier: 'PremiumV3'
}
}
@description('Optional feature flag')
var featureX = enableFeatureX ? 'Enabled' : 'Disabled'
AKS Deployment with Helm & Dapr Sidecars
steps:
- task: AzureCLI@2
inputs:
azureSubscription: $(appSubscription)
scriptType: bash
scriptLocation: inlineScript
inlineScript: |
az aks get-credentials -g rg-myapp-prod -n aks-prod --admin
helm repo add dapr https://dapr.github.io/helm-charts
helm upgrade --install dapr dapr/dapr --namespace dapr-system --create-namespace
helm upgrade --install myapp ./charts/myapp \
--namespace prod-apps \
--set image.repository=$(containerRegistry)/$(imageRepository) \
--set image.tag=$(Build.BuildId) \
--set dapr.enabled=true
Release Gates & Approvals
Configure manual approval on the production environment and add pre-deployment checks:
environment: production
strategy:
runOnce:
preDeploy:
steps:
- script: echo 'Validating change ticket...'
- task: AzureCLI@2
inputs:
scriptType: bash
scriptLocation: inlineScript
inlineScript: |
# Example ticket validation placeholder
echo "Ticket APPROVED"
deploy:
steps:
- script: echo 'Deploying...'

Observability Integration
- task: AzureCLI@2
displayName: Push Custom Metrics
inputs:
scriptType: bash
scriptLocation: inlineScript
inlineScript: |
az monitor metric alert create \
--name HighErrorRate \
--resource-group rg-myapp-prod \
--scopes /subscriptions/xxxx/resourceGroups/rg-myapp-prod/providers/microsoft.insights/components/myapp-prod-appinsights \
--condition "count requests > 100" \
--window-size 5m \
--evaluation-frequency 1m
Microservices Scenario (Service Composition)
Use pipeline matrices for multi-service builds:
strategy:
matrix:
serviceA:
path: 'services/service-a'
serviceB:
path: 'services/service-b'
serviceC:
path: 'services/service-c'
steps:
- script: |
cd ${{ matrix.path }}
dotnet build
dotnet test
Web Picture References (Placeholders)
Include real screenshots in content/images/azure-devops/:
- Pipeline summary:
 - Approval gate:
 - Test results:
 - Code coverage:
 - Deployment slots:

Extended Troubleshooting
| Symptom | Cause | Resolution |
|---|---|---|
| Pipeline stuck queued | Agent capacity exhausted | Add parallel agents / switch pool |
| Key Vault task fails | Missing access policy | Assign identity and set get/list permissions |
| Sonar gate fails | New hotspots or coverage drop | Refactor tests; adjust quality profile only if justified |
| Helm upgrade error | Namespace missing | Ensure namespace creation with --create-namespace |
| Canary rollback triggered | Latency spike | Investigate App Insights traces; compare deployment diff |
| Bicep deployment fails | API version deprecated | Update resource module versions |
| Trivy scan fails | Network restriction | Add outbound firewall rule / use cached DB |
| Artifact not found | Publish path mismatch | Verify $(Build.ArtifactStagingDirectory) usage |
Additional Metrics to Track
- Lead time for changes
- Deployment frequency
- Change failure rate
- Mean time to recovery (MTTR)
- Test flakiness rate
- Container image vulnerability count (critical/high)
- Coverage trend over last 10 builds
Final Best Practice Additions
- Enforce mandatory PR checks (build + security scan) before merge.
- Generate SBOM for every production artifact.
- Use service connections scoped to resource groups only.
- Tag container images with commit SHA + semantic version.
- Prefer canary for stateful services; blue-green for stateless web apps.
- Automate drift detection for IaC (run daily
what-if). - Centralize alert definitions in templates.
- Periodically prune unused pipelines and variable groups.
Next Improvement Opportunities
- Add pipeline decorators for universal pre/post steps.
- Integrate GitHub Advanced Security (CodeQL) if repo mirrored.
- Add load testing stage using Azure Load Testing.
- Implement dynamic environment naming for ephemeral review apps.
- Add chaos experiments post-deployment.
- Introduce feature flag management (LaunchDarkly / Azure App Config).
- Consolidate security findings into single SARIF lifecycle.
- Add data migration stage with automated rollback validation.
- Implement dependency graph build skipping.
- Add chat notifications (Teams webhook) with rich status cards.