Most Azure DevOps pipelines start as a YAML file someone copy-pasted from the docs. That's fine. The problem is what happens when that pipeline is in production two years later — 800 lines long, no templates, secrets hardcoded in variables, no gates between environments, and everyone's afraid to touch it.
This post covers the architecture decisions that prevent that outcome.
Template Libraries: The Foundation
The most important structural decision in an Azure DevOps organization is separating pipeline templates from application code. Templates define how things are built and deployed. Application repositories define what is built.
A Separate Templates Repository
azure-pipelines-templates/ # Separate repo
templates/
jobs/
build-dotnet.yml
build-node.yml
run-tests.yml
security-scan.yml
steps/
install-dependencies.yml
publish-artifacts.yml
notify-teams.yml
stages/
deploy-app-service.yml
deploy-aks.yml
deploy-function.yml
Application pipelines reference templates via repository resource:
# In application repo: azure-pipelines.yml
resources:
repositories:
- repository: templates
type: git
name: MyOrg/azure-pipelines-templates
ref: refs/tags/v2.1.0 # Pin to a version — never use main
stages:
- template: stages/deploy-app-service.yml@templates
parameters:
environment: production
appServiceName: $(APP_SERVICE_NAME)Pinning to a tag (not a branch) is critical. If a template change breaks all pipelines that reference main, you've had a bad day. Pin to versions, and require template changes to go through their own review and release process.
⚠ Warning
Never reference pipeline templates via refs/heads/main. A broken template commit will take down every pipeline that uses it simultaneously. Tag your releases.
Environment Gates
Environments in Azure DevOps are where deployment safety lives. They're not just labels — they're approval gates, deployment history, and resource associations.
Define Environments Explicitly
stages:
- stage: DeployDev
jobs:
- deployment: DeployApp
environment: development # Maps to an Environment in AzDO
strategy:
runOnce:
deploy:
steps:
- template: steps/deploy-app-service.yml@templates
- stage: DeployProd
dependsOn: DeployDev
condition: succeeded()
jobs:
- deployment: DeployApp
environment: production # Requires manual approval gateConfigure the production environment in Azure DevOps with:
- ›Required reviewers — at minimum one other person must approve before deployment proceeds
- ›Branch control — only the
mainbranch can deploy to production - ›Exclusive lock — prevents concurrent deployments to the same environment
Approval Notifications
Wire environment approvals to your team communication channel. An approval request that nobody sees defeats the purpose.
# In environment settings: configure approvals + Teams webhook notification
# Approval timeout: 4 hours (auto-reject if nobody responds)Secrets and Variable Groups
The most common pipeline security failure: secrets in YAML or in pipeline variables that anyone in the organization can view.
Variable Groups Linked to Key Vault
variables:
- group: production-secrets # Variable group backed by Key VaultVariable groups linked to Azure Key Vault fetch secrets at pipeline runtime, not at definition time. The pipeline agent gets a token-scoped pull — engineers never see the secret values, and rotation happens in Key Vault without touching any pipeline config.
Restrict variable group access: only the service connections and pipelines that actually need a secret should have access to the group containing it.
💡 Tip
Audit your variable groups quarterly. Teams accumulate secrets they no longer use. A stale database password sitting in a variable group is an unnecessary exposure.
Service Connection Scoping
Service connections are the most over-permissioned thing in most Azure DevOps organizations. Teams create a single service connection with Contributor on the whole subscription and use it for everything.
The right pattern:
- ›One service connection per environment per workload, scoped to the minimum resource group needed
- ›Federated identity (workload identity federation) instead of client secrets — no secrets to rotate, no secrets to leak
- ›Restrict service connection access in project settings — only the pipelines that need it can use it
# Modern: federated credentials, no client secret
- task: AzureCLI@2
inputs:
azureSubscription: sc-myapp-prod-deployment # Scoped service connection
scriptType: bash
scriptLocation: inlineScript
inlineScript: |
az webapp deploy --name $(APP_NAME) --resource-group $(RG_NAME)Artifact Integrity
For applications where you care about supply chain integrity (and PCI/SOC 2 compliance requires you to), build artifacts once and promote them — don't rebuild per environment.
stages:
- stage: Build
jobs:
- job: BuildAndPublish
steps:
- task: DotNetCoreCLI@2
inputs:
command: publish
publishWebProjects: true
arguments: --configuration Release --output $(Build.ArtifactStagingDirectory)
- task: PublishPipelineArtifact@1
inputs:
artifactName: app-drop
targetPath: $(Build.ArtifactStagingDirectory)
- stage: DeployProd
dependsOn: DeployDev
jobs:
- deployment: Deploy
environment: production
strategy:
runOnce:
deploy:
steps:
- download: current
artifact: app-drop # Same artifact, promoted from devThe artifact deployed to prod is byte-for-byte identical to what was tested in dev. No rebuilds, no environmental drift.
Security Scanning in the Pipeline
Security scanning belongs in the pipeline, not in a separate manual process that happens quarterly.
A baseline security stage:
- stage: SecurityScan
dependsOn: Build
jobs:
- job: SAST
steps:
- task: Bash@3
displayName: OWASP Dependency Check
inputs:
targetType: inline
script: |
dependency-check --project $(Build.Repository.Name) \
--scan . \
--format JSON \
--out dependency-check-report.json \
--failOnCVSS 7 # Fail on high+ severity
- task: PublishPipelineArtifact@1
inputs:
artifactName: security-reports
targetPath: dependency-check-report.jsonThe pipeline fails on high-severity CVEs. Security findings become build failures, not recommendations.
The Maintenance Reality
Well-structured pipelines are significantly cheaper to maintain. Template changes propagate automatically. Security scans catch issues before production. Approval gates prevent unauthorized deployments. Artifact promotion removes rebuild risk.
The upfront cost of getting this right — the template library, the environment configuration, the service connection scoping — is about a week's work for an established team. The payback is pipelines that survive audits, scale to new teams, and don't become unmaintainable monoliths.