Azure DevOps CI/CD Pipelines: A Complete Guide to Automated Deployments

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

  1. YAML-based pipelines provide version control, code review, and reusability for CI/CD workflows
  2. Multi-stage pipelines combine build and deployment in a single, auditable process
  3. Deployment strategies like blue-green and canary reduce risk in production releases
  4. Testing integration ensures quality gates throughout the pipeline
  5. Security practices protect secrets and credentials using Key Vault and service connections
  6. Monitoring and notifications provide visibility into pipeline health and deployment status
  7. Templates and reusability reduce duplication and standardize workflows across teams

Additional Resources

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

Self-Hosted Agent Configuration

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...'

Environment Approval Screenshot

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: ![Pipeline Summary](images/azure-devops/pipeline-summary.png)
  • Approval gate: ![Approval Gate](images/azure-devops/approval-gate.png)
  • Test results: ![Test Analytics](images/azure-devops/test-analytics.png)
  • Code coverage: ![Coverage Report](images/azure-devops/coverage-report.png)
  • Deployment slots: ![Slot Swap](images/azure-devops/slot-swap.png)

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

  1. Add pipeline decorators for universal pre/post steps.
  2. Integrate GitHub Advanced Security (CodeQL) if repo mirrored.
  3. Add load testing stage using Azure Load Testing.
  4. Implement dynamic environment naming for ephemeral review apps.
  5. Add chaos experiments post-deployment.
  6. Introduce feature flag management (LaunchDarkly / Azure App Config).
  7. Consolidate security findings into single SARIF lifecycle.
  8. Add data migration stage with automated rollback validation.
  9. Implement dependency graph build skipping.
  10. Add chat notifications (Teams webhook) with rich status cards.