Back to Blog
3 min read

Building Multi-Stage YAML Pipelines in Azure DevOps

With remote teams becoming the norm, having robust CI/CD pipelines is more important than ever. Azure DevOps multi-stage YAML pipelines provide a way to define your entire deployment workflow as code. Let me show you how to set one up.

Why YAML Pipelines?

  • Version controlled - Pipeline definition lives with your code
  • Reviewable - Changes go through pull requests
  • Reusable - Create templates for common patterns
  • Portable - Easy to copy between projects

Basic Structure

# azure-pipelines.yml
trigger:
  branches:
    include:
      - main
      - develop

pool:
  vmImage: 'ubuntu-latest'

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

stages:
  - stage: Build
    displayName: 'Build and Test'
    jobs:
      - job: BuildJob
        steps:
          - task: UseDotNet@2
            displayName: 'Use .NET Core SDK'
            inputs:
              version: $(dotnetVersion)

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

          - task: DotNetCoreCLI@2
            displayName: 'Build solution'
            inputs:
              command: 'build'
              projects: '**/*.csproj'
              arguments: '--configuration $(buildConfiguration)'

          - task: DotNetCoreCLI@2
            displayName: 'Run tests'
            inputs:
              command: 'test'
              projects: '**/*Tests.csproj'
              arguments: '--configuration $(buildConfiguration) --collect:"XPlat Code Coverage"'

          - task: PublishBuildArtifacts@1
            displayName: 'Publish artifacts'
            inputs:
              PathtoPublish: '$(Build.ArtifactStagingDirectory)'
              ArtifactName: 'drop'

Adding Deployment Stages

stages:
  - stage: Build
    # ... build jobs from above

  - stage: DeployDev
    displayName: 'Deploy to Development'
    dependsOn: Build
    condition: succeeded()
    jobs:
      - deployment: DeployWebApp
        displayName: 'Deploy Web App'
        environment: 'Development'
        strategy:
          runOnce:
            deploy:
              steps:
                - download: current
                  artifact: drop

                - task: AzureWebApp@1
                  displayName: 'Deploy to Azure Web App'
                  inputs:
                    azureSubscription: 'Azure-Service-Connection'
                    appType: 'webApp'
                    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: DeployWebApp
        displayName: 'Deploy Web App'
        environment: 'Production'
        strategy:
          runOnce:
            deploy:
              steps:
                - download: current
                  artifact: drop

                - task: AzureWebApp@1
                  displayName: 'Deploy to Azure Web App'
                  inputs:
                    azureSubscription: 'Azure-Service-Connection'
                    appType: 'webApp'
                    appName: 'myapp-prod'
                    package: '$(Pipeline.Workspace)/drop/**/*.zip'

Using Templates

Create reusable templates for common tasks:

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

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

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

Use the template:

stages:
  - stage: Build
    jobs:
      - job: BuildJob
        steps:
          - template: templates/dotnet-build.yml
            parameters:
              buildConfiguration: 'Release'

Environment Approvals

Configure approvals in Azure DevOps:

  1. Go to Pipelines > Environments
  2. Select your environment (e.g., Production)
  3. Click the three dots > Approvals and checks
  4. Add approvers

Variable Groups

Store sensitive configuration securely:

variables:
  - group: 'Production-Secrets'
  - name: buildConfiguration
    value: 'Release'

Conditional Execution

- stage: DeployProd
  condition: |
    and(
      succeeded(),
      eq(variables['Build.SourceBranch'], 'refs/heads/main'),
      ne(variables['Build.Reason'], 'PullRequest')
    )

Running Jobs in Parallel

- stage: Test
  jobs:
    - job: UnitTests
      steps:
        - script: dotnet test --filter Category=Unit

    - job: IntegrationTests
      steps:
        - script: dotnet test --filter Category=Integration

    - job: E2ETests
      dependsOn: []  # Run in parallel
      steps:
        - script: npm run e2e

Multi-stage YAML pipelines bring consistency and maintainability to your deployment processes, essential for distributed teams working remotely.

Michael John Peña

Michael John Peña

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