Why Templates Matter
By Module 5, you can build CI pipelines for any language — .NET, Node.js, Python, containers. But here's a problem that emerges in real organizations: you're copy-pasting the same YAML blocks across 50+ repositories. When a security team mandates a new scanning step, you touch 50 files. When a build optimization is discovered, you propagate it manually. This is the DRY problem (Don't Repeat Yourself) applied to CI/CD.
Templates are to pipelines what functions are to code — they encapsulate reusable logic, accept parameters, and can be versioned independently. An extends template is like an abstract base class: it defines the structure, and consuming pipelines fill in the details.
The Five Template Types
| Template Type | What It Encapsulates | Use Case |
|---|---|---|
| Step template | A sequence of steps | Reusable build/test/deploy steps |
| Job template | Complete job with pool/strategy | Standardized build or validation jobs |
| Stage template | Full stage with jobs | Deployment stages with approvals |
| Variable template | Variable definitions | Environment-specific configuration |
| Extends template | Entire pipeline skeleton | Enterprise governance & enforcement |
When to Extract a Template
Not everything should be a template. Extract when:
- 3+ pipelines share the same logic (the "rule of three")
- A security or compliance requirement must be enforced everywhere
- Teams want golden paths — opinionated defaults with escape hatches
- You need versioned, auditable infrastructure-as-code patterns
Step Templates
Step templates are the simplest form — a file containing a reusable sequence of steps. Think of them as macros: you define steps once, then insert them into any job.
Creating a Step Template
A step template file contains a parameters section (inputs) and a steps section (what to execute). It lives in your repo or a shared template repo.
# File: templates/steps/dotnet-build.yml
# Step template: Build and test a .NET application
parameters:
- name: dotnetVersion
type: string
default: '8.0.x'
- name: projectPath
type: string
default: '**/*.csproj'
- name: configuration
type: string
default: 'Release'
- name: runTests
type: boolean
default: true
steps:
# Install the specified .NET SDK version
- task: UseDotNet@2
displayName: 'Install .NET SDK ${{ parameters.dotnetVersion }}'
inputs:
packageType: 'sdk'
version: '${{ parameters.dotnetVersion }}'
# Restore NuGet packages
- script: dotnet restore ${{ parameters.projectPath }}
displayName: 'Restore packages'
# Build the project
- script: dotnet build ${{ parameters.projectPath }} --configuration ${{ parameters.configuration }} --no-restore
displayName: 'Build (${{ parameters.configuration }})'
# Conditionally run tests
- ${{ if eq(parameters.runTests, true) }}:
- script: dotnet test ${{ parameters.projectPath }} --configuration ${{ parameters.configuration }} --no-build --logger trx
displayName: 'Run tests'
- task: PublishTestResults@2
displayName: 'Publish test results'
inputs:
testResultsFormat: 'VSTest'
testResultsFiles: '**/*.trx'
Consuming a Step Template
The consuming pipeline references the template with a relative path and passes parameters:
# File: azure-pipelines.yml
# Pipeline that uses the step template
trigger:
branches:
include: [main]
pool:
vmImage: 'ubuntu-latest'
steps:
# Insert the build template with custom parameters
- template: templates/steps/dotnet-build.yml
parameters:
dotnetVersion: '8.0.x'
projectPath: 'src/MyApp/MyApp.csproj'
configuration: 'Release'
runTests: true
# Additional steps specific to this pipeline
- script: echo "Build complete — custom post-processing here"
displayName: 'Post-build step'
The template: keyword injects all steps from the template file at that point in the pipeline. Parameters flow in, and the template's steps execute as if they were written inline.
Job Templates
When you need to standardize not just the steps but also the agent pool, strategy, and job-level settings, use a job template. This is common for matrix builds, build + publish patterns, and validation gates.
Job Template for Build + Publish
# File: templates/jobs/build-publish.yml
# Job template: Build an application and publish artifacts
parameters:
- name: buildName
type: string
- name: pool
type: string
default: 'ubuntu-latest'
- name: dotnetVersion
type: string
default: '8.0.x'
- name: projectPath
type: string
- name: artifactName
type: string
default: 'drop'
jobs:
- job: Build_${{ replace(parameters.buildName, ' ', '_') }}
displayName: 'Build: ${{ parameters.buildName }}'
pool:
vmImage: '${{ parameters.pool }}'
steps:
- task: UseDotNet@2
displayName: 'Install .NET SDK'
inputs:
packageType: 'sdk'
version: '${{ parameters.dotnetVersion }}'
- script: |
dotnet restore ${{ parameters.projectPath }}
dotnet build ${{ parameters.projectPath }} --configuration Release --no-restore
dotnet publish ${{ parameters.projectPath }} --configuration Release --no-build --output $(Build.ArtifactStagingDirectory)
displayName: 'Restore, Build, Publish'
- task: PublishBuildArtifacts@1
displayName: 'Publish artifact: ${{ parameters.artifactName }}'
inputs:
PathtoPublish: '$(Build.ArtifactStagingDirectory)'
ArtifactName: '${{ parameters.artifactName }}'
Consuming the Job Template
# File: azure-pipelines.yml
# Pipeline consuming a job template
trigger:
branches:
include: [main, develop]
# Use the job template for both API and Worker service
jobs:
- template: templates/jobs/build-publish.yml
parameters:
buildName: 'API Service'
projectPath: 'src/Api/Api.csproj'
artifactName: 'api-drop'
- template: templates/jobs/build-publish.yml
parameters:
buildName: 'Worker Service'
projectPath: 'src/Worker/Worker.csproj'
artifactName: 'worker-drop'
pool: 'windows-latest' # Override default pool
Notice how the same template generates two independent jobs with different parameters. Each runs in parallel on its own agent. This pattern scales — add 10 services by adding 10 template references.
Stage Templates
Stage templates encapsulate complete deployment stages — including environment references, approval gates, and deployment strategies. This is where templates become genuinely powerful for multi-environment pipelines.
# File: templates/stages/deploy.yml
# Stage template: Deploy to an environment with approvals
parameters:
- name: environment
type: string
- name: artifactName
type: string
default: 'drop'
- name: appServiceName
type: string
- name: resourceGroup
type: string
- name: dependsOn
type: object
default: []
- name: condition
type: string
default: 'succeeded()'
stages:
- stage: Deploy_${{ parameters.environment }}
displayName: 'Deploy to ${{ parameters.environment }}'
dependsOn: ${{ parameters.dependsOn }}
condition: ${{ parameters.condition }}
jobs:
- deployment: deploy
displayName: 'Deploy to ${{ parameters.environment }}'
pool:
vmImage: 'ubuntu-latest'
environment: '${{ parameters.environment }}'
strategy:
runOnce:
deploy:
steps:
- download: current
artifact: '${{ parameters.artifactName }}'
- task: AzureWebApp@1
displayName: 'Deploy to App Service'
inputs:
azureSubscription: 'Azure-ServiceConnection'
appType: 'webApp'
appName: '${{ parameters.appServiceName }}'
resourceGroupName: '${{ parameters.resourceGroup }}'
package: '$(Pipeline.Workspace)/${{ parameters.artifactName }}/**/*.zip'
Composing a Multi-Stage Pipeline
# File: azure-pipelines.yml
# Multi-stage pipeline assembled from templates
trigger:
branches:
include: [main]
stages:
# Build stage (inline — or could also be a template)
- stage: Build
jobs:
- template: templates/jobs/build-publish.yml
parameters:
buildName: 'WebApp'
projectPath: 'src/WebApp/WebApp.csproj'
# Deploy to Dev (automatic after build)
- template: templates/stages/deploy.yml
parameters:
environment: 'dev'
appServiceName: 'myapp-dev'
resourceGroup: 'rg-myapp-dev'
dependsOn: ['Build']
# Deploy to Staging (automatic after dev)
- template: templates/stages/deploy.yml
parameters:
environment: 'staging'
appServiceName: 'myapp-staging'
resourceGroup: 'rg-myapp-staging'
dependsOn: ['Deploy_dev']
# Deploy to Production (manual approval via environment)
- template: templates/stages/deploy.yml
parameters:
environment: 'production'
appServiceName: 'myapp-prod'
resourceGroup: 'rg-myapp-prod'
dependsOn: ['Deploy_staging']
Three environments, same deployment logic, zero duplication. Approvals are configured on the environment resource in Azure DevOps (not in the YAML), so the template stays generic.
Variable Templates
Variable templates externalize configuration into separate YAML files. This separates what to do (pipeline logic) from what values to use (configuration), making it trivial to have per-environment settings.
# File: templates/variables/dev.yml
# Variable template: Development environment settings
variables:
environment: 'dev'
azureSubscription: 'Azure-Dev-ServiceConnection'
resourceGroup: 'rg-myapp-dev'
appServiceName: 'myapp-dev'
appInsightsKey: '$(DEV_APPINSIGHTS_KEY)' # from pipeline secrets
minReplicas: 1
maxReplicas: 3
# File: templates/variables/production.yml
# Variable template: Production environment settings
variables:
environment: 'production'
azureSubscription: 'Azure-Prod-ServiceConnection'
resourceGroup: 'rg-myapp-prod'
appServiceName: 'myapp-prod'
appInsightsKey: '$(PROD_APPINSIGHTS_KEY)'
minReplicas: 3
maxReplicas: 20
Using Variable Templates in a Pipeline
# File: azure-pipelines.yml
# Pipeline that loads variables from a template
trigger:
branches:
include: [main]
variables:
# Load shared variables for this environment
- template: templates/variables/dev.yml
# You can also combine with inline variables
- name: buildConfiguration
value: 'Release'
pool:
vmImage: 'ubuntu-latest'
steps:
- script: echo "Deploying to $(environment) — App Service: $(appServiceName)"
displayName: 'Show configuration'
- script: echo "Replicas: min=$(minReplicas), max=$(maxReplicas)"
displayName: 'Show scaling config'
Variable templates are especially useful when combined with parameters — a pipeline can choose which variable file to load based on a parameter value.
Extends Templates (Enterprise Governance)
This is the most powerful — and most important — template type. An extends template defines the entire pipeline skeleton. Consuming pipelines don't write their own structure; they fill in hooks provided by the template. The template "wraps" the pipeline.
Why Extends Templates Exist
Imagine you're a platform engineering team responsible for 200 developers across 80 repositories. You need every pipeline to:
- Run credential scanning (detect leaked secrets)
- Run SAST (static application security testing)
- Generate an SBOM (software bill of materials)
- Only deploy to approved environments
With step/job templates, teams can forget to include them. With extends + required template policies, teams cannot skip them.
Creating an Extends Template
# File: templates/pipeline-skeleton.yml (in central shared repo)
# Extends template: Enforces security scanning on every pipeline
parameters:
- name: buildSteps
type: stepList
default: []
- name: testSteps
type: stepList
default: []
- name: pool
type: string
default: 'ubuntu-latest'
stages:
- stage: Build
displayName: 'Build & Security Scan'
jobs:
- job: BuildAndScan
displayName: 'Build, Test & Security Scan'
pool:
vmImage: '${{ parameters.pool }}'
steps:
# === MANDATORY: Credential scanning (teams CANNOT remove this) ===
- task: CredScan@3
displayName: ' Credential Scan (enforced)'
inputs:
toolMajorVersion: 'V2'
# === TEAM-PROVIDED: Build steps injected here ===
- ${{ each step in parameters.buildSteps }}:
- ${{ step }}
# === TEAM-PROVIDED: Test steps injected here ===
- ${{ each step in parameters.testSteps }}:
- ${{ step }}
# === MANDATORY: SAST scanning (teams CANNOT remove this) ===
- task: RoslynAnalyzers@3
displayName: ' SAST Analysis (enforced)'
# === MANDATORY: SBOM generation ===
- task: ManifestGeneratorTask@0
displayName: ' Generate SBOM (enforced)'
inputs:
BuildDropPath: '$(Build.ArtifactStagingDirectory)'
Consuming an Extends Template
# File: azure-pipelines.yml (in team's repo)
# This pipeline EXTENDS the central template — it cannot remove mandatory steps
trigger:
branches:
include: [main]
resources:
repositories:
- repository: templates
type: git
name: Platform/pipeline-templates
ref: refs/tags/v2.1.0
extends:
template: templates/pipeline-skeleton.yml@templates
parameters:
pool: 'ubuntu-latest'
buildSteps:
- script: dotnet restore src/MyApp.csproj
displayName: 'Restore'
- script: dotnet build src/MyApp.csproj --configuration Release
displayName: 'Build'
- script: dotnet publish src/MyApp.csproj -o $(Build.ArtifactStagingDirectory)
displayName: 'Publish'
testSteps:
- script: dotnet test tests/MyApp.Tests.csproj --logger trx
displayName: 'Unit Tests'
How Extends Wraps the Pipeline
flowchart TD
A["Team's azure-pipelines.yml"] -->|extends| B[Central Template]
B --> C["Credential Scan (mandatory)"]
C --> D["Team's Build Steps (injected)"]
D --> E["Team's Test Steps (injected)"]
E --> F["SAST Analysis (mandatory)"]
F --> G["SBOM Generation (mandatory)"]
The team controls what they build and test. The platform team controls the security envelope around it. Neither can override the other.
Advanced Template Parameters
Template parameters go far beyond simple strings. Azure Pipelines supports rich parameter types that enable complex, type-safe template configurations.
Parameter Types
| Type | Description | Example Value |
|---|---|---|
string |
Free-form text | 'ubuntu-latest' |
number |
Integer value | 3 |
boolean |
True/false | true |
object |
YAML mapping (key-value pairs) | { key: value } |
stepList |
List of pipeline steps | Array of step definitions |
jobList |
List of pipeline jobs | Array of job definitions |
stageList |
List of pipeline stages | Array of stage definitions |
Constrained Parameters with Allowed Values
# Template with constrained and validated parameters
parameters:
- name: environment
type: string
values: # Only these values are allowed
- dev
- staging
- production
- name: deployRegions
type: object # Complex object parameter
default:
primary: 'eastus'
secondary: 'westeurope'
enableFailover: true
- name: extraSteps
type: stepList # Teams can inject custom steps
default: []
Conditional Logic with Parameters
# File: templates/steps/deploy-with-conditionals.yml
# Template demonstrating conditional logic based on parameters
parameters:
- name: environment
type: string
- name: runSmoke
type: boolean
default: true
steps:
- script: echo "Deploying to ${{ parameters.environment }}"
displayName: 'Deploy'
# Only run smoke tests in non-production
- ${{ if and(eq(parameters.runSmoke, true), ne(parameters.environment, 'production')) }}:
- script: npm run test:smoke
displayName: 'Smoke Tests'
# Production gets a canary deployment step
- ${{ if eq(parameters.environment, 'production') }}:
- script: |
echo "Canary deployment — routing 10% traffic to new version"
az webapp traffic-routing set --name myapp --resource-group rg-prod \
--distribution staging=10
displayName: 'Canary: 10% traffic routing'
# Always run health check
- script: curl --fail https://myapp-${{ parameters.environment }}.azurewebsites.net/health
displayName: 'Health Check'
Template expressions (${{ }}) are evaluated at compile time — before the pipeline runs. This means conditional steps are included or excluded from the plan before any agent picks up the job.
Template Expressions & Iteration
Template expressions let you generate pipeline YAML dynamically — looping over lists, conditionally inserting blocks, and transforming data. This is where templates become a genuine metaprogramming layer over your pipelines.
Iterating with ${{ each }}
# File: templates/stages/multi-env-deploy.yml
# Template that generates a deploy stage for EACH environment in a list
parameters:
- name: environments
type: object
default:
- name: dev
appService: myapp-dev
resourceGroup: rg-dev
dependsOn: Build
- name: staging
appService: myapp-staging
resourceGroup: rg-staging
dependsOn: Deploy_dev
- name: production
appService: myapp-prod
resourceGroup: rg-prod
dependsOn: Deploy_staging
stages:
- ${{ each env in parameters.environments }}:
- stage: Deploy_${{ env.name }}
displayName: 'Deploy → ${{ env.name }}'
dependsOn: ${{ env.dependsOn }}
jobs:
- deployment: deploy
pool:
vmImage: 'ubuntu-latest'
environment: '${{ env.name }}'
strategy:
runOnce:
deploy:
steps:
- download: current
artifact: 'drop'
- task: AzureWebApp@1
inputs:
azureSubscription: 'Azure-ServiceConnection'
appName: '${{ env.appService }}'
resourceGroupName: '${{ env.resourceGroup }}'
package: '$(Pipeline.Workspace)/drop/**/*.zip'
One template, three stages generated. Add a fourth environment? Just add an entry to the environments list — no template changes needed.
Conditional Insertion with ${{ if }}
# File: templates/jobs/build-matrix.yml
# Template that conditionally adds platforms to a build matrix
parameters:
- name: includeWindows
type: boolean
default: true
- name: includeLinux
type: boolean
default: true
- name: includeMac
type: boolean
default: false
jobs:
- ${{ if eq(parameters.includeLinux, true) }}:
- job: Build_Linux
pool:
vmImage: 'ubuntu-latest'
steps:
- script: echo "Building on Linux"
- ${{ if eq(parameters.includeWindows, true) }}:
- job: Build_Windows
pool:
vmImage: 'windows-latest'
steps:
- script: echo "Building on Windows"
- ${{ if eq(parameters.includeMac, true) }}:
- job: Build_Mac
pool:
vmImage: 'macos-latest'
steps:
- script: echo "Building on macOS"
Combining Iteration with Conditionals
# File: templates/steps/notify.yml
# Generate notification steps based on a list of channels
parameters:
- name: channels
type: object
default:
- type: slack
webhook: '$(SLACK_WEBHOOK)'
- type: teams
webhook: '$(TEAMS_WEBHOOK)'
- name: status
type: string
default: 'succeeded'
steps:
- ${{ each channel in parameters.channels }}:
- ${{ if eq(channel.type, 'slack') }}:
- script: |
curl -X POST -H 'Content-type: application/json' \
--data '{"text":"Pipeline ${{ parameters.status }}"}' \
${{ channel.webhook }}
displayName: 'Notify Slack'
- ${{ if eq(channel.type, 'teams') }}:
- script: |
curl -X POST -H 'Content-type: application/json' \
--data '{"text":"Pipeline ${{ parameters.status }}"}' \
${{ channel.webhook }}
displayName: 'Notify Teams'
${{ }}) resolve at compile time — they generate YAML. Runtime expressions ($[ ]) evaluate during execution. Use template expressions for structural decisions (which stages/jobs/steps exist) and runtime expressions for value decisions (which branch to deploy).
Shared Template Repositories
In real organizations, templates don't live in each repo — they live in a central template repository shared across all projects. This enables versioning, code review, and controlled rollouts of template changes.
Template Repository Structure
# Recommended structure for a shared template repo
# Repository: Platform/pipeline-templates
pipeline-templates/
├── README.md # Documentation and usage guide
├── CHANGELOG.md # Version history
├── templates/
│ ├── steps/
│ │ ├── dotnet-build.yml # .NET build + test steps
│ │ ├── docker-build.yml # Container build + push steps
│ │ ├── npm-build.yml # Node.js build + test steps
│ │ └── security-scan.yml # Mandatory security scanning
│ ├── jobs/
│ │ ├── build-publish.yml # Build + publish artifacts
│ │ └── integration-test.yml # Integration test job
│ ├── stages/
│ │ ├── deploy-appservice.yml # App Service deployment
│ │ ├── deploy-aks.yml # Kubernetes deployment
│ │ └── deploy-functions.yml # Azure Functions deployment
│ ├── variables/
│ │ ├── dev.yml # Dev environment config
│ │ ├── staging.yml # Staging environment config
│ │ └── production.yml # Production environment config
│ └── pipelines/
│ ├── dotnet-webapp.yml # Extends: full .NET web app pipeline
│ └── container-app.yml # Extends: containerized app pipeline
└── examples/
├── simple-dotnet.yml # Example: consuming .NET templates
└── multi-stage.yml # Example: full multi-stage pipeline
Referencing Templates from Another Repository
# File: azure-pipelines.yml (in your application repo)
# Consuming templates from a shared central repository
trigger:
branches:
include: [main]
# Declare the external template repository
resources:
repositories:
- repository: templates # Alias used in template references
type: git # Azure Repos Git
name: Platform/pipeline-templates # Project/RepoName
ref: refs/tags/v2.1.0 # Pin to a specific tag!
variables:
- template: templates/variables/dev.yml@templates
stages:
- stage: Build
jobs:
- template: templates/jobs/build-publish.yml@templates
parameters:
buildName: 'MyApp'
projectPath: 'src/MyApp.csproj'
- template: templates/stages/deploy-appservice.yml@templates
parameters:
environment: 'dev'
appServiceName: '$(appServiceName)'
resourceGroup: '$(resourceGroup)'
dependsOn: ['Build']
The @templates suffix tells Azure Pipelines to look in the declared repository alias, not the current repo.
ref: refs/tags/v2.1.0 so pipeline behavior doesn't change unexpectedly when the template repo is updated. Teams can upgrade by bumping the tag reference in their pipeline YAML.
Cross-Project References
# Referencing templates from a different Azure DevOps project
resources:
repositories:
- repository: shared
type: git
name: InfraProject/shared-pipelines # Different project
ref: refs/tags/v3.0.0
# For GitHub-hosted templates:
resources:
repositories:
- repository: github_templates
type: github
name: my-org/pipeline-templates
endpoint: 'GitHub-ServiceConnection'
ref: refs/tags/v1.0.0
Required Templates (Security Policy)
Required templates are the enforcement mechanism — a project-level policy that prevents any pipeline from running unless it extends an approved template. This is the governance tool that makes extends templates meaningful at scale.
How It Works
- Platform team creates an extends template with mandatory security steps
- Admin configures "Required YAML templates" in Project Settings → Pipelines → Settings
- Policy specifies: the required template file path and the repository it lives in
- Any pipeline that doesn't
extendsthe required template will be blocked from running
Configuration Steps
# The required template policy is configured in the UI:
# Project Settings → Pipelines → Settings → "Required YAML templates"
#
# Configuration:
# Repository: Platform/pipeline-templates
# Path: templates/pipelines/dotnet-webapp.yml
# Ref: refs/tags/v2.1.0
# Applies to: All pipelines (or specific folders/repos)
#
# Result:
# Pipelines without `extends:` pointing to this template → BLOCKED
# Pipelines that extend the approved template → ALLOWED
Governance Architecture
flowchart TD
A[Developer pushes code] --> B[Pipeline triggered]
B --> C{Uses required template?}
C -->|Yes| D[Pipeline executes]
C -->|No| E[Pipeline BLOCKED]
D --> F[Mandatory security steps run]
F --> G["Team's custom steps run"]
G --> H[Mandatory post-steps run]
H --> I[Pipeline completes]
Case Study: Enterprise Template Library
Building a Shared Template Library at Scale
Challenge: A financial services company with 200 engineers across 80+ repositories had inconsistent CI/CD practices. Security scanning was optional (and often skipped). Deployment procedures varied wildly — some teams had 3-stage pipelines, others pushed directly to production. A compliance audit flagged 40% of pipelines as non-compliant.
Solution: Centralized Template Library
Phase 1 — Foundation (Weeks 1–2):
- Created
Platform/pipeline-templatesrepository with step, job, and stage templates - Built templates for the three primary tech stacks: .NET, Node.js, and Python
- Established versioning via Git tags (semver: v1.0.0, v1.1.0, v2.0.0)
Phase 2 — Migration (Weeks 3–6):
- Migrated 20 "early adopter" repos to use shared templates
- Created an extends template with mandatory credential scanning + SBOM
- Published migration guide with before/after examples
Phase 3 — Enforcement (Weeks 7–8):
- Enabled "Required YAML templates" policy for all new pipelines
- Gave existing pipelines a 4-week grace period to migrate
- Provided office hours and PR reviews for teams needing help
Results After 3 Months
- 70% reduction in total pipeline YAML across the organization
- 100% compliance with security scanning requirements
- 15-minute average to set up CI/CD for a new repository (from 2+ hours)
- Single PR to roll out pipeline improvements to all 80 repos (update the template, teams bump the tag)
- Zero security audit findings related to CI/CD in subsequent audits
Template Versioning Strategy
# Versioning strategy for the shared template repository:
#
# MAJOR (v2.0.0) — Breaking changes:
# - Renamed parameters
# - Removed template files
# - Changed parameter types
# Teams MUST update their pipelines when bumping major versions
#
# MINOR (v2.1.0) — Backwards-compatible additions:
# - New optional parameters (with defaults)
# - New template files
# - New steps added to extends template
# Teams can bump safely without pipeline changes
#
# PATCH (v2.1.1) — Bug fixes:
# - Fixed typos in display names
# - Corrected task version references
# - Documentation updates
#
# Tagging workflow:
# git tag -a v2.1.0 -m "Add Node.js 20 support, add Docker layer caching"
# git push origin v2.1.0
Exercises
Step Templates for Build & Test
Goal: Create reusable step templates and consume them from two different pipelines.
- Create
templates/steps/npm-build.yml— a step template that installs Node.js, runsnpm ci, and runsnpm run build. Accept parameters fornodeVersion(default:'20.x') andworkingDirectory(default:'.'). - Create
templates/steps/npm-test.yml— a step template that runsnpm testand publishes test results. Accept aworkingDirectoryparameter. - Create two pipelines (
pipeline-frontend.ymlandpipeline-api.yml) that both use these templates with differentworkingDirectoryvalues.
Success criteria: Both pipelines use the same template files. Changing the template updates both pipelines.
Stage Template for Deployment
Goal: Build a deploy stage template and compose a 3-environment pipeline.
- Create
templates/stages/deploy-webapp.ymlwith parameters for:environment,appServiceName,resourceGroup,dependsOn. - The template should use a
deploymentjob withrunOncestrategy. - Create a pipeline that uses this template three times — dev, staging, production — with appropriate
dependsOnchains. - Add a condition: production only deploys from the
mainbranch.
Success criteria: Pipeline shows 3 sequential stages. Production is skipped on non-main branches.
Extends Template with Enforced Scanning
Goal: Create an extends template that mandates security steps and test consuming it.
- Create
templates/pipelines/secure-build.yml— an extends template with:- Mandatory
CredScanstep before team steps - A
buildStepsparameter (type:stepList) for team code - Mandatory SBOM generation after team steps
- Mandatory
- Create a consuming pipeline that extends this template and provides build steps for a .NET application.
- Verify that removing the
extends:block (switching to direct YAML) would be blocked by policy.
Success criteria: Consuming pipeline cannot remove mandatory steps. Security scanning always runs.
Shared Template Repository with Version Tagging
Goal: Set up a multi-repo template consumption pattern with version control.
- Create a new Azure Repos repository called
pipeline-templates. - Add step, job, and stage templates with a
templates/folder structure. - Create a Git tag
v1.0.0on the initial commit. - In a separate application repo, reference the template repo with
ref: refs/tags/v1.0.0. - Make a change to the template repo, create tag
v1.1.0, and verify the consuming pipeline still usesv1.0.0until you update the ref.
Success criteria: Template changes don't affect consuming pipelines until they explicitly bump the version tag.
Next in the Bootcamp
In Module 7: Multi-Stage Pipelines & Deployment, we'll cover deployment strategies (rolling, canary, blue-green), environment approvals and gates, service connections, deployment jobs, and building production-grade release pipelines with rollback capabilities.