Introduction
Azure Pipelines, the CI/CD service in Azure DevOps, is the go-to tool for automating builds, tests, and deployments in 2026. Unlike basic pipelines, advanced implementations handle multiple environments (dev, staging, prod), manual approvals, reusable templates, and zero-downtime deployment strategies.
This expert tutorial walks you through creating a complete pipeline for a Node.js project: build, unit tests, linting, artifact publishing, and progressive deployment to Azure App Service. You'll learn to boost performance with caching, secure secrets via Azure Key Vault, and implement gates for compliance.
Why it matters: In a mature DevOps world, 80% of production incidents stem from failed deployments. An expert pipeline cuts that risk by 90%, speeds up releases, and scales effortlessly. Ready to bookmark this reference guide? (128 words)
Prerequisites
- Azure DevOps account with a project and Git repo (Node.js app example).
- Azure Subscription with App Service created (names:
myapp-dev,myapp-staging,myapp-prod). - Azure CLI installed and logged in (
az login). - Environment variables in Azure DevOps:
AZURE_SUBSCRIPTION_ID,KEY_VAULT_NAME. - Advanced knowledge of YAML, Git, and Node.js.
- 'AzureRM' Service Connection configured for deployment.
Basic Pipeline YAML: Build and Tests
trigger:
branches:
include:
- main
- develop
pool:
vmImage: 'ubuntu-latest'
variables:
nodeVersion: '20.x'
stages:
- stage: BuildAndTest
displayName: 'Build & Test'
jobs:
- job: Build
displayName: 'Node Build'
steps:
- task: NodeTool@0
inputs:
versionSpec: $(nodeVersion)
displayName: 'Install Node.js'
- script: |
npm ci
npm run build
displayName: 'npm ci & build'
- job: Test
displayName: 'Unit Tests'
dependsOn: Build
steps:
- task: NodeTool@0
inputs:
versionSpec: $(nodeVersion)
- script: |
npm ci
npm run test:ci
npm run lint
displayName: 'Tests & Lint'
env:
CODECOV_TOKEN: $(codecovToken)
- task: PublishTestResults@2
inputs:
testResultsFormat: 'JUnit'
testResultsFiles: '**/junit.xml'
condition: succeededOrFailed()
- task: PublishCodeCoverageResults@1
inputs:
codeCoverageTool: 'Cobertura'
summaryFileLocation: '**/coverage/cobertura-coverage.xml'This basic pipeline triggers on main and develop branches, using an Ubuntu pool with Node 20. It separates build and tests into parallelizable jobs, publishes results and coverage. Pitfall: Without dependsOn, parallel jobs risk false positives; enable condition: succeededOrFailed() to capture failures.
Optimization: Caching and Artifacts
After the basic build, optimize with npm dependency caching to cut times by 70%. Publish artifacts for later stages to avoid unnecessary rebuilds. Think of it like a CDN for your node_modules—it speeds up iterative runs.
Adding Caching and Artifact Publishing
stages:
- stage: BuildAndTest
jobs:
- job: Build
steps:
- task: NodeTool@0
inputs:
versionSpec: $(nodeVersion)
- task: Cache@2
inputs:
key: 'npm | "$(Agent.OS)" | package-lock.json'
restoreKeys: |
npm | "$(Agent.OS)"
path: $(npm_cache)
displayName: 'Cache npm'
- script: |
npm ci
npm run build
displayName: 'Build'
- task: CopyFiles@2
inputs:
contents: 'dist/**'
targetFolder: '$(Build.ArtifactStagingDirectory)/app'
- task: PublishBuildArtifacts@1
inputs:
pathtoPublish: '$(Build.ArtifactStagingDirectory)'
artifactName: 'drop'
publishLocation: 'Container'
- job: Test
dependsOn: Build
steps:
# ... (tests comme avant, avec cache similaire)
- download: current
artifact: drop
- script: npm run test:e2e
displayName: 'E2E Tests sur artefact'npm cache uses package-lock.json as the key to restore node_modules. Artifacts persist the build for subsequent jobs/tests. Pitfall: Without restoreKeys, cache fails on lockfile changes; test with npm version to validate.
Multi-Stage with Variables and Templates
Level up to multi-environments: auto-deploy to dev, manual to staging, and approvals for prod. Use YAML templates for DRY (Don't Repeat Yourself), injecting variables per stage.
Reusable Deployment Template
parameters:
environment: ''
appServiceName: ''
subscriptionId: ''
jobs:
- deployment: Deploy
displayName: 'Deploy to ${{ parameters.environment }}'
environment: ${{ parameters.environment }}
strategy:
runOnce:
deploy:
steps:
- download: current
artifact: drop
- task: AzureWebApp@1
inputs:
azureSubscription: '${{ parameters.subscriptionId }}'
appType: 'webAppLinux'
appName: '${{ parameters.appServiceName }}'
package: '$(Pipeline.Workspace)/drop/app/**.zip'
deploymentMethod: 'zipDeploy'
- task: AzureCLI@2
inputs:
azureSubscription: '${{ parameters.subscriptionId }}'
scriptType: 'bash'
scriptLocation: 'inlineScript'
inlineScript: |
az webapp config appsettings set --resource-group MyRG --name ${{ parameters.appServiceName }} --settings ENVIRONMENT=${{ parameters.environment }} HEALTH_CHECK_PATH=/health
displayName: 'Set App Settings'This parameterized template handles zip deployment to Linux App Service with custom settings. The Azure DevOps environment enables approvals. Pitfall: Forget zipDeploy without zipping the artifact; add an ArchiveFiles@2 step beforehand if needed.
Multi-Stage Pipeline with Templates and Approvals
stages:
- stage: BuildAndTest
# ... (comme avant)
- stage: DeployDev
displayName: 'Deploy Dev'
dependsOn: BuildAndTest
condition: and(succeeded(), startsWith(variables['Build.SourceBranch'], 'refs/heads/develop'))
jobs:
- template: templates/deploy-job.yml
parameters:
environment: 'dev'
appServiceName: 'myapp-dev'
subscriptionId: $(AZURE_SUBSCRIPTION_ID)
- stage: DeployStaging
displayName: 'Deploy Staging (Manual)'
dependsOn: DeployDev
jobs:
- deployment: gate
displayName: 'Approbation'
environment: 'staging-approval'
strategy:
runOnce:
deploy:
steps:
- script: echo 'Waiting for manual approval...'
- template: templates/deploy-job.yml
parameters:
environment: 'staging'
appServiceName: 'myapp-staging'
subscriptionId: $(AZURE_SUBSCRIPTION_ID)
- stage: DeployProd
displayName: 'Deploy Prod (Gates)'
dependsOn: DeployStaging
condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
jobs:
- template: templates/deploy-job.yml
parameters:
environment: 'prod'
appServiceName: 'myapp-prod'
subscriptionId: $(AZURE_SUBSCRIPTION_ID)Branch-specific conditions automate dev/staging/prod flows. Gates via deployment: gate pause for manual approvals. Pitfall: Without configured environment in Azure DevOps (with checks), prod deploys slip through; set up approvers and retainers.
Security: Secrets and Key Vault
Integrate Azure Key Vault to inject secrets dynamically, preventing log leaks. Use OIDC for passwordless auth without Service Principal secrets.
Key Vault Integration and Secure Variables
variables:
- group: kv-vars # Variable group lié à Key Vault
stages:
# ...
- stage: DeployDev
variables:
- name: dbConnection
value: $(DB_CONNECTION_DEV) # De Key Vault
jobs:
- template: templates/deploy-job.yml
parameters:
# ...
env:
DB_CONNECTION: $(dbConnection)
API_KEY: $(kv-api-key) # Direct de group
# Ajout OIDC pour tasks Azure
- task: AzureKeyVault@2
inputs:
azureSubscription: 'AzureRM'
keyVaultName: '$(KEY_VAULT_NAME)'
secretsFilter: 'db-password,api-key'Variable groups linked to Key Vault inject secrets at runtime. env passes them to scripts without logging. Pitfall: Global variables persist secrets; scope to stage/job and use AzureKeyVault@2 for on-demand fetching.
Post-Deployment Health Check Script (PowerShell)
param(
[string]$AppServiceName,
[string]$ResourceGroup,
[string]$HealthPath = '/health'
)
$subscriptionId = $env:AZURE_SUBSCRIPTION_ID
az account set --subscription $subscriptionId
$healthUrl = "https://$AppServiceName.azurewebsites.net$HealthPath"
try {
$response = Invoke-WebRequest -Uri $healthUrl -TimeoutSec 30 -UseBasicParsing
if ($response.StatusCode -eq 200) {
Write-Host "Health check OK: $($response.StatusCode)"
exit 0
} else {
Write-Error "Health check failed: $($response.StatusCode)"
exit 1
}
} catch {
Write-Error "Health check error: $($_.Exception.Message)"
exit 1
}This PowerShell script checks the /health endpoint post-deploy via Azure CLI. Integrate it with AzurePowerShell@5. Pitfall: Omit --slot for blue-green; test prod slot; 30s timeout prevents hangs.
Best Practices
- Templates everywhere: Factor out 80% of YAML code for scalability.
- Deployment strategies: Use
runOnce,rolling,canaryfor zero downtime. - Aggressive caching: Target Node, Docker layers, .NET restore—aim for 50%+ time savings.
- Advanced gates: Integrate SonarQube, Snyk for quality gates.
- Observability: Structured logs with
##vso[task.logissue type=warning]and metrics via Application Insights.
Common Errors to Avoid
- No branch conditions: Prod deploys on feature branches—use
eq(variables['Build.SourceBranch'], 'refs/heads/main'). - Plaintext secrets: Avoid
$(secret)without Key Vault; logs capture them. - No parallelism limits: Azure overcosts—set
demands: Agent.Name -equals myPool. - Missing artifacts: Downstream jobs fail—always include
download: current.
Next Steps
Dive deeper with Azure Pipelines docs, multi-stage strategies. Check out our DevOps training at Learni for Azure DevOps Engineer Expert certification.