7.3 Reusable Elements: Templates, Parameters & Variables
Key Takeaways
- YAML templates modularize pipeline logic across step, job, and stage tiers, enabling enterprise governance, compliance enforcement, and cross-team standardization.
- Templates can be sourced locally within the repository or externally from centralized governance repositories declared in the resources: repositories: block.
- The extends template pattern enforces strict enterprise compliance by requiring application pipelines to inject logic into a predefined organizational template skeleton.
- Variable syntax spans three distinct evaluation phases: compile-time template expressions (${{ ... }}), job initialization expressions ($[ ... ]), and task runtime macro expansion ($( ... )).
- Passing output variables across jobs and stages requires setting isOutput=true via VSO logging commands and referencing them via the dependencies or stageDependencies context.
7.3 Reusable Elements: Templates, Parameters & Variables
As enterprise organizations scale their adoption of Azure DevOps, maintaining monolithic, copy-pasted pipeline definitions across dozens or hundreds of repositories quickly creates maintenance bottlenecks, security drifts, and compliance violations. A security update to a container scanning tool or a change in artifact signing requirements would require editing hundreds of individual YAML files.
Azure Pipelines provides enterprise-grade modularization mechanisms through YAML Templates, Typed Parameters, and a multi-tiered Variable Evaluation Engine. Mastering these constructs is fundamental to architecting scalable CI/CD pipelines and excelling on the AZ-400 exam.
1. YAML Templates Architecture: Step, Job, Stage, and Extends
Templates allow you to define reusable logic in separate YAML files and include them in multiple pipeline definitions. Templates can be structured at four architectural levels:
- Step Templates: Encapsulate a sequence of one or more steps (e.g., configuring an SDK, running SonarQube analysis, signing binaries).
- Job Templates: Encapsulate one or more complete jobs (e.g., standard build matrix, container packaging workflow).
- Stage Templates: Encapsulate entire stages (e.g., an entire standardized QA deployment and automated verification stage).
- Extends Templates (Governance): Inverts the relationship—rather than an application pipeline importing snippets, the application pipeline extends a centralized enterprise template. This ensures that organizational security gates, scanning, and compliance checks cannot be bypassed by development teams.
Step Template Example (Local Reference)
Consider a reusable step template located at .pipelines/templates/security-scan.yml:
# File: .pipelines/templates/security-scan.yml
parameters:
- name: scanLevel
type: string
default: 'standard'
values:
- 'quick'
- 'standard'
- 'deep'
- name: failOnVulnerabilities
type: boolean
default: true
steps:
- script: echo "Running vulnerability scan with depth ${{ parameters.scanLevel }}..."
displayName: 'Execute Security Scanner'
- ${{ if eq(parameters.failOnVulnerabilities, true) }}:
- script: echo "Enforcing zero-tolerance security gate..."
displayName: 'Verify Security Vulnerability Gate'
Consuming the step template in the main azure-pipelines.yml:
# File: azure-pipelines.yml
trigger:
- main
pool:
vmImage: 'ubuntu-latest'
jobs:
- job: BuildAndScan
steps:
- script: echo "Compiling application..."
- template: .pipelines/templates/security-scan.yml
parameters:
scanLevel: 'deep'
failOnVulnerabilities: true
Centralized Multi-Repository Templates
In enterprise environments, templates are maintained in a dedicated, secure repository owned by the Central DevOps or Platform Engineering team. Application repositories reference this external repository via the resources: repositories: declaration:
# File: azure-pipelines.yml (Application Repository)
resources:
repositories:
- repository: SharedTemplates
type: git
name: CorePlatform/PipelineTemplates # ProjectName/RepoName
ref: refs/heads/main # Branch, tag, or commit
stages:
- stage: Build
jobs:
- job: AppBuild
steps:
- checkout: self
# Reference external template using @<repoIdentifier>
- template: steps/dotnet-build.yml@SharedTemplates
parameters:
buildConfiguration: 'Release'
enableTelemetry: true
The extends Pattern for Security and Governance
The extends template syntax is a critical AZ-400 architectural pattern. When using extends, the central template controls the execution skeleton of the pipeline, and the child pipeline can only supply approved parameters or inject steps into designated hook points.
# Central Governance Template: CoreGovernance/templates/secure-pipeline.yml
parameters:
- name: buildSteps
type: stepList
default: []
stages:
- stage: ComplianceAndBuild
jobs:
- job: MandatorySecurityScan
steps:
- script: run-mandatory-org-audit.sh
displayName: 'Mandatory InfoSec Audit'
- job: DeveloperBuild
dependsOn: MandatorySecurityScan
steps:
# Developer build steps injected here
- ${{ parameters.buildSteps }}
# Central mandatory post-build validation
- script: upload-sbom-to-central-audit.sh
displayName: 'Upload SBOM'
# Application Repository azure-pipelines.yml using extends
resources:
repositories:
- repository: GovernanceRepo
type: git
name: InfoSec/EnterpriseTemplates
extends:
template: templates/secure-pipeline.yml@GovernanceRepo
parameters:
buildSteps:
- script: dotnet build
- script: dotnet test
2. Template Parameters: Types, Validation, and Compile-Time Logic
Parameters are defined at the top of a template or root pipeline. Unlike variables, parameters are strongly typed, evaluated strictly at compile time, and validated before the pipeline begins execution.
Supported Parameter Types
string: Free-form text (can be constrained using avaluesenumeration list).number: Numeric values.boolean:trueorfalse.object: Complex YAML structures, arrays, or key-value dictionaries.step/stepList: A single step or a list of steps injected dynamically.job/jobList: A job or list of jobs.stage/stageList: A stage or list of stages.
Parameter Validation
Parameters support explicit validation rules using the values keyword to enforce allowed options:
parameters:
- name: environmentName
displayName: 'Deployment Environment Target'
type: string
default: 'Development'
values:
- 'Development'
- 'Testing'
- 'Staging'
- 'Production'
If a pipeline run is triggered with a value outside this allowed list, Azure Pipelines fails during compile-time validation before any agent is provisioned.
3. Variable Evaluation Phases: Compile-Time vs. Runtime
A frequent source of bugs and exam questions is conflating variable syntaxes. Azure Pipelines evaluates variables across three distinct operational phases:
[ Phase 1: Compile Time ] ──► [ Phase 2: Job Initialization ] ──► [ Phase 3: Task Execution ]
Syntax: ${{ ... }} Syntax: $[ ... ] Syntax: $( ... )
Template Parsing & Parsing Agent Scheduling & Conditions In-Task Macro Expansion
Detailed Variable Syntax Comparison Matrix
| Syntax | Name / Mechanism | Evaluation Timing | Primary Context / Location | Handles Secrets? |
|---|---|---|---|---|
${{ <expression> }} | Template Expression | Compile time (before pipeline runs) | Template insertion, loops (${{ each }}), conditional blocks (${{ if }}), parameter parsing | No (Secrets do not exist at compile time) |
$[ <expression> ] | Runtime Expression | Job initialization time | Stage/job condition:, job-level variables: definitions, output variables from prior jobs | No (Not expanded into secret text) |
$( <variableName> ) | Macro Syntax | Task execution runtime (immediately prior to step start) | Task inputs, step display names, environment variables | Yes (Secrets are injected and masked in logs) |
Common Syntactic Pitfalls
- Trying to read a secret via template syntax
${{ ... }}: At compile time, secrets have not been decrypted or fetched from Key Vault. Using${{ variables.secretKey }}produces an empty string. - Using macro syntax
$( ... )in a condition expression: The pipeline parser does not expand macros inside expression functions likeeq(...). Writingcondition: eq('$(Build.SourceBranch)', 'refs/heads/main')will evaluate the literal text'$(Build.SourceBranch)'. You must writecondition: eq(variables['Build.SourceBranch'], 'refs/heads/main'). - Expecting compile-time loops to iterate over runtime output: Template loops (
${{ each item in ... }}) execute only once when the YAML file is parsed into memory. They cannot iterate over lists generated dynamically by earlier build steps.
4. Passing Output Variables Across Steps, Jobs, and Stages
A pipeline often needs to dynamically compute a value in one step (e.g., an image tag, a release version, an Azure resource ID) and consume it in downstream steps, jobs, or stages.
Mechanism 1: Step-to-Step Within the Same Job
To pass a variable to subsequent steps within the same job, output an Azure DevOps logging command to standard out:
echo "##vso[task.setvariable variable=DYNAMIC_VERSION]1.4.2"
In the next step on the same agent, access it as $(DYNAMIC_VERSION) or via the environment variable $DYNAMIC_VERSION.
Mechanism 2: Job-to-Job Within the Same Stage
To pass an output variable to a downstream job within the same stage:
- The producing step must set
isOutput=true. - The producing step must define a
name:attribute. - The consumer job must declare a dependency on the producer job using
dependsOn. - The consumer job maps the variable at the job level using runtime expression syntax:
$[ dependencies.<JobName>.outputs['<StepName>.<VariableName>'] ].
jobs:
- job: ProducerJob
steps:
- script: |
BUILD_HASH=$(git rev-parse --short HEAD)
echo "##vso[task.setvariable variable=appHash;isOutput=true]$BUILD_HASH"
name: VersionStep
displayName: 'Generate Version Hash'
- job: ConsumerJob
dependsOn: ProducerJob
variables:
# Map output variable using dependencies context
computedHash: $[ dependencies.ProducerJob.outputs['VersionStep.appHash'] ]
steps:
- script: echo "Received build hash from producer: $(computedHash)"
Mechanism 3: Stage-to-Stage Across Stages
To pass an output variable across stage boundaries:
- The producing step sets
isOutput=trueand has aname:attribute. - The consumer stage specifies
dependsOn: <ProducerStage>. - The consumer job inside the downstream stage maps the variable using the
stageDependenciescontext:$[ stageDependencies.<ProducerStage>.<ProducerJob>.outputs['<StepName>.<VariableName>'] ].
stages:
- stage: BuildStage
jobs:
- job: BuildJob
steps:
- script: |
CLUSTER_IP="10.240.0.45"
echo "##vso[task.setvariable variable=targetIp;isOutput=true]$CLUSTER_IP"
name: NetworkStep
displayName: 'Discover Target IP'
- stage: DeployStage
dependsOn: BuildStage
jobs:
- job: DeployJob
variables:
# Map cross-stage output variable
serverIp: $[ stageDependencies.BuildStage.BuildJob.outputs['NetworkStep.targetIp'] ]
steps:
- script: echo "Deploying payload to destination IP: $(serverIp)"
5. Realistic Exam Scenarios & Common Traps
Scenario: Enforcing Enterprise Security Compliance via Extends
Context: An enterprise financial institution with 150 development teams mandates that every container image built in Azure Pipelines must undergo static vulnerability scanning with Prisma Cloud and generate an SBOM using Syft. Developers frequently edit their azure-pipelines.yml files to comment out scanning steps when deadlines approach.
Solution: Transition the organization from step templates to the extends template pattern. Define a centralized template in an administrative repository (InfoSec/ComplianceTemplates). In the root application repositories, restrict pipeline definitions so they can only provide parameters to extends: template: secure-build.yml@ComplianceTemplates. Developers cannot modify the sequence of stages, ensuring compliance scans execute before any image is pushed to the container registry.
Common Exam Traps to Avoid
- Trap: Omitting
name:on the step generating an output variable. When calling##vso[task.setvariable ...;isOutput=true], if the step lacks aname:attribute (e.g.,name: OutputStep), the output variable will not be registered in the job'soutputsdictionary, causing downstream jobs to read empty values. - Trap: Conflating
dependenciesandstageDependencies. Within the same stage, downstream jobs referencedependencies.<Job>.outputs[...]. Across different stages, you must usestageDependencies.<Stage>.<Job>.outputs[...]. - Trap: Attempting to evaluate runtime variables at compile time. If you write
${{ if eq(variables['Build.SourceBranch'], 'refs/heads/main') }}, template expressions are evaluated before the branch context is established during queuing. Usecondition: eq(variables['Build.SourceBranch'], 'refs/heads/main')instead.
A release architect wants to set a dynamically generated semantic version number in an initial compilation stage and consume that exact version number in a downstream deployment stage. In the producer stage, the job 'BuildJob' contains a step named 'VersionStep' that executes 'echo "##vso[task.setvariable variable=SemVer;isOutput=true]1.2.3"'. How must the consumer job in the downstream stage declare and consume this variable?
An enterprise organization requires that all 80 microservice delivery pipelines strictly execute a corporate SonarQube quality gate and an image vulnerability scan prior to deploying any code to production. Individual development teams currently own their respective 'azure-pipelines.yml' files and have occasionally commented out these scanning steps to expedite emergency releases. What is the recommended architectural solution to prevent development teams from bypassing these mandatory checks?
A DevOps engineer is designing an Azure Pipelines YAML template and needs to configure conditional behavior based on the target deployment environment. The engineer must choose between template syntax '${{ if ... }}' and runtime condition syntax 'condition: ...'. Which statement correctly describes the architectural difference and appropriate usage?