YAML Pipelines in Azure DevOps

What are YAML Pipelines?

YAML pipelines define CI/CD workflows as code using YAML syntax, stored directly in the repository alongside application code. This infrastructure-as-code approach enables version control, code review, and collaboration on pipeline definitions.

YAML pipelines provide flexibility, reusability, and portability that classic visual pipelines cannot match.


Why Use YAML Pipelines?


YAML Pipeline Structure

# Triggers trigger: branches: include: - main # Variables variables: buildConfiguration: 'Release' # Stages stages: - stage: Build jobs: - job: BuildJob pool: vmImage: 'ubuntu-latest' steps: - script: echo "Building..."

Pipeline Schema Hierarchy

graph TD A["Pipeline"] -->B["Trigger"] A -->C["Variables"] A -->D["Stages"] D -->E["Stage 1"] D -->F["Stage 2"] E -->G["Job 1"] E -->H["Job 2"] G -->I["Step 1"] G -->J["Step 2"] G -->K["Step 3"] style A fill:#e1f5ff style D fill:#fff3e0 style E fill:#e8f5e9 style G fill:#fffacd

Basic YAML Pipeline

# azure-pipelines.yml name: $(Date:yyyyMMdd)$(Rev:.r) trigger: - main pool: vmImage: 'ubuntu-latest' variables: NODE_VERSION: '18.x' steps: - task: NodeTool@0 displayName: 'Install Node.js' inputs: versionSpec: $(NODE_VERSION) - script: | npm ci npm run build npm test displayName: 'Build and test' - task: PublishTestResults@2 displayName: 'Publish test results' condition: succeededOrFailed() inputs: testResultsFiles: '**/test-results.xml' testRunTitle: 'Node.js Tests'

Stages, Jobs, and Steps

Multi-Stage Pipeline

stages: - stage: Build displayName: 'Build Application' jobs: - job: BuildJob pool: vmImage: 'ubuntu-latest' steps: - script: echo "Building" - script: dotnet build - stage: Test displayName: 'Run Tests' dependsOn: Build jobs: - job: UnitTests steps: - script: dotnet test - job: IntegrationTests steps: - script: npm run test:integration - stage: Deploy displayName: 'Deploy to Production' dependsOn: Test condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main')) jobs: - deployment: DeployWeb environment: production strategy: runOnce: deploy: steps: - script: echo "Deploying"

Stage Dependencies and Conditions

graph LR A["Build Stage"] -->B["Test Stage"] B -->C["Deploy Staging"] B -->D["Deploy Production"] C -->E{Manual
Approval} E -->F["Smoke Tests"] style A fill:#e1f5ff style B fill:#fff3e0 style C fill:#e8f5e9 style D fill:#e8f5e9

Conditional Stages

stages: - stage: Build jobs: - job: BuildJob steps: - script: echo "Building" - stage: DeployDev condition: eq(variables['Build.SourceBranch'], 'refs/heads/develop') dependsOn: Build jobs: - job: Deploy steps: - script: echo "Deploy to dev" - stage: DeployProd condition: eq(variables['Build.SourceBranch'], 'refs/heads/main') dependsOn: Build jobs: - job: Deploy steps: - script: echo "Deploy to production"

Variables in YAML

Pipeline Variables

variables: # Simple variables buildConfiguration: 'Release' dotnetVersion: '7.x' # Computed variables buildNumber: $[counter(format('{0:yyyyMMdd}', pipeline.startTime), 1)] steps: - script: echo "Build config is $(buildConfiguration)"

Variable Groups

variables: - group: 'shared-variables' - group: 'production-secrets' - name: localVar value: 'local-value' steps: - script: echo "Using variable group values"

Runtime Variables

steps: - script: | echo "##vso[task.setvariable variable=myOutputVar;isOutput=true]Hello" name: SetVar - script: echo "Value is $(SetVar.myOutputVar)"

Variable Templates

# variables-template.yml variables: buildConfiguration: 'Release' vmImage: 'ubuntu-latest' # azure-pipelines.yml variables: - template: variables-template.yml pool: vmImage: $(vmImage)

Templates

Templates enable reusability and consistency across pipelines.

Step Template

# templates/build-steps.yml parameters: - name: buildConfiguration type: string default: 'Release' steps: - task: DotNetCoreCLI@2 displayName: 'Restore packages' inputs: command: 'restore' - task: DotNetCoreCLI@2 displayName: 'Build project' inputs: command: 'build' arguments: '--configuration ${{ parameters.buildConfiguration }}' - task: DotNetCoreCLI@2 displayName: 'Run tests' inputs: command: 'test' # Main pipeline steps: - template: templates/build-steps.yml parameters: buildConfiguration: 'Release'

Job Template

# templates/test-job.yml parameters: - name: testType type: string - name: vmImage type: string default: 'ubuntu-latest' jobs: - job: ${{ parameters.testType }} pool: vmImage: ${{ parameters.vmImage }} steps: - script: echo "Running ${{ parameters.testType }}" - script: npm test # Main pipeline jobs: - template: templates/test-job.yml parameters: testType: 'UnitTests' - template: templates/test-job.yml parameters: testType: 'IntegrationTests' vmImage: 'windows-latest'

Stage Template

# templates/deploy-stage.yml parameters: - name: environment type: string - name: serviceConnection type: string stages: - stage: Deploy_${{ parameters.environment }} jobs: - deployment: Deploy environment: ${{ parameters.environment }} strategy: runOnce: deploy: steps: - task: AzureWebApp@1 inputs: azureSubscription: ${{ parameters.serviceConnection }} # Main pipeline stages: - template: templates/deploy-stage.yml parameters: environment: 'staging' serviceConnection: 'AzureStaging' - template: templates/deploy-stage.yml parameters: environment: 'production' serviceConnection: 'AzureProduction'

Extends Templates

Extends templates provide pipeline structure inheritance.

Base Template

# templates/base-pipeline.yml parameters: - name: stages type: stageList stages: - stage: Security jobs: - job: SecurityScan steps: - script: echo "Running security scan" - ${{ each stage in parameters.stages }}: - ${{ stage }} - stage: Compliance jobs: - job: ComplianceCheck steps: - script: echo "Running compliance check"

Extending Base Template

# azure-pipelines.yml extends: template: templates/base-pipeline.yml parameters: stages: - stage: Build jobs: - job: BuildJob steps: - script: dotnet build - stage: Deploy jobs: - job: DeployJob steps: - script: echo "Deploying"

Result: Security stage runs first, then Build and Deploy, then Compliance stage.


Template Flow

graph TD A["Main Pipeline"] -->B["Extends Base Template"] B -->C["Security Stage
(from base)"] C -->D["Build Stage
(from main)"] D -->E["Deploy Stage
(from main)"] E -->F["Compliance Stage
(from base)"] style A fill:#e1f5ff style B fill:#fff3e0 style C fill:#ffcccc style D fill:#ccffcc style E fill:#ccffcc style F fill:#ffcccc

Conditions and Expressions

Built-in Conditions

steps: - script: echo "Always runs" condition: always() - script: echo "Runs if previous succeeded" condition: succeeded() - script: echo "Runs if previous failed" condition: failed() - script: echo "Runs if previous succeeded or failed" condition: succeededOrFailed()

Custom Conditions

steps: - script: echo "Production deployment" condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main')) - script: echo "PR validation" condition: eq(variables['Build.Reason'], 'PullRequest') - script: echo "Manual or scheduled" condition: or(eq(variables['Build.Reason'], 'Manual'), eq(variables['Build.Reason'], 'Schedule'))

Variable-Based Conditions

variables: deployToProduction: $[eq(variables['Build.SourceBranch'], 'refs/heads/main')] stages: - stage: Deploy condition: eq(variables.deployToProduction, true) jobs: - job: DeployJob steps: - script: echo "Deploying"

Container Jobs

Run jobs inside Docker containers for consistent environments.

resources: containers: - container: node image: node:18 - container: python image: python:3.11 jobs: - job: NodeJob container: node steps: - script: | node --version npm --version - job: PythonJob container: python steps: - script: | python --version pip --version

Service Containers

Run databases or services alongside jobs.

resources: containers: - container: postgres image: postgres:15 env: POSTGRES_PASSWORD: postgres POSTGRES_DB: testdb ports: - 5432:5432 jobs: - job: IntegrationTests services: postgres: postgres steps: - script: | echo "Postgres available at localhost:5432" npm run test:integration env: DATABASE_URL: postgres://postgres:postgres@localhost:5432/testdb

Matrix Strategy

Test across multiple configurations.

strategy: matrix: Linux_Node16: vmImage: 'ubuntu-latest' nodeVersion: '16.x' Linux_Node18: vmImage: 'ubuntu-latest' nodeVersion: '18.x' Windows_Node18: vmImage: 'windows-latest' nodeVersion: '18.x' Mac_Node18: vmImage: 'macOS-latest' nodeVersion: '18.x' pool: vmImage: $(vmImage) steps: - task: NodeTool@0 inputs: versionSpec: $(nodeVersion) - script: npm test

Parallel and Sequential Execution

Parallel Jobs

jobs: - job: Job1 steps: - script: echo "Job 1" - job: Job2 steps: - script: echo "Job 2" # Both run in parallel by default

Sequential Jobs

jobs: - job: Job1 steps: - script: echo "Job 1" - job: Job2 dependsOn: Job1 steps: - script: echo "Job 2" - job: Job3 dependsOn: - Job1 - Job2 steps: - script: echo "Job 3"

Deployment Jobs

Deployment jobs track deployments to environments with history and approvals.

stages: - stage: Deploy jobs: - deployment: DeployWeb displayName: 'Deploy Web App' environment: production pool: vmImage: 'ubuntu-latest' strategy: runOnce: deploy: steps: - download: current artifact: webapp - task: AzureWebApp@1 inputs: azureSubscription: 'AzureConnection' appName: 'my-web-app' package: '$(Pipeline.Workspace)/webapp/**/*.zip'

Deployment Strategies

# Rolling deployment strategy: rolling: maxParallel: 2 deploy: steps: - script: echo "Deploying" # Canary deployment strategy: canary: increments: [10, 20, 50, 100] deploy: steps: - script: echo "Deploying to canary"

Real-World Multi-Environment Pipeline

trigger: branches: include: - main - develop variables: - group: 'shared-variables' - name: buildConfiguration value: 'Release' stages: - stage: Build displayName: 'Build Application' jobs: - job: BuildJob pool: vmImage: 'ubuntu-latest' steps: - task: UseDotNet@2 inputs: version: '7.x' - task: DotNetCoreCLI@2 displayName: 'Restore dependencies' inputs: command: 'restore' - 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: DotNetCoreCLI@2 displayName: 'Publish' inputs: command: 'publish' publishWebProjects: true arguments: '--configuration $(buildConfiguration) --output $(Build.ArtifactStagingDirectory)' - task: PublishBuildArtifacts@1 inputs: pathToPublish: '$(Build.ArtifactStagingDirectory)' artifactName: 'drop' - stage: DeployDev displayName: 'Deploy to Development' dependsOn: Build condition: eq(variables['Build.SourceBranch'], 'refs/heads/develop') jobs: - deployment: DeployDev environment: development strategy: runOnce: deploy: steps: - task: AzureWebApp@1 inputs: azureSubscription: 'AzureDev' appName: 'myapp-dev' package: '$(Pipeline.Workspace)/drop/**/*.zip' - stage: DeployStaging displayName: 'Deploy to Staging' dependsOn: Build condition: eq(variables['Build.SourceBranch'], 'refs/heads/main') jobs: - deployment: DeployStaging environment: staging strategy: runOnce: deploy: steps: - task: AzureWebApp@1 inputs: azureSubscription: 'AzureStaging' appName: 'myapp-staging' package: '$(Pipeline.Workspace)/drop/**/*.zip' - stage: DeployProduction displayName: 'Deploy to Production' dependsOn: DeployStaging condition: succeeded() jobs: - deployment: DeployProduction environment: production strategy: runOnce: deploy: steps: - task: AzureWebApp@1 inputs: azureSubscription: 'AzureProduction' appName: 'myapp-prod' package: '$(Pipeline.Workspace)/drop/**/*.zip' - task: PowerShell@2 displayName: 'Smoke test' inputs: targetType: 'inline' script: | $response = Invoke-WebRequest -Uri "https://myapp-prod.azurewebsites.net/health" if ($response.StatusCode -ne 200) { throw "Health check failed" }

Best Practices

Keep Pipelines DRY

Use templates to avoid repetition:

# Bad - repetitive jobs: - job: TestWindows pool: vmImage: 'windows-latest' steps: - script: npm install - script: npm test - job: TestLinux pool: vmImage: 'ubuntu-latest' steps: - script: npm install - script: npm test # Good - template jobs: - template: templates/test-job.yml parameters: vmImage: 'windows-latest' - template: templates/test-job.yml parameters: vmImage: 'ubuntu-latest'

Validate YAML Syntax

# Install Azure CLI az pipelines runs validate --yaml-path azure-pipelines.yml

Use Meaningful Names

# Good - stage: BuildAndTest displayName: 'Build Application and Run Tests' jobs: - job: CompileCode displayName: 'Compile Source Code' # Avoid - stage: Stage1 jobs: - job: Job1

Key Takeaways

Next Steps: Convert existing classic pipelines to YAML, create reusable templates for common patterns, and implement multi-stage pipelines with environment approvals.