Cloud Security20 min read

Azure DevOps Pipelines: Complete Beginner's Guide (2026) with YAML Examples

Learn how to set up your first CI/CD pipeline in Azure DevOps. This hands-on guide walks you through creating build and release pipelines with real examples.

I
Microsoft Cloud Solution Architect
Azure DevOpsCI/CDPipelinesAutomationDevOps

Why Azure DevOps Pipelines Matter

If you're still manually deploying code or running scripts by hand, you're spending time on tasks that should be automated. Azure DevOps Pipelines lets you automate your build, test, and deployment processes so your team can focus on what actually matters: building great software.

I've helped dozens of teams move from manual deployments to automated pipelines, and the difference is night and day. Fewer errors, faster releases, and happier developers.

What You'll Need

Before we start, make sure you have:

  • An Azure DevOps organization (free tier works fine)
  • A code repository (Azure Repos, GitHub, or Bitbucket)
  • Basic familiarity with YAML (don't worry, it's straightforward)

Azure DevOps Pipeline YAML Examples

Before stepping through the full setup, here are three production-ready YAML examples you can drop directly into your azure-pipelines.yml.

Example 1: Basic CI Pipeline - Trigger on Push, Run Tests

This is the minimum viable pipeline. It triggers on every push to main or develop, installs dependencies, lints, and runs tests:

trigger:
  branches:
    include:
     - main
     - develop

pool:
  vmImage: 'ubuntu-22.04'

variables:
  nodeVersion: '20.x'

steps:
 - task: NodeTool@0
    inputs:
      versionSpec: $(nodeVersion)
    displayName: 'Install Node.js $(nodeVersion)'

 - script: npm ci
    displayName: 'Install dependencies'

 - script: npm run lint
    displayName: 'Run linter'

 - script: npm test -- --reporter=junit --outputFile=test-results.xml
    displayName: 'Run unit tests'

 - task: PublishTestResults@2
    inputs:
      testResultsFormat: 'JUnit'
      testResultsFiles: '**/test-results.xml'
    displayName: 'Publish test results'
    condition: always()

Key detail: condition: always() on PublishTestResults ensures diagnostic data uploads even when the test run fails; otherwise a failing build exits before you see which tests broke.

Example 2: Multi-Stage CI/CD Pipeline - Build → Test → Deploy

This three-stage pipeline models a real delivery workflow. Each stage depends on the previous one, and the deploy stage only runs on the main branch:

trigger:
  branches:
    include:
     - main

pool:
  vmImage: 'ubuntu-22.04'

stages:
 - stage: Build
    displayName: 'Build'
    jobs:
     - job: BuildJob
        steps:
         - task: NodeTool@0
            inputs:
              versionSpec: '20.x'
            displayName: 'Install Node.js'
         - script: |
              npm ci
              npm run build
            displayName: 'Build application'
         - task: PublishBuildArtifacts@1
            inputs:
              pathToPublish: '$(System.DefaultWorkingDirectory)/dist'
              artifactName: 'drop'
            displayName: 'Publish build artifact'

 - stage: Test
    displayName: 'Test'
    dependsOn: Build
    jobs:
     - job: TestJob
        steps:
         - task: NodeTool@0
            inputs:
              versionSpec: '20.x'
            displayName: 'Install Node.js'
         - script: npm ci && npm test
            displayName: 'Run tests'

 - stage: Deploy
    displayName: 'Deploy to Production'
    dependsOn: Test
    condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
    jobs:
     - deployment: DeployWebApp
        environment: 'production'
        strategy:
          runOnce:
            deploy:
              steps:
               - task: DownloadBuildArtifacts@1
                  inputs:
                    buildType: 'current'
                    downloadType: 'single'
                    artifactName: 'drop'
               - task: AzureWebApp@1
                  inputs:
                    azureSubscription: 'MyServiceConnection'
                    appName: 'my-production-app'
                    package: '$(System.ArtifactsDirectory)/drop/**/*.zip'
                  displayName: 'Deploy to Azure App Service'

The environment: 'production' declaration creates a deployment environment in Azure DevOps where you configure approval gates and track deployment history. The conditional expression ensures the deploy stage only runs from refs/heads/main; feature branches build and test, but never deploy.

Example 3: Pipeline with Environment Variables and Secrets

This example shows the correct pattern for sensitive configuration: no secrets in YAML, everything sourced from Azure Key Vault via a variable group:

trigger:
 - main

pool:
  vmImage: 'ubuntu-22.04'

variables:
 - group: 'production-secrets'     # Variable group linked to Azure Key Vault
 - name: APP_ENV
    value: 'production'
 - name: BUILD_NUMBER
    value: $(Build.BuildId)

steps:
 - script: |
      echo "Environment: $(APP_ENV)"
      echo "Build: $(BUILD_NUMBER)"
    displayName: 'Log build context'

 - task: AzureKeyVault@2
    inputs:
      azureSubscription: 'MyServiceConnection'
      KeyVaultName: 'my-keyvault-prod'
      SecretsFilter: 'DatabaseConnectionString,ApiSecretKey'
    displayName: 'Fetch secrets from Azure Key Vault'

 - script: |
      npm ci
      npm run build
    displayName: 'Build application'
    env:
      DATABASE_URL: $(DatabaseConnectionString)
      SECRET_KEY: $(ApiSecretKey)

 - script: npm run deploy
    displayName: 'Deploy application'
    env:
      DATABASE_URL: $(DatabaseConnectionString)

The env: block under each step maps Key Vault secrets to environment variables scoped to that step only. Azure DevOps automatically masks these values in all log output; they never appear in plaintext in your build logs.

Azure Pipelines vs GitHub Actions: Full Comparison (2026)

Both are mature CI/CD platforms. Here is a side-by-side comparison across eight criteria:

CriteriaAzure PipelinesGitHub Actions
**Pricing (private repos)**1 free parallel job (1,800 min/month); $40/month per extra2,000 minutes/month free; $0.008/min after
**YAML syntax**Verbose hierarchy: pipeline → stages → jobs → stepsFlat structure: workflow → jobs → steps
**Marketplace**1,000+ tasks at marketplace.visualstudio.com20,000+ actions at github.com/marketplace
**Self-hosted runners**Self-hosted agents - Windows, Linux, macOS, container poolsSelf-hosted runners - same platforms, plus managed larger runners
**Matrix builds**`strategy.matrix` - parallel jobs across variable combinations`strategy.matrix` - identical concept, nearly identical syntax
**YAML complexity**Higher - stage/job/step hierarchy required even for simple pipelinesLower - single job with steps is a valid minimal workflow
**Azure integration**Native - built-in service connections, Key Vault tasks, deployment targetsGood - via official Microsoft actions, needs extra configuration
**GitHub integration**Good - requires GitHub App installationNative - repo context, PR metadata, and secrets built-in

Choose Azure Pipelines if your team already uses Azure DevOps for boards, repos, and test plans, or deploys primarily to Azure services. The native service connection and built-in Azure task library reduce configuration overhead significantly.

Choose GitHub Actions if your code lives on GitHub and you want the path of least resistance. The flat YAML syntax is simpler to learn, and the GitHub Marketplace has the largest action ecosystem.

Creating Your First Pipeline

Step 1: Set Up Your Project

Head to Azure DevOps and create a new project. Give it a meaningful name. You'll thank yourself later when you have 20 projects to manage.

Here's a basic pipeline configuration in your azure-pipelines.yml:

trigger:
 - main

pool:
  vmImage: 'ubuntu-latest'

stages:
 - stage: Build
    displayName: 'Build Application'
    jobs:
     - job: BuildJob
        steps:
         - task: NodeTool@0
            inputs:
              versionSpec: '18.x'
            displayName: 'Install Node.js'
         - script: |
              npm ci
              npm run build
            displayName: 'Install and Build'

This basic pipeline triggers on every push to main, installs Node.js, builds your app, and saves the output as an artifact.

Step 2: Add Testing

Never deploy without testing. Add a test stage after your build stage that runs your unit tests and publishes the results:

- stage: Test
  displayName: 'Run Tests'
  dependsOn: Build
  jobs:
   - job: TestJob
      steps:
       - task: NodeTool@0
          inputs:
            versionSpec: '18.x'
          displayName: 'Install Node.js'
       - script: |
            npm ci
            npm test -- --reporter=junit --outputFile=test-results.xml
          displayName: 'Run Unit Tests'
       - task: PublishTestResults@2
          inputs:
            testResultsFormat: 'JUnit'
            testResultsFiles: '**/test-results.xml'
          displayName: 'Publish Test Results'
          condition: always()

The PublishTestResults task uploads results to Azure DevOps so you can see pass/fail trends over time directly in the pipeline run view.

Step 3: Deploy to Azure

Once your tests pass, add a deployment stage using the AzureWebApp task:

- stage: Deploy
  displayName: 'Deploy to Azure'
  dependsOn: Test
  condition: succeeded()
  jobs:
   - deployment: DeployWebApp
      environment: 'production'
      strategy:
        runOnce:
          deploy:
            steps:
             - task: AzureWebApp@1
                inputs:
                  azureSubscription: 'MyServiceConnection'
                  appName: 'my-app-name'
                  package: '$(Pipeline.Workspace)/**/*.zip'
                displayName: 'Deploy to Azure App Service'

The environment: 'production' line creates a deployment environment in Azure DevOps where you can configure approval gates; nobody deploys to production without a human sign-off.

Variables and Secrets

Never hardcode secrets in your pipeline. Use variable groups instead and link them to Azure Key Vault:

variables:
 - group: 'my-variable-group'

stages:
 - stage: Build
    jobs:
     - job: BuildJob
        steps:
         - script: echo 'Connecting to $(DatabaseConnectionString)'
            displayName: 'Use Secret Variable'

To create a variable group: Go to Pipelines > Library > Variable groups. Enable the Key Vault toggle to pull secrets directly from Azure Key Vault. Any secret you add there is available in your pipeline as $(SecretName) without ever being visible in logs.

Common Patterns That Work

Matrix Builds

Test across multiple Node.js versions by using a matrix strategy:

strategy:
  matrix:
    Node18:
      nodeVersion: '18.x'
    Node20:
      nodeVersion: '20.x'
    Node22:
      nodeVersion: '22.x'

steps:
 - task: NodeTool@0
    inputs:
      versionSpec: $(nodeVersion)
    displayName: 'Install Node.js $(nodeVersion)'
 - script: npm ci && npm test
    displayName: 'Test on Node.js $(nodeVersion)'

This creates three parallel jobs: one per Node.js version. If your app breaks on Node 22 but passes on 18, you'll know before it reaches production.

Conditional Deployments

Deploy to different environments based on which branch triggered the build:

stages:
 - stage: DeployDev
    condition: eq(variables['Build.SourceBranch'], 'refs/heads/develop')
    jobs:
     - deployment: DeployToDev
        environment: 'development'
        strategy:
          runOnce:
            deploy:
              steps:
               - task: AzureWebApp@1
                  inputs:
                    appName: 'my-app-dev'
 - stage: DeployProd
    condition: eq(variables['Build.SourceBranch'], 'refs/heads/main')
    jobs:
     - deployment: DeployToProd
        environment: 'production'
        strategy:
          runOnce:
            deploy:
              steps:
               - task: AzureWebApp@1
                  inputs:
                    appName: 'my-app-prod'

Azure DevOps Pipeline Best Practices

Applying these ten practices will make your pipelines more reliable, faster, and easier to maintain as your team grows.

  1. Pin agent image versions explicitly - Use ubuntu-22.04 instead of ubuntu-latest. Microsoft updates the latest image without notice, which can silently break pipelines that depend on specific tool versions. Pin deliberately and update on your own schedule.
  2. Never store secrets in YAML - Use variable groups linked to Azure Key Vault. Every secret in a variable group is masked in logs, rotated centrally, and auditable. Hardcoded tokens in azure-pipelines.yml get committed to version control, the most common source of secret leaks in CI/CD pipelines.
  3. Cache package manager dependencies - Add the Cache@2 task to cache node_modules, .pip, or Maven .m2 directories. A cold npm install takes 45–60 seconds. A warm cache hit takes 2–5 seconds. On a team running 50 pipeline jobs per day, this adds up to hours saved weekly.
  4. Publish build artifacts between stages - Never re-run npm run build in the deploy stage. Use PublishBuildArtifacts@1 in the build stage and DownloadBuildArtifacts@1 in the deploy stage. What you tested is exactly what gets deployed: no environment drift from a second build.
  5. Require approval gates on production environments - In the Azure DevOps portal, open Environments and configure pre-deployment approval for your production environment. A human signs off before any automated pipeline can release to production, giving you a circuit breaker for runaway deployments.
  6. Use templates for shared pipeline logic - If three repositories all install Node.js and run the same test commands, extract that logic into a templates/build-steps.yml file in a shared repo. Pipelines reference it with - template: templates/build-steps.yml@shared-templates. One change updates all consumers.
  7. Set condition: always() on diagnostic tasks - Test result publishing, code coverage upload, and artifact publishing should run whether the preceding step succeeded or failed. Otherwise a failing test exits the pipeline before diagnostic data is uploaded, the worst time to lose visibility.
  8. Scan deployed infrastructure after each release - After deploying, run Protego's [Vulnerability Scanner](/tools/vulnerability-scanner) against your endpoints to catch exposed secrets, missing security headers, and common misconfigurations before they reach production users.
  9. Integrate Azure Policy compliance checks - For pipelines deploying infrastructure, add an [Azure Policy](/blog/azure-policy-vs-defender-for-cloud-difference) evaluation step before applying changes. Non-compliant resources are caught in the pipeline, not discovered during a security audit weeks later.
  10. Secure Terraform remote state in your pipelines - If your pipelines provision infrastructure with Terraform, configure an [Azure Storage remote backend with state locking](/blog/terraform-remote-state-azure-storage-security) so concurrent pipeline runs cannot corrupt state files. Pair with managed identity authentication so no static credentials are required.

Troubleshooting Tips

Pipeline fails silently? Check your trigger configuration and branch policies.

Slow builds? Enable caching for your package manager.

Permission issues? Verify your service connection has the right access.

Frequently Asked Questions

What is the difference between Azure DevOps and Azure Pipelines?

Azure DevOps is a complete platform that includes five services: Boards (work item tracking), Repos (Git hosting), Pipelines (CI/CD automation), Test Plans (test case management), and Artifacts (package management). Azure Pipelines is specifically the CI/CD automation service within that platform. You can use Azure Pipelines independently, connecting it to a GitHub repository without using Azure Repos, but you need an Azure DevOps organization to access it. When someone says "we use Azure DevOps," they typically mean the full suite; "we use Azure Pipelines" refers specifically to CI/CD automation.

Is Azure DevOps free to use?

Azure DevOps offers a free tier for both public and private projects. For public (open source) projects, you get unlimited parallel jobs and unlimited minutes. For private projects, the free tier includes one parallel job with 1,800 minutes per month and unlimited users. Additional parallel jobs cost $40 per month each. The free tier is sufficient for most small teams: one parallel job means one pipeline run at a time, which works fine until your team grows or needs simultaneous builds across multiple branches.

How do I create my first Azure pipeline?

Navigate to your Azure DevOps project, click Pipelines in the left sidebar, then New pipeline. Select your code source (Azure Repos Git, GitHub, Bitbucket), then choose your repository. Azure DevOps will analyze your code and suggest a starter template; select the template that matches your tech stack (Node.js, .NET, Python) or choose the Starter pipeline for a blank YAML. Review the generated azure-pipelines.yml, click Save and run, and your first pipeline build starts. The full step-by-step walkthrough is in the "Creating Your First Pipeline" section above.

Can I use Azure Pipelines with a GitHub repository?

Yes. Azure Pipelines integrates directly with GitHub through the Azure Pipelines GitHub App. When creating a new pipeline, select GitHub as the code source, authorize the GitHub App, and pick your repository. The pipeline YAML file (azure-pipelines.yml) lives in your GitHub repo and is triggered by GitHub push and pull request events. Your code stays on GitHub; Azure Pipelines handles the build, test, and deployment automation. This is a common setup for teams that prefer GitHub for code review but need Azure Pipelines for deployment to Azure services.

What is a multi-stage pipeline in Azure DevOps?

A multi-stage pipeline is a single YAML file that defines the entire delivery workflow, from building code to deploying to production, as a sequence of ordered stages. Each stage contains jobs, and each job contains steps. Stages run sequentially by default (use dependsOn to make dependencies explicit) and each stage must succeed before the next one starts. Multi-stage pipelines replaced the older two-tool model (build pipelines + release pipelines) and give you complete deployment history, environment-specific approval gates, and a visual view of which stage a deployment is currently at. See Example 2 above for a complete runnable build → test → deploy multi-stage pipeline.

What's Next

Once you're comfortable with basic pipelines, explore:

  • Multi-stage approvals for controlled deployments
  • Template files to share common pipeline logic
  • Integration with Azure Boards for work item tracking
  • [Break-glass emergency account setup](/blog/entra-id-break-glass-account-setup-monitoring) for your Azure environment; before automating production deployments, ensure emergency access accounts are excluded from all Conditional Access policies

The goal isn't to build the most complex pipeline possible. It's to automate the repetitive stuff so your team can ship faster and with confidence.

Get weekly security insights

Cloud security, zero trust, and identity guides — straight to your inbox.

I

Microsoft Cloud Solution Architect

Cloud Solution Architect with deep expertise in Microsoft Azure and a strong background in systems and IT infrastructure. Passionate about cloud technologies, security best practices, and helping organizations modernize their infrastructure.

Share this article

Questions & Answers

Related Articles

Need Help with Your Security?

Our team of security experts can help you implement the strategies discussed in this article.

Contact Us