Back to Blog
5 min read

Mastering Azure DevOps YAML Pipelines

Introduction

Azure DevOps YAML pipelines provide infrastructure-as-code for your CI/CD workflows. Unlike classic pipelines, YAML pipelines are versioned alongside your code, enabling better collaboration and audit trails. This guide covers essential patterns and advanced techniques for building robust pipelines.

Basic Pipeline Structure

# azure-pipelines.yml
trigger:
  branches:
    include:
      - main
      - develop
  paths:
    include:
      - src/*
    exclude:
      - docs/*

pool:
  vmImage: 'ubuntu-latest'

variables:
  buildConfiguration: 'Release'
  dotnetVersion: '6.0.x'

stages:
  - stage: Build
    jobs:
      - job: BuildJob
        steps:
          - task: UseDotNet@2
            inputs:
              version: '$(dotnetVersion)'
              includePreviewVersions: true

          - task: DotNetCoreCLI@2
            displayName: 'Restore'
            inputs:
              command: 'restore'
              projects: '**/*.csproj'

          - task: DotNetCoreCLI@2
            displayName: 'Build'
            inputs:
              command: 'build'
              arguments: '--configuration $(buildConfiguration)'

          - task: DotNetCoreCLI@2
            displayName: 'Test'
            inputs:
              command: 'test'
              arguments: '--configuration $(buildConfiguration) --collect:"XPlat Code Coverage"'

          - task: PublishCodeCoverageResults@1
            inputs:
              codeCoverageTool: 'Cobertura'
              summaryFileLocation: '$(Agent.TempDirectory)/**/coverage.cobertura.xml'

Multi-Stage Pipelines

trigger:
  - main

stages:
  - stage: Build
    displayName: 'Build Stage'
    jobs:
      - job: Build
        pool:
          vmImage: 'ubuntu-latest'
        steps:
          - task: DotNetCoreCLI@2
            inputs:
              command: 'publish'
              publishWebProjects: true
              arguments: '--configuration Release --output $(Build.ArtifactStagingDirectory)'

          - publish: $(Build.ArtifactStagingDirectory)
            artifact: drop

  - stage: DeployDev
    displayName: 'Deploy to Development'
    dependsOn: Build
    condition: succeeded()
    jobs:
      - deployment: DeployDev
        environment: 'development'
        pool:
          vmImage: 'ubuntu-latest'
        strategy:
          runOnce:
            deploy:
              steps:
                - download: current
                  artifact: drop

                - task: AzureWebApp@1
                  inputs:
                    azureSubscription: 'Azure-Dev'
                    appName: 'myapp-dev'
                    package: '$(Pipeline.Workspace)/drop/*.zip'

  - stage: DeployProd
    displayName: 'Deploy to Production'
    dependsOn: DeployDev
    condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
    jobs:
      - deployment: DeployProd
        environment: 'production'
        pool:
          vmImage: 'ubuntu-latest'
        strategy:
          runOnce:
            deploy:
              steps:
                - download: current
                  artifact: drop

                - task: AzureWebApp@1
                  inputs:
                    azureSubscription: 'Azure-Prod'
                    appName: 'myapp-prod'
                    package: '$(Pipeline.Workspace)/drop/*.zip'

Variable Groups and Templates

Defining Variable Groups

# Variables can be defined in Azure DevOps UI or via CLI
# az pipelines variable-group create --name "Production-Variables" --variables ConnectionString=xxx

variables:
  - group: Production-Variables
  - name: localVariable
    value: 'local-value'

Variable Templates

# templates/variables-dev.yml
variables:
  environment: 'development'
  appServiceName: 'myapp-dev'
  resourceGroup: 'rg-dev'

# templates/variables-prod.yml
variables:
  environment: 'production'
  appServiceName: 'myapp-prod'
  resourceGroup: 'rg-prod'

# Main pipeline
stages:
  - stage: DeployDev
    variables:
      - template: templates/variables-dev.yml
    jobs:
      - job: Deploy
        steps:
          - script: echo "Deploying to $(environment)"

Job and Step Templates

Step Template

# templates/dotnet-build-steps.yml
parameters:
  - name: buildConfiguration
    type: string
    default: 'Release'
  - name: projects
    type: string
    default: '**/*.csproj'

steps:
  - task: UseDotNet@2
    inputs:
      version: '6.0.x'

  - task: DotNetCoreCLI@2
    displayName: 'Restore'
    inputs:
      command: 'restore'
      projects: '${{ parameters.projects }}'

  - task: DotNetCoreCLI@2
    displayName: 'Build'
    inputs:
      command: 'build'
      projects: '${{ parameters.projects }}'
      arguments: '--configuration ${{ parameters.buildConfiguration }}'

  - task: DotNetCoreCLI@2
    displayName: 'Test'
    inputs:
      command: 'test'
      projects: '**/*Tests.csproj'
      arguments: '--configuration ${{ parameters.buildConfiguration }}'

Job Template

# templates/deploy-webapp-job.yml
parameters:
  - name: environment
    type: string
  - name: azureSubscription
    type: string
  - name: appName
    type: string

jobs:
  - deployment: Deploy${{ parameters.environment }}
    displayName: 'Deploy to ${{ parameters.environment }}'
    environment: ${{ parameters.environment }}
    pool:
      vmImage: 'ubuntu-latest'
    strategy:
      runOnce:
        deploy:
          steps:
            - download: current
              artifact: drop

            - task: AzureWebApp@1
              inputs:
                azureSubscription: ${{ parameters.azureSubscription }}
                appName: ${{ parameters.appName }}
                package: '$(Pipeline.Workspace)/drop/*.zip'

Using Templates

# azure-pipelines.yml
trigger:
  - main

stages:
  - stage: Build
    jobs:
      - job: Build
        pool:
          vmImage: 'ubuntu-latest'
        steps:
          - template: templates/dotnet-build-steps.yml
            parameters:
              buildConfiguration: 'Release'
              projects: 'src/**/*.csproj'

          - task: DotNetCoreCLI@2
            displayName: 'Publish'
            inputs:
              command: 'publish'
              arguments: '--configuration Release --output $(Build.ArtifactStagingDirectory)'

          - publish: $(Build.ArtifactStagingDirectory)
            artifact: drop

  - stage: DeployDev
    dependsOn: Build
    jobs:
      - template: templates/deploy-webapp-job.yml
        parameters:
          environment: 'development'
          azureSubscription: 'Azure-Dev'
          appName: 'myapp-dev'

  - stage: DeployProd
    dependsOn: DeployDev
    condition: eq(variables['Build.SourceBranch'], 'refs/heads/main')
    jobs:
      - template: templates/deploy-webapp-job.yml
        parameters:
          environment: 'production'
          azureSubscription: 'Azure-Prod'
          appName: 'myapp-prod'

Conditional Execution

stages:
  - stage: Build
    jobs:
      - job: Build
        steps:
          # Always run
          - script: echo "Building..."

          # Run only on main branch
          - script: echo "Main branch build"
            condition: eq(variables['Build.SourceBranch'], 'refs/heads/main')

          # Run on PR builds
          - script: echo "PR build"
            condition: eq(variables['Build.Reason'], 'PullRequest')

          # Run if previous step succeeded
          - script: echo "Previous step succeeded"
            condition: succeeded()

          # Run even if previous steps failed
          - script: echo "Cleanup"
            condition: always()

          # Complex conditions
          - script: echo "Production release"
            condition: |
              and(
                succeeded(),
                eq(variables['Build.SourceBranch'], 'refs/heads/main'),
                ne(variables['Build.Reason'], 'PullRequest')
              )

Matrix Builds

jobs:
  - job: Build
    strategy:
      matrix:
        Linux:
          vmImage: 'ubuntu-latest'
          os: 'linux'
        Windows:
          vmImage: 'windows-latest'
          os: 'windows'
        macOS:
          vmImage: 'macOS-latest'
          os: 'macos'
    pool:
      vmImage: $(vmImage)
    steps:
      - script: echo "Building on $(os)"
      - task: DotNetCoreCLI@2
        inputs:
          command: 'build'

Parallel Jobs

jobs:
  - job: UnitTests
    pool:
      vmImage: 'ubuntu-latest'
    steps:
      - script: dotnet test UnitTests/

  - job: IntegrationTests
    pool:
      vmImage: 'ubuntu-latest'
    steps:
      - script: dotnet test IntegrationTests/

  - job: SecurityScan
    pool:
      vmImage: 'ubuntu-latest'
    steps:
      - task: WhiteSource@21
        inputs:
          projectName: 'MyProject'

  - job: PublishArtifact
    dependsOn:
      - UnitTests
      - IntegrationTests
      - SecurityScan
    condition: succeeded('UnitTests', 'IntegrationTests', 'SecurityScan')
    steps:
      - script: echo "All checks passed, publishing..."

Secure Files and Variables

jobs:
  - job: Deploy
    steps:
      # Download secure file
      - task: DownloadSecureFile@1
        name: certificate
        inputs:
          secureFile: 'production-cert.pfx'

      # Use secure file
      - script: |
          certutil -f -p $(certPassword) -importpfx $(certificate.secureFilePath)
        displayName: 'Install certificate'

      # Use secret variable
      - task: AzureCLI@2
        inputs:
          azureSubscription: 'Azure-Prod'
          scriptType: 'bash'
          scriptLocation: 'inlineScript'
          inlineScript: |
            az webapp config appsettings set \
              --name myapp \
              --resource-group myrg \
              --settings "ConnectionString=$(connectionString)"

Approvals and Checks

Configure in Azure DevOps UI or via YAML:

stages:
  - stage: DeployProd
    jobs:
      - deployment: Production
        environment: 'production'  # Configure approvals on this environment
        strategy:
          runOnce:
            deploy:
              steps:
                - script: echo "Deploying to production"

Caching Dependencies

variables:
  NUGET_PACKAGES: $(Pipeline.Workspace)/.nuget/packages

steps:
  - task: Cache@2
    displayName: 'Cache NuGet packages'
    inputs:
      key: 'nuget | "$(Agent.OS)" | **/packages.lock.json'
      restoreKeys: |
        nuget | "$(Agent.OS)"
      path: $(NUGET_PACKAGES)

  - task: DotNetCoreCLI@2
    inputs:
      command: 'restore'
      projects: '**/*.csproj'

Conclusion

Azure DevOps YAML pipelines provide powerful, version-controlled CI/CD capabilities. By leveraging templates, variable groups, and conditional execution, you can build maintainable pipelines that scale across multiple projects and environments. The key is to start simple and progressively add complexity as your needs evolve.

References

Michael John Peña

Michael John Peña

Senior Data Engineer based in Sydney. Writing about data, cloud, and technology.