CloudOpsGuide
azure

Azure DevOps Pipelines: Complete Guide with Security Integration

Intermediate
16 minutes
2026-02-15
CloudOpsGuide Team

Azure DevOps Pipelines: Complete Guide with Security Integration

Learn to build Azure DevOps YAML pipelines from scratch — CI builds, multi-stage deployments, Key Vault secrets, and security scanning built into every stage.

Difficulty: Intermediate Estimated Reading Time: 16 minutes Last Updated: 2026-02-15

Table of Contents

What Are Azure DevOps Pipelines

Azure DevOps Pipelines is Microsoft's CI/CD service. Pipelines are defined in a YAML file (azure-pipelines.yml) stored in your repository, so your build definition is versioned alongside your code.

The service is organised into five areas:

ServicePurpose
ReposGit repositories with branch policies
PipelinesBuild and release automation
BoardsWork item tracking
ArtifactsPackage feeds (NuGet, npm, Maven)
Test PlansManual test management

For DevSecOps, Pipelines matter because every stage of your delivery process — build, scan, test, deploy — runs through them. Security checks become pipeline steps, not afterthoughts.

Creating Your First Pipeline

Prerequisites

  • Azure DevOps organisation at dev.azure.com
  • A project with a repository
  • Permissions to create pipelines

Minimal pipeline

Create azure-pipelines.yml in your repo root:

trigger:
  - main

pool:
  vmImage: 'ubuntu-latest'

steps:
  - script: echo "Hello from Azure DevOps"
    displayName: 'First step'

Commit and push. Azure DevOps detects the file and creates a pipeline run automatically.

A realistic Node.js build

trigger:
  - main

pool:
  vmImage: 'ubuntu-latest'

steps:
  - task: NodeTool@0
    inputs:
      versionSpec: '20.x'
    displayName: 'Install Node.js'

  - script: npm ci
    displayName: 'Install dependencies'

  - script: npm run build
    displayName: 'Build'

  - script: npm test
    displayName: 'Run tests'

Pipeline Structure Explained

Pipelines have three levels — stages contain jobs contain steps:

stages:
  - stage: Build
    jobs:
      - job: CompileAndTest
        steps:
          - script: npm run build

  - stage: Deploy
    dependsOn: Build
    jobs:
      - job: DeployApp
        steps:
          - script: ./deploy.sh
LevelRuns onParallel?
StageLogical groupingSequential by default (dependsOn controls order)
JobOne agentParallel within a stage
StepInside a jobSequential

Jobs on the same stage run in parallel across agents — useful for running unit tests and lint checks simultaneously:

- stage: Validate
  jobs:
    - job: Tests
      steps:
        - script: npm test
    - job: Lint
      steps:
        - script: npm run lint
    - job: TypeCheck
      steps:
        - script: npx tsc --noEmit

Variables and Secrets

Pipeline variables

variables:
  - name: imageName
    value: 'myapp'
  - name: environment
    value: 'staging'

steps:
  - script: echo "Building $(imageName) for $(environment)"

Variable groups

Shared variables across pipelines go in Pipelines → Library → Variable groups. Mark sensitive values as secret — Azure DevOps masks them in logs.

Azure Key Vault integration

Never store secrets in YAML. Link a variable group to Key Vault, or fetch secrets in a task:

steps:
  - task: AzureKeyVault@2
    inputs:
      azureSubscription: 'my-service-connection'
      KeyVaultName: 'prod-secrets-kv'
      SecretsFilter: 'api-key,db-password'
      RunAsPreJob: true

Secrets fetched this way are available as $(api-key) and are masked in all pipeline output.

Adding Security Scanning

This is where pipelines become DevSecOps pipelines. Add scanning as early-stage jobs that fail the build on critical findings.

Dependency scanning

- script: npm audit --audit-level=high
  displayName: 'Audit npm dependencies'

Container image scanning with Trivy

- script: |
    docker build -t $(imageName):$(Build.BuildId) .
    docker run --rm -v /var/run/docker.sock:/var/run/docker.sock \
      aquasec/trivy:latest image --severity CRITICAL --exit-code 1 \
      $(imageName):$(Build.BuildId)
  displayName: 'Build and scan image'

--exit-code 1 makes the pipeline fail on critical vulnerabilities — the image never reaches your registry.

Secret detection

- script: |
    docker run --rm -v $(Build.SourcesDirectory):/path \
      zricethezav/gitleaks:latest detect --source /path --verbose
  displayName: 'Scan for committed secrets'

Static analysis (SAST)

SonarQube has a native Azure DevOps extension:

- task: SonarQubePrepare@6
  inputs:
    SonarQube: 'SonarQubeServiceConnection'
    scannerMode: 'cli'
    configMode: 'manual'
    cliProjectKey: 'myapp'
    cliProjectName: 'My App'

- task: SonarQubeAnalyze@6

- task: SonarQubePublish@6
  inputs:
    pollingTimeoutSec: '300'

Multi-Stage Deployments

Use deployment jobs for environment-aware deployments with built-in tracking:

- stage: DeployStaging
  dependsOn: Build
  jobs:
    - deployment: DeployToStaging
      environment: 'staging'
      strategy:
        runOnce:
          deploy:
            steps:
              - task: KubernetesManifest@1
                inputs:
                  action: 'deploy'
                  kubernetesServiceConnection: 'aks-staging'
                  namespace: 'myapp'
                  manifests: '$(Build.ArtifactStagingDirectory)/manifests/*.yaml'

- stage: DeployProduction
  dependsOn: DeployStaging
  jobs:
    - deployment: DeployToProd
      environment: 'production'
      strategy:
        runOnce:
          deploy:
            steps:
              - script: ./deploy-production.sh

The environment keyword is what unlocks approvals — see next section.

Approvals and Gates

In Pipelines → Environments → production → Approvals and checks, add required approvers. The pipeline pauses before touching production until a human approves.

Available checks:

CheckWhat it does
ApprovalsNamed users must approve before deploy
Business hoursOnly deploy during specified windows
Azure FunctionCall a custom function that returns pass/fail
REST APIQuery an external service for a go/no-go
Branch controlOnly allow deploys from specific branches

This gives you continuous delivery with a human safety gate — deploy to staging on every merge, require sign-off for production.

Reusable Templates

Extract repeated step sequences into template files:

# templates/build-node.yml
parameters:
  - name: nodeVersion
    default: '20.x'

steps:
  - task: NodeTool@0
    inputs:
      versionSpec: '${{ parameters.nodeVersion }}'
  - script: npm ci && npm run build

Use it in any pipeline:

stages:
  - stage: Build
    jobs:
      - job: BuildJob
        steps:
          - template: templates/build-node.yml
            parameters:
              nodeVersion: '20.x'

Templates keep pipelines DRY — update the template once, every pipeline gets the fix.

Troubleshooting

ProblemLikely causeFix
"No hosted parallelism"Free tier limit reachedRequest parallel jobs grant or use self-hosted agents
##[error]Variable $(x) is not definedVariable not in scopeCheck dependsOn — variables don't cross stages without stageDependencies
Secrets print in logsUsing plain variablesUse secret variables or Key Vault task
Agent can't reach ACR/AKSService connection missingCreate ARM service connection in Project Settings
YAML validation errorIndentation wrongUse the pipeline editor's Validate button
dependsOn stage skippedPrevious stage failedCheck condition: succeeded() — default behaviour

Debug a failing run with verbose logs:

variables:
  - name: system.debug
    value: 'true'

Best Practices

  1. YAML over Classic — pipeline definitions live in Git, get reviewed in PRs, and roll back with the code.
  2. Fail fast — run lint/tests/security scans before the slow build steps.
  3. Secret hygiene — Key Vault for all secrets; secret variables are write-only (you can't read them back in the UI).
  4. Branch policies — require a passing build on every PR to main.
  5. Environments for prod — approvals + business-hours checks on the production environment.
  6. Self-hosted agents for internal resources — if builds need to reach resources inside your VNET, host agents there.
  7. Cache dependencies — use Cache@2 for node_modules/nuget to cut build times.
  8. Template everything repeated — if two pipelines share steps, extract a template.

Related Articles