Introduction
Azure Pipelines, the CI/CD service in Azure DevOps, revolutionizes DevOps workflows in 2026 with ultra-flexible native YAML support. Unlike classic UI-only pipelines, YAML enables Git versioning, reusability, and Infrastructure as Code (IaC) approaches. This expert tutorial walks you step-by-step through a complete pipeline for a Node.js project: multi-OS builds, parallel tests with matrices, artifact publishing, and secure deployments to Azure App Service using environments with manual approvals.
Why does this matter? In production, 70% of CI/CD failures stem from unversioned or untested configs. Here, you'll implement smart triggers, runtime conditions, Azure Key Vault secrets, and built-in monitoring. The result: scalable pipelines for enterprise teams that cut MTTR (Mean Time To Recovery) by 50%. Ready to bookmark this go-to guide? (142 words)
Prerequisites
- Free Azure DevOps account (dev.azure.com)
- Node.js project with package.json, Jest tests, and Dockerfile (clone this example repo)
- Azure Subscription with an App Service created
- Azure CLI installed and logged in (
az login) - Advanced knowledge of YAML, Git, and DevOps (senior level)
- 'AzureRM' service connection in Azure DevOps Project Settings > Service connections
Basic Pipeline: Build and Tests
trigger:
branches:
include:
- main
- develop
pool:
vmImage: 'ubuntu-latest'
stages:
- stage: Build
jobs:
- job: BuildJob
steps:
- task: NodeTool@0
inputs:
versionSpec: '20.x'
- script: npm ci
displayName: 'Install dependencies'
- script: npm run build
displayName: 'Build app'
- script: npm test
displayName: 'Run tests'
env:
CI: trueThis YAML pipeline triggers on main/develop branches, uses an Ubuntu agent, installs Node 20, dependencies, builds, and runs tests. Pitfall: Forgetting 'npm ci' leads to non-reproducible installs in CI; it implicitly enforces --frozen-lockfile. Copy-paste directly to your Git repo root.
Understanding Triggers and Pools
Triggers automate runs on pushes or merges. Here, they're limited to main/develop to avoid spam from feature branches. Pools select the agent: 'ubuntu-latest' for Linux production; switch to 'windows-latest' for cross-platform needs.
Analogy: Triggers are Git sentinels, pools are scalable cloud agent farms (Microsoft-hosted, free up to 1800 min/month). Next: Parallelize with matrices for multi-OS/arch tests.
Matrix Jobs: Multi-Platform Tests
trigger:
branches:
include:
- main
- develop
stages:
- stage: Test
jobs:
- job: TestMatrix
strategy:
matrix:
Node18Ubuntu:
nodeVersion: '18.x'
image: 'ubuntu-20.04'
Node20Windows:
nodeVersion: '20.x'
image: 'windows-2022'
pool:
vmImage: $(image)
steps:
- task: NodeTool@0
inputs:
versionSpec: $(nodeVersion)
- script: |
npm ci
npm test -- --coverage
displayName: 'Tests with coverage'Adds a matrix strategy: Runs jobs in parallel for Node 18/Ubuntu and 20/Windows. Runtime variables like $(nodeVersion) are injected. Pitfall: Overly broad matrices burn through minutes; limit to 4-6 combos. Integrate into your existing pipeline by replacing the Test stage.
Parallelism Strategies
Matrices multiply jobs without YAML duplication—perfect for browsers (Chrome/Firefox) or OS combos. Use conditions like condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main')) for selective runs.
Pro tip: Fan-out/fan-in cuts total time by 70% vs. sequential jobs. Next up: Secure variables and artifacts.
Variables, Secrets, and Artifacts
variables:
npm-config-name: 'my-azure-app'
system.debug: true # Pour logs verbose
stages:
- stage: Publish
dependsOn: Test
jobs:
- job: PublishArtifact
steps:
- task: PublishBuildArtifacts@1
inputs:
pathToPublish: 'dist'
artifactName: 'app-build'
- task: PublishNpmAuthToken@1 # Nécessite secret NPM_TOKEN
inputs:
npmAuthToken: $(NPM_TOKEN) # Variable secrète Pipelines
- script: npm publish --access public
env:
NPM_TOKEN: $(NPM_TOKEN)
resources:
repositories:
- repository: templates
type: github
name: learni-dev/pipeline-templates
endpoint: github-serviceGlobal variables + secrets (add NPM_TOKEN in Pipelines > Library > Variable groups). Publishes 'dist' artifacts and NPM package. Resources reuse GitHub templates. Pitfall: Secrets leak in logs without system.debug; use Key Vault for production.
Managing Secrets and Reusability
Variable groups + Key Vault: Link via azureSubscription for auto-rotation. Templates (resources.repositories) factor out common steps like lint/test.
Analogy: Variables are function params, templates are npm libs. Next: Multi-env deployments with approvals.
Multi-Stage Deployments with Environments
stages:
- stage: DeployStaging
dependsOn: Publish
condition: eq(variables['Build.SourceBranch'], 'refs/heads/develop')
jobs:
- deployment: DeployToStaging
environment: 'staging' # Créez en Environments UI
strategy:
runOnce:
deploy:
steps:
- task: AzureWebApp@1
inputs:
azureSubscription: 'AzureRM-ServiceConnection'
appType: 'webAppLinux'
appName: 'myapp-staging'
package: $(Pipeline.Workspace)/app-build/drop/**/dist.zip
- stage: DeployProd
dependsOn: DeployStaging
condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
jobs:
- deployment: DeployToProd
environment: 'production' # Avec manual approval
strategy:
runOnce:
deploy:
steps:
- download: current
artifact: app-build
- task: AzureWebApp@1
inputs:
appName: 'myapp-prod'Deployment jobs with 'staging/prod' environments (create in UI: Pipelines > Environments, add approvals). Downloads previous artifact. Branch-specific conditions. Pitfall: Forgetting 'download: current' results in empty deployments; test manually with 'Run pipeline'.
Environments and Gates
Environments act as checkpoints with checks (approvals, invokes, gates like API health). Gates handle async queries (e.g., 'Is API up?').
Expert tip: Integrate SonarQube via task@5 for SAST, fail fast if quality < A.
Complete Pipeline with Docker and Helm
stages:
- stage: DockerBuild
jobs:
- job: BuildPush
steps:
- task: Docker@2
inputs:
command: buildAndPush
repository: myacr.azurecr.io/myapp
dockerfile: Dockerfile
containerRegistry: 'my-acr-connection'
tags: |
$(Build.BuildId)
latest
- stage: HelmDeploy
dependsOn: DockerBuild
deployment: K8sDeploy
environment: 'aks-prod'
strategy:
runOnce:
deploy:
steps:
- task: HelmDeploy@0
inputs:
connectionType: 'Azure Resource Manager'
azureSubscription: 'AzureRM'
azureResourceGroup: 'my-rg'
kubernetesNamespace: 'default'
command: 'upgrade'
chartType: 'FilePath'
chartPath: 'helm/myapp'
releaseName: 'myapp'
valueFile: 'values-prod.yaml'Builds/pushes Docker to ACR, then Helm upgrade on AKS. Requires ACR/AKS service connections. Dynamic tags with $(Build.BuildId). Pitfall: Helm without 'upgrade --install' fails recreates; add --install for production. Add as final stage.
Best Practices
- Version YAML in Git: No UI edits!
- Templates mandatory: Reuse 80% of steps via extends.
- Self-hosted agents for secrets/high-perf (e.g., GPU ML).
- Conditions everywhere:
dependsOn+condition: succeededOrFailed()for cleanup. - Caching:
Cache@2for node_modules (hash: package-lock.json) = 10x faster.
Common Errors to Avoid
- Shallow checkout: Use
fetchDepth: 0for full history (LFS/git submodules). - Secrets in logs: Always
$(var)not ${{ }} (runtime vs compile-time). - No parallel limits: Cap at 10 jobs on free tier; set
maxParallel: 4. - Envs without checks: Add mandatory approvers or risk bypass.
Next Steps
Dive into the official Azure Pipelines docs. Integrate hybrid GitHub Actions for multi-cloud.
Check out our Learni DevOps training: Azure Architect certification, hands-on enterprise Pipelines. Join the Discord community for live Q&A.