6 min read
Azure Pipelines Templates for Reusable CI/CD
Introduction
Azure Pipelines templates enable you to define reusable content, logic, and parameters. By extracting common patterns into templates, you reduce duplication and maintain consistency across multiple pipelines. This post explores advanced template patterns for enterprise-scale CI/CD.
Template Types
Azure Pipelines supports four template types:
- Stage templates - Reusable stages
- Job templates - Reusable jobs
- Step templates - Reusable steps
- Variable templates - Reusable variables
Step Templates
Basic Step Template
# templates/steps/dotnet-restore-build.yml
parameters:
- name: buildConfiguration
type: string
default: 'Release'
- name: dotnetVersion
type: string
default: '6.0.x'
steps:
- task: UseDotNet@2
displayName: 'Setup .NET SDK'
inputs:
version: ${{ parameters.dotnetVersion }}
includePreviewVersions: true
- task: DotNetCoreCLI@2
displayName: 'Restore packages'
inputs:
command: 'restore'
projects: '**/*.csproj'
feedsToUse: 'config'
nugetConfigPath: 'nuget.config'
- task: DotNetCoreCLI@2
displayName: 'Build solution'
inputs:
command: 'build'
projects: '**/*.csproj'
arguments: '--configuration ${{ parameters.buildConfiguration }} --no-restore'
Step Template with Conditional Logic
# templates/steps/docker-build-push.yml
parameters:
- name: imageName
type: string
- name: dockerFile
type: string
default: 'Dockerfile'
- name: pushImage
type: boolean
default: true
- name: registry
type: string
default: 'myregistry.azurecr.io'
steps:
- task: Docker@2
displayName: 'Build Docker image'
inputs:
command: 'build'
dockerfile: ${{ parameters.dockerFile }}
repository: ${{ parameters.registry }}/${{ parameters.imageName }}
tags: |
$(Build.BuildId)
latest
- ${{ if eq(parameters.pushImage, true) }}:
- task: Docker@2
displayName: 'Push Docker image'
inputs:
command: 'push'
repository: ${{ parameters.registry }}/${{ parameters.imageName }}
tags: |
$(Build.BuildId)
latest
Job Templates
Deployment Job Template
# templates/jobs/deploy-app-service.yml
parameters:
- name: environmentName
type: string
- name: azureSubscription
type: string
- name: appServiceName
type: string
- name: resourceGroup
type: string
- name: slotName
type: string
default: 'staging'
- name: useSlot
type: boolean
default: true
jobs:
- deployment: Deploy_${{ parameters.environmentName }}
displayName: 'Deploy to ${{ parameters.environmentName }}'
environment: ${{ parameters.environmentName }}
pool:
vmImage: 'ubuntu-latest'
strategy:
runOnce:
deploy:
steps:
- download: current
artifact: drop
- ${{ if eq(parameters.useSlot, true) }}:
- task: AzureWebApp@1
displayName: 'Deploy to staging slot'
inputs:
azureSubscription: ${{ parameters.azureSubscription }}
appType: 'webApp'
appName: ${{ parameters.appServiceName }}
deployToSlotOrASE: true
resourceGroupName: ${{ parameters.resourceGroup }}
slotName: ${{ parameters.slotName }}
package: '$(Pipeline.Workspace)/drop/*.zip'
- task: AzureAppServiceManage@0
displayName: 'Swap staging to production'
inputs:
azureSubscription: ${{ parameters.azureSubscription }}
Action: 'Swap Slots'
WebAppName: ${{ parameters.appServiceName }}
ResourceGroupName: ${{ parameters.resourceGroup }}
SourceSlot: ${{ parameters.slotName }}
- ${{ if eq(parameters.useSlot, false) }}:
- task: AzureWebApp@1
displayName: 'Deploy directly to production'
inputs:
azureSubscription: ${{ parameters.azureSubscription }}
appType: 'webApp'
appName: ${{ parameters.appServiceName }}
package: '$(Pipeline.Workspace)/drop/*.zip'
Test Job Template with Matrix
# templates/jobs/run-tests.yml
parameters:
- name: testProjects
type: string
default: '**/*Tests.csproj'
- name: platforms
type: object
default:
- os: 'ubuntu-latest'
name: 'Linux'
- os: 'windows-latest'
name: 'Windows'
jobs:
- ${{ each platform in parameters.platforms }}:
- job: Test_${{ platform.name }}
displayName: 'Run tests on ${{ platform.name }}'
pool:
vmImage: ${{ platform.os }}
steps:
- task: UseDotNet@2
inputs:
version: '6.0.x'
- task: DotNetCoreCLI@2
displayName: 'Run tests'
inputs:
command: 'test'
projects: ${{ parameters.testProjects }}
arguments: '--configuration Release --collect:"XPlat Code Coverage"'
- task: PublishCodeCoverageResults@1
inputs:
codeCoverageTool: 'Cobertura'
summaryFileLocation: '$(Agent.TempDirectory)/**/coverage.cobertura.xml'
Stage Templates
Complete CI Stage Template
# templates/stages/ci-stage.yml
parameters:
- name: buildConfiguration
type: string
default: 'Release'
- name: runTests
type: boolean
default: true
- name: runCodeAnalysis
type: boolean
default: true
- name: publishArtifact
type: boolean
default: true
stages:
- stage: CI
displayName: 'Continuous Integration'
jobs:
- job: Build
pool:
vmImage: 'ubuntu-latest'
steps:
- template: ../steps/dotnet-restore-build.yml
parameters:
buildConfiguration: ${{ parameters.buildConfiguration }}
- ${{ if eq(parameters.runTests, true) }}:
- task: DotNetCoreCLI@2
displayName: 'Run unit tests'
inputs:
command: 'test'
projects: '**/*Tests.csproj'
arguments: '--configuration ${{ parameters.buildConfiguration }} --no-build'
- ${{ if eq(parameters.runCodeAnalysis, true) }}:
- task: SonarCloudPrepare@1
inputs:
SonarCloud: 'SonarCloud'
organization: 'myorg'
scannerMode: 'MSBuild'
projectKey: 'myproject'
- task: SonarCloudAnalyze@1
- task: SonarCloudPublish@1
- ${{ if eq(parameters.publishArtifact, true) }}:
- task: DotNetCoreCLI@2
displayName: 'Publish application'
inputs:
command: 'publish'
publishWebProjects: true
arguments: '--configuration ${{ parameters.buildConfiguration }} --output $(Build.ArtifactStagingDirectory)'
zipAfterPublish: true
- publish: $(Build.ArtifactStagingDirectory)
artifact: drop
CD Stage Template
# templates/stages/cd-stage.yml
parameters:
- name: environmentName
type: string
- name: azureSubscription
type: string
- name: appServiceName
type: string
- name: resourceGroup
type: string
- name: dependsOn
type: object
default: []
- name: condition
type: string
default: 'succeeded()'
stages:
- stage: Deploy_${{ parameters.environmentName }}
displayName: 'Deploy to ${{ parameters.environmentName }}'
dependsOn: ${{ parameters.dependsOn }}
condition: ${{ parameters.condition }}
jobs:
- template: ../jobs/deploy-app-service.yml
parameters:
environmentName: ${{ parameters.environmentName }}
azureSubscription: ${{ parameters.azureSubscription }}
appServiceName: ${{ parameters.appServiceName }}
resourceGroup: ${{ parameters.resourceGroup }}
Variable Templates
Environment-Specific Variables
# templates/variables/dev.yml
variables:
environmentName: 'development'
appServiceName: 'myapp-dev'
resourceGroup: 'rg-myapp-dev'
azureSubscription: 'Azure-Dev-Connection'
appInsightsKey: '$(DevAppInsightsKey)'
# templates/variables/prod.yml
variables:
environmentName: 'production'
appServiceName: 'myapp-prod'
resourceGroup: 'rg-myapp-prod'
azureSubscription: 'Azure-Prod-Connection'
appInsightsKey: '$(ProdAppInsightsKey)'
Composing Templates
Main Pipeline Using All Templates
# azure-pipelines.yml
trigger:
branches:
include:
- main
- develop
- feature/*
pr:
branches:
include:
- main
- develop
resources:
repositories:
- repository: templates
type: git
name: MyProject/pipeline-templates
ref: refs/heads/main
variables:
- template: templates/variables/common.yml
stages:
# CI Stage
- template: templates/stages/ci-stage.yml
parameters:
buildConfiguration: 'Release'
runTests: true
runCodeAnalysis: true
# Deploy to Dev (on develop branch or main)
- template: templates/stages/cd-stage.yml
parameters:
environmentName: 'development'
azureSubscription: 'Azure-Dev'
appServiceName: 'myapp-dev'
resourceGroup: 'rg-dev'
dependsOn: ['CI']
condition: |
and(
succeeded(),
or(
eq(variables['Build.SourceBranch'], 'refs/heads/develop'),
eq(variables['Build.SourceBranch'], 'refs/heads/main')
)
)
# Deploy to Staging (main branch only)
- template: templates/stages/cd-stage.yml
parameters:
environmentName: 'staging'
azureSubscription: 'Azure-Staging'
appServiceName: 'myapp-staging'
resourceGroup: 'rg-staging'
dependsOn: ['Deploy_development']
condition: |
and(
succeeded(),
eq(variables['Build.SourceBranch'], 'refs/heads/main')
)
# Deploy to Production (main branch with approval)
- template: templates/stages/cd-stage.yml
parameters:
environmentName: 'production'
azureSubscription: 'Azure-Prod'
appServiceName: 'myapp-prod'
resourceGroup: 'rg-prod'
dependsOn: ['Deploy_staging']
condition: |
and(
succeeded(),
eq(variables['Build.SourceBranch'], 'refs/heads/main')
)
Template Expressions
Iterating Over Objects
# templates/steps/deploy-multiple-regions.yml
parameters:
- name: regions
type: object
default:
- name: 'AustraliaEast'
appName: 'myapp-aue'
- name: 'SoutheastAsia'
appName: 'myapp-sea'
- name: 'WestEurope'
appName: 'myapp-weu'
steps:
- ${{ each region in parameters.regions }}:
- task: AzureWebApp@1
displayName: 'Deploy to ${{ region.name }}'
inputs:
azureSubscription: 'Azure-Prod'
appName: ${{ region.appName }}
package: '$(Pipeline.Workspace)/drop/*.zip'
Conditional Template Insertion
# templates/jobs/build-job.yml
parameters:
- name: includeSecurityScan
type: boolean
default: false
- name: securityScanTool
type: string
default: 'WhiteSource'
jobs:
- job: Build
steps:
- template: ../steps/dotnet-restore-build.yml
- ${{ if eq(parameters.includeSecurityScan, true) }}:
- ${{ if eq(parameters.securityScanTool, 'WhiteSource') }}:
- task: WhiteSource@21
inputs:
projectName: '$(Build.Repository.Name)'
- ${{ if eq(parameters.securityScanTool, 'Snyk') }}:
- task: SnykSecurityScan@1
inputs:
serviceConnectionEndpoint: 'Snyk'
Shared Template Repository
Setting Up Template Repository
# In consuming pipeline
resources:
repositories:
- repository: templates
type: git
name: SharedProject/pipeline-templates
ref: refs/tags/v1.0.0 # Use tags for versioning
stages:
- template: stages/ci.yml@templates
parameters:
buildConfiguration: 'Release'
Template Repository Structure
pipeline-templates/
├── stages/
│ ├── ci.yml
│ ├── cd-webapp.yml
│ └── cd-function.yml
├── jobs/
│ ├── build-dotnet.yml
│ ├── build-node.yml
│ └── deploy-azure.yml
├── steps/
│ ├── dotnet-restore-build.yml
│ ├── docker-build-push.yml
│ └── azure-login.yml
├── variables/
│ ├── common.yml
│ ├── dev.yml
│ └── prod.yml
└── README.md
Conclusion
Azure Pipelines templates are essential for maintaining DRY (Don’t Repeat Yourself) principles in CI/CD. By investing time in well-designed templates, you create a foundation that accelerates future pipeline development and ensures consistency across your organization. Start with step templates for common tasks, progress to job and stage templates, and eventually establish a shared template repository.