Deploying with Azure Pipelines
A deployment pipeline does more than run a script on a server. It tracks history, enforces approval gates, targets specific environments, and gives you a consistent path from code to production. This page covers how to write multi-stage Azure Pipelines deployments with environments, conditions, and deployment jobs.
Deployment jobs vs regular jobs
A standard pipeline job runs tasks in sequence on an agent. A deployment job is a special job type that records deployment history, integrates with Azure DevOps Environments, and supports built-in deployment strategies (runOnce, rolling, canary). Use deployment jobs for anything that changes a live environment.
# The key difference: a deployment job targets an environment
jobs:
# Regular job — no deployment tracking
- job: BuildJob
steps:
- script: dotnet build
# Deployment job — tracked in Environments, supports approval gates
- deployment: DeployDev
environment: 'dev' # must exist as an Azure DevOps Environment
strategy:
runOnce:
deploy:
steps:
- script: echo "Deploying..."
Multi-stage pipeline: build → dev → staging → prod
The following pipeline builds an app, deploys to dev automatically, then waits for approval before deploying to staging and production. Each stage passes the build artifact forward rather than rebuilding.
# azure-pipelines.yml
trigger:
branches:
include:
- main
variables:
buildConfiguration: 'Release'
webAppName: 'myapp'
pool:
vmImage: 'ubuntu-latest'
stages:
- stage: Build
displayName: 'Build'
jobs:
- job: Build
steps:
- task: UseDotNet@2
inputs:
packageType: 'sdk'
version: '8.x'
- task: DotNetCoreCLI@2
displayName: 'Restore'
inputs:
command: 'restore'
projects: '**/*.csproj'
- task: DotNetCoreCLI@2
displayName: 'Build'
inputs:
command: 'build'
projects: '**/*.csproj'
arguments: '--configuration $(buildConfiguration) --no-restore'
- task: DotNetCoreCLI@2
displayName: 'Test'
inputs:
command: 'test'
projects: '**/*Tests.csproj'
arguments: '--configuration $(buildConfiguration) --no-build --collect:"XPlat Code Coverage"'
- task: DotNetCoreCLI@2
displayName: 'Publish'
inputs:
command: 'publish'
projects: '**/MyApp.csproj'
arguments: '--configuration $(buildConfiguration) --no-build --output $(Build.ArtifactStagingDirectory)'
zipAfterPublish: true
- task: PublishBuildArtifacts@1
inputs:
pathToPublish: '$(Build.ArtifactStagingDirectory)'
artifactName: 'drop'
- stage: Deploy_Dev
displayName: 'Deploy to Dev'
dependsOn: Build
condition: succeeded()
jobs:
- deployment: DeployDev
environment: 'dev' # no approval gates — auto-deploys
strategy:
runOnce:
deploy:
steps:
- download: current
artifact: drop
- task: AzureWebApp@1
displayName: 'Deploy to App Service (Dev)'
inputs:
azureSubscription: 'My Azure Subscription'
appType: 'webApp'
appName: '$(webAppName)-dev'
package: '$(Pipeline.Workspace)/drop/**/*.zip'
deploymentMethod: 'auto'
- stage: Deploy_Staging
displayName: 'Deploy to Staging'
dependsOn: Deploy_Dev
condition: succeeded()
jobs:
- deployment: DeployStaging
environment: 'staging' # has approval gate configured in Azure DevOps UI
strategy:
runOnce:
deploy:
steps:
- download: current
artifact: drop
- task: AzureWebApp@1
displayName: 'Deploy to App Service (Staging)'
inputs:
azureSubscription: 'My Azure Subscription'
appType: 'webApp'
appName: '$(webAppName)-staging'
package: '$(Pipeline.Workspace)/drop/**/*.zip'
deploymentMethod: 'zipDeploy'
- stage: Deploy_Prod
displayName: 'Deploy to Production'
dependsOn: Deploy_Staging
condition: succeeded()
jobs:
- deployment: DeployProd
environment: 'prod' # requires approval + business hours check
strategy:
runOnce:
deploy:
steps:
- download: current
artifact: drop
- task: AzureWebApp@1
displayName: 'Deploy to App Service (Prod)'
inputs:
azureSubscription: 'My Azure Subscription'
appType: 'webApp'
appName: '$(webAppName)-prod'
package: '$(Pipeline.Workspace)/drop/**/*.zip'
deploymentMethod: 'zipDeploy'
Controlling stage execution with conditions
The condition field on a stage or job controls when it runs. Azure Pipelines has a set of built-in condition functions. You can combine them with variable checks to create rules like “deploy to production only on main branch merges.”
# Only run this stage if the previous succeeded AND we're on main
- stage: Deploy_Prod
condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
# Run this stage even if a previous stage failed (for cleanup stages)
- stage: Cleanup
condition: always()
# Run only if manually triggered
- stage: HotfixDeploy
condition: eq(variables['Build.Reason'], 'Manual')
# Run if this specific stage succeeded (not just the immediate predecessor)
- stage: Notify
dependsOn:
- Deploy_Dev
- Deploy_Staging
condition: and(succeeded('Deploy_Dev'), succeeded('Deploy_Staging'))
Deploying to AKS with Kubernetes manifests
- stage: Deploy_AKS
displayName: 'Deploy to AKS'
jobs:
- deployment: DeployAKS
environment: 'aks-dev.default' # environment/namespace format for AKS resources
strategy:
runOnce:
deploy:
steps:
- task: KubernetesManifest@1
displayName: 'Create imagePullSecret'
inputs:
action: 'createSecret'
secretType: 'dockerRegistry'
secretName: 'acr-secret'
dockerRegistryEndpoint: 'MyACRServiceConnection'
- task: KubernetesManifest@1
displayName: 'Deploy to Kubernetes'
inputs:
action: 'deploy'
manifests: |
k8s/deployment.yaml
k8s/service.yaml
containers: |
myappregistry.azurecr.io/myapp/api:$(Build.BuildId)
imagePullSecrets: 'acr-secret'
Real failure scenario: deployment job hung in “In progress”
A deployment job can get stuck in “In progress” indefinitely when an approval gate has been configured on an environment but the approver never receives the notification. This happens when the approver email is incorrect, the notification settings are turned off for the Azure DevOps organization, or the approval check was created with a zero-timeout (which causes it to immediately fail rather than wait).
To diagnose and recover:
# Check if a pipeline run is stuck
az pipelines runs show --id <run-id> --output table
# Cancel a stuck run
az pipelines runs cancel --id <run-id>
# List environment checks (approvals)
az devops invoke \
--area pipelines \
--resource approvals \
--route-parameters runId=<run-id> \
--output json
Set a timeout on environment approval checks. In the environment settings, configure the approval check with a maximum waiting time (for example, 24 hours). This prevents pipelines from waiting forever and consuming a parallel job slot indefinitely. The run will fail cleanly rather than hanging.
Reusing steps with pipeline templates
If multiple pipelines share the same deployment steps, extract them into a YAML template file and reference it from each pipeline. Templates accept parameters and can be stored in a separate repository.
# templates/deploy-appservice.yml
parameters:
- name: environment
type: string
- name: appName
type: string
- name: azureSubscription
type: string
jobs:
- deployment: Deploy_${{ parameters.environment }}
environment: ${{ parameters.environment }}
strategy:
runOnce:
deploy:
steps:
- download: current
artifact: drop
- task: AzureWebApp@1
displayName: 'Deploy to ${{ parameters.environment }}'
inputs:
azureSubscription: ${{ parameters.azureSubscription }}
appType: 'webApp'
appName: ${{ parameters.appName }}
package: '$(Pipeline.Workspace)/drop/**/*.zip'
# azure-pipelines.yml — calling the template
stages:
- stage: Deploy_Dev
jobs:
- template: templates/deploy-appservice.yml
parameters:
environment: 'dev'
appName: 'myapp-dev'
azureSubscription: 'My Azure Subscription'
- stage: Deploy_Prod
jobs:
- template: templates/deploy-appservice.yml
parameters:
environment: 'prod'
appName: 'myapp-prod'
azureSubscription: 'My Azure Subscription'
Common mistakes
- Using regular jobs instead of deployment jobs for environment deployments. Regular jobs do not record deployment history, cannot pause for approvals, and do not integrate with Azure DevOps Environments. Any step that changes a running environment should use a deployment job.
- Not downloading the artifact before deploying. Deployment jobs do not automatically have access to artifacts from the Build stage. You must explicitly download the artifact with a
downloadstep, or the deployment tasks will not find any files to deploy. - Triggering production deploys on every branch push. The trigger should be scoped to the main branch. Add a condition like
eq(variables[‘Build.SourceBranch’], ‘refs/heads/main’)on your production stage to prevent feature branch builds from accidentally deploying to production.
Summary
- Multi-stage pipelines chain Build, Dev, Staging, and Prod stages together, with each stage depending on the previous one succeeding.
- Deployment jobs (not regular jobs) provide deployment history, approval gate integration, and built-in deployment strategies.
- Azure DevOps Environments are where you configure approval gates. The pipeline pauses at a deployment job and waits for approval before continuing.
- Use conditions on stages to control which branches trigger which stages and to handle failure scenarios.
- Extract repeated deployment steps into YAML templates for reuse across multiple pipelines.
Frequently asked questions
What is the difference between a pipeline stage and an environment in Azure Pipelines?
A stage is a logical division in your pipeline YAML that groups jobs. An environment is an Azure DevOps resource that represents a deployment target (like dev, staging, or prod). Environments have approval gates, deployment history, and resource tracking. A stage uses an environment by specifying it in the deployment job. Without environments, stages have no approval mechanism.
How do I require manual approval before deploying to production?
Create an Azure DevOps Environment named "prod" in the Environments section of Azure Pipelines. In the environment settings, add an Approvals check. Add a user or group as required approvers. When a pipeline reaches a deployment job targeting that environment, it pauses and sends a notification to approvers. The pipeline resumes only after approval.
Can I deploy from Azure Pipelines to Kubernetes?
Yes. Use the KubernetesManifest@1 task or the Kubernetes@1 task. Create a Kubernetes service connection pointing to your AKS cluster. The pipeline authenticates with the cluster and applies your manifest files or Helm charts.