---
title: "Azure DevOps Pipelines: YAML Pipelines, Templates, Service Connections, and AKS Integration"
description: "Reference for Azure DevOps YAML pipelines covering stages, jobs, steps, service connections, variable groups, environments with approvals, template references, and integration with Azure Kubernetes Service and Azure Container Registry."
url: https://agent-zone.ai/knowledge/cicd/azure-devops-pipelines/
section: knowledge
date: 2026-02-22
categories: ["cicd"]
tags: ["azure-devops","cicd","pipeline","aks","acr","yaml-pipelines","templates","service-connections","environments"]
skills: ["ci-pipeline-design","azure-devops-authoring","azure-integration","pipeline-optimization"]
tools: ["azure-devops","az-cli","docker","kubectl","helm"]
levels: ["intermediate","advanced"]
word_count: 1269
formats:
  json: https://agent-zone.ai/knowledge/cicd/azure-devops-pipelines/index.json
  html: https://agent-zone.ai/knowledge/cicd/azure-devops-pipelines/?format=html
  api: https://api.agent-zone.ai/api/v1/knowledge/search?q=Azure+DevOps+Pipelines%3A+YAML+Pipelines%2C+Templates%2C+Service+Connections%2C+and+AKS+Integration
---


# Azure DevOps Pipelines

Azure DevOps Pipelines uses YAML files stored in your repository to define build and deployment workflows. The pipeline model has three levels: stages contain jobs, jobs contain steps. This hierarchy maps directly to how you think about CI/CD -- build stage, test stage, deploy-to-staging stage, deploy-to-production stage -- with each stage containing one or more parallel jobs.

## Pipeline Structure

A complete pipeline in `azure-pipelines.yml`:

```yaml
trigger:
  branches:
    include:
      - main
      - release/*
  paths:
    exclude:
      - docs/**
      - README.md

pool:
  vmImage: 'ubuntu-latest'

variables:
  - group: common-vars
  - name: buildConfiguration
    value: 'Release'

stages:
  - stage: Build
    jobs:
      - job: BuildApp
        steps:
          - task: GoTool@0
            inputs:
              version: '1.22'
          - script: |
              go build -o $(Build.ArtifactStagingDirectory)/myapp ./cmd/myapp
            displayName: 'Build binary'
          - publish: $(Build.ArtifactStagingDirectory)
            artifact: drop

  - stage: Test
    dependsOn: Build
    jobs:
      - job: UnitTests
        steps:
          - task: GoTool@0
            inputs:
              version: '1.22'
          - script: go test ./... -v -coverprofile=coverage.out
            displayName: 'Run tests'
          - task: PublishCodeCoverageResults@2
            inputs:
              summaryFileLocation: coverage.out
              codecoverageTool: 'Cobertura'

  - stage: DeployStaging
    dependsOn: Test
    condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
    jobs:
      - deployment: DeployToStaging
        environment: staging
        strategy:
          runOnce:
            deploy:
              steps:
                - download: current
                  artifact: drop
                - script: echo "Deploying to staging"
```

`trigger` controls which branches and paths trigger the pipeline. `dependsOn` creates stage ordering. `condition` adds logic -- `succeeded()` checks the previous stage passed, and you can combine it with variable checks to restrict certain stages to specific branches.

## Stages, Jobs, and Steps

**Stages** are the top-level grouping. Each stage can target a different environment, have its own approval gates, and run on different agent pools. Stages run sequentially by default; use `dependsOn: []` to run a stage with no dependencies (parallel to others).

**Jobs** run on an agent. A `job` is a generic execution unit. A `deployment` job is specialized for deploying to an environment -- it tracks deployment history and supports strategies like `runOnce`, `rolling`, and `canary`.

**Steps** are the atomic units: `script` (inline shell), `task` (built-in or marketplace tasks), `checkout` (source code), `download` (pipeline artifacts), and `publish` (upload artifacts).

```yaml
stages:
  - stage: Build
    jobs:
      - job: BuildLinux
        pool:
          vmImage: 'ubuntu-latest'
        steps:
          - script: make build-linux
      - job: BuildWindows
        pool:
          vmImage: 'windows-latest'
        steps:
          - script: make build-windows
```

Jobs within a stage run in parallel by default. Use `dependsOn` between jobs for sequential ordering within a stage.

## Service Connections

Service connections authenticate pipelines to external services -- Azure subscriptions, Docker registries, Kubernetes clusters, AWS accounts. They are created in Project Settings and referenced by name in YAML:

```yaml
steps:
  - task: Docker@2
    inputs:
      containerRegistry: 'acr-connection'
      repository: 'myapp'
      command: 'buildAndPush'
      Dockerfile: '**/Dockerfile'
      tags: |
        $(Build.BuildId)
        latest

  - task: KubernetesManifest@1
    inputs:
      action: 'deploy'
      connectionType: 'kubernetesServiceConnection'
      kubernetesServiceConnection: 'aks-staging'
      namespace: 'myapp'
      manifests: 'k8s/*.yaml'
      containers: 'myacr.azurecr.io/myapp:$(Build.BuildId)'
```

Service connections use Azure AD service principals or managed identities. The credentials are managed centrally and never appear in pipeline YAML. You can restrict which pipelines access a connection through pipeline permissions on the service connection settings.

## Variable Groups and Secrets

Variable groups bundle related variables and are managed in Library. Link them to pipelines with the `group` keyword:

```yaml
variables:
  - group: staging-config
  - group: common-secrets
  - name: localVar
    value: 'inline-value'

steps:
  - script: |
      echo "Deploying to $(environment-url)"
      echo "Using registry $(acr-name)"
    env:
      DB_PASSWORD: $(db-password)
```

Variables marked as secret in the group are masked in logs. Reference them with `$(variable-name)` in YAML or map them to environment variables with `env`. Secret variables cannot be used in conditions or printed directly -- they are only available as environment variables within steps.

For Azure Key Vault integration, link a variable group to a Key Vault:

```yaml
variables:
  - group: keyvault-secrets  # Linked to Azure Key Vault

steps:
  - script: ./deploy.sh
    env:
      API_KEY: $(api-key)        # Pulled from Key Vault at runtime
      DB_CONN: $(db-connection)  # Pulled from Key Vault at runtime
```

This keeps secrets in Key Vault with full audit logging and rotation capabilities, while making them available as pipeline variables without duplication.

## Environments and Approvals

Environments represent deployment targets. They provide approval gates, deployment history, and Kubernetes resource tracking:

```yaml
stages:
  - stage: DeployProduction
    jobs:
      - deployment: ProductionDeploy
        environment: production.myapp-namespace
        strategy:
          runOnce:
            deploy:
              steps:
                - task: KubernetesManifest@1
                  inputs:
                    action: deploy
                    kubernetesServiceConnection: 'aks-production'
                    namespace: 'myapp'
                    manifests: 'k8s/production/*.yaml'
```

Configure approvals in the Azure DevOps UI under Environments > production > Approvals and checks. Options include:

- **Approvals**: Require one or more specific users or groups to approve before deployment proceeds.
- **Branch control**: Restrict which branches can deploy to the environment.
- **Business hours**: Only allow deployments during specified hours.
- **Exclusive lock**: Prevent parallel deployments to the same environment.

The `production.myapp-namespace` syntax targets a specific Kubernetes namespace within the environment. Azure DevOps tracks which resources are deployed there and shows deployment history per namespace.

## Template References

Templates enable reusable pipeline components. They can define steps, jobs, stages, or variables:

```yaml
# templates/build-go.yml
parameters:
  - name: goVersion
    type: string
    default: '1.22'
  - name: outputBinary
    type: string

steps:
  - task: GoTool@0
    inputs:
      version: ${{ parameters.goVersion }}
  - script: |
      CGO_ENABLED=0 go build -o $(Build.ArtifactStagingDirectory)/${{ parameters.outputBinary }} ./cmd/${{ parameters.outputBinary }}
    displayName: 'Build ${{ parameters.outputBinary }}'
  - publish: $(Build.ArtifactStagingDirectory)
    artifact: ${{ parameters.outputBinary }}
```

Reference templates in your main pipeline:

```yaml
stages:
  - stage: Build
    jobs:
      - job: BuildApp
        steps:
          - template: templates/build-go.yml
            parameters:
              goVersion: '1.22'
              outputBinary: 'myapp'
```

Stage-level templates enable full pipeline composition:

```yaml
# templates/deploy-stage.yml
parameters:
  - name: environment
    type: string
  - name: serviceConnection
    type: string

stages:
  - stage: Deploy_${{ parameters.environment }}
    jobs:
      - deployment: Deploy
        environment: ${{ parameters.environment }}
        strategy:
          runOnce:
            deploy:
              steps:
                - task: AzureCLI@2
                  inputs:
                    azureSubscription: ${{ parameters.serviceConnection }}
                    scriptType: 'bash'
                    scriptLocation: 'inlineScript'
                    inlineScript: |
                      az aks get-credentials --resource-group myapp-rg --name myapp-aks
                      helm upgrade --install myapp ./chart --set image.tag=$(Build.BuildId)
```

```yaml
# azure-pipelines.yml
stages:
  - stage: Build
    jobs:
      - job: Build
        steps:
          - template: templates/build-go.yml
            parameters:
              outputBinary: myapp

  - template: templates/deploy-stage.yml
    parameters:
      environment: staging
      serviceConnection: azure-staging

  - template: templates/deploy-stage.yml
    parameters:
      environment: production
      serviceConnection: azure-production
```

Templates can live in the same repository or in a separate template repository referenced with `resources.repositories`:

```yaml
resources:
  repositories:
    - repository: templates
      type: git
      name: MyOrg/pipeline-templates
      ref: refs/tags/v2.0

stages:
  - template: stages/deploy.yml@templates
    parameters:
      environment: production
```

Pin template references to tags or specific commits. Using `ref: refs/heads/main` means template changes immediately affect all consuming pipelines.

## AKS and ACR Integration

The typical Azure pipeline builds a container, pushes to ACR, and deploys to AKS:

```yaml
variables:
  acrName: 'myacr'
  acrLoginServer: 'myacr.azurecr.io'
  imageRepository: 'myapp'

stages:
  - stage: Build
    jobs:
      - job: BuildAndPush
        steps:
          - task: Docker@2
            displayName: 'Build and push to ACR'
            inputs:
              containerRegistry: 'acr-service-connection'
              repository: $(imageRepository)
              command: 'buildAndPush'
              Dockerfile: 'Dockerfile'
              tags: |
                $(Build.BuildId)
                $(Build.SourceBranchName)-latest

  - stage: Deploy
    dependsOn: Build
    jobs:
      - deployment: DeployToAKS
        environment: staging.myapp
        strategy:
          runOnce:
            deploy:
              steps:
                - task: KubernetesManifest@1
                  displayName: 'Deploy to AKS'
                  inputs:
                    action: 'deploy'
                    connectionType: 'kubernetesServiceConnection'
                    kubernetesServiceConnection: 'aks-staging-connection'
                    namespace: 'myapp'
                    manifests: 'k8s/deployment.yaml'
                    containers: '$(acrLoginServer)/$(imageRepository):$(Build.BuildId)'
                    imagePullSecrets: 'acr-secret'
```

The `containers` field in `KubernetesManifest` performs image substitution -- it replaces image references in your manifests with the fully-qualified tag. The `imagePullSecrets` field injects the pull secret into deployed pods.

For Helm deployments, use `HelmDeploy@0`:

```yaml
- task: HelmDeploy@0
  inputs:
    connectionType: 'Kubernetes Service Connection'
    kubernetesServiceConnection: 'aks-production'
    namespace: 'myapp'
    command: 'upgrade'
    chartType: 'FilePath'
    chartPath: 'charts/myapp'
    releaseName: 'myapp'
    overrideValues: 'image.tag=$(Build.BuildId),image.repository=$(acrLoginServer)/$(imageRepository)'
```

## Common Mistakes

1. **Using classic (UI-based) pipelines instead of YAML.** Classic pipelines are not version-controlled. YAML pipelines live in the repo and follow the same review process as code.
2. **Not using template references for repeated patterns.** Copy-pasting deploy stages across pipelines guarantees drift. Extract templates and share them via a central repository.
3. **Putting secrets in variable definitions instead of variable groups.** Inline `variables` in YAML are visible to anyone with repo access. Use variable groups with secret masking or Key Vault-linked groups.
4. **Ignoring environment checks.** Without approvals on production environments, any push to main deploys directly to production. Always configure at least one approval gate on production environments.
5. **Not pinning template repository references.** Using `ref: refs/heads/main` for shared templates means any template change immediately affects all pipelines. Pin to tags and upgrade deliberately.

