How to Set Up Manual Approval for Azure App Service Slot Swaps in Azure DevOps Pipelines
Overview
Deploying updates to production environments demands both speed and control. Azure App Service deployment slots, combined with Azure DevOps pipelines, offer a powerful way to manage releases, enabling teams to validate changes in a live-like environment before they go public. However, ensuring that only reviewed and approved changes reach your users is critical for maintaining reliability and compliance.
Azure App Service Slots unlock powerful advantages:
- Site Review. Preview and test your site before it goes live, ensuring everything works as expected.
- Warmed-up Instances. Swap with confidence. Your instances are pre-warmed, so there’s no downtime, seamless traffic redirection, and zero dropped requests.
- Instant Rollback. If something goes wrong after a swap, simply swap back to restore your last known good version in seconds.
Best of all, deployment slots come at no extra cost. Check the Azure App Service limits to see how many slots your App Service Tier supports.
Automatic vs. Manual Slot Swaps
When it comes to swapping slots, there are two main approaches: automatic and manual.
- Automatic swaps are ideal when you want to quickly promote changes without a review, while still benefiting from pre-warmed instances and instant rollback.
- Manual swaps give you the opportunity to review and verify your site before it goes live, adding an extra layer of confidence.
But how do you keep a manual swap process governed and controlled? That’s where Azure DevOps pipelines and approvals come in.
In this article, you’ll learn how to set up a robust, approval-driven slot swap process in Azure DevOps.
Defining Azure Resources with Bicep
First, we need to define the resources needed in Azure. To do this I am going to use Infrastructure as Code (IaC). The template shown below creates the following resources:
- App Service Plan.
- App Service linked to the App Service Plan
- Sub Resource that defines the App Service App Settings
- App Service Deployment Slot. Only created for Production environments - this is enforced through a conditional on the resource
Bicep Template: App Service and Deployment Slot
// Bicep
/**********************************
Bicep Template: App Service Deploy
Author: Andrew Wilson
***********************************/
targetScope = 'resourceGroup'
// ** Parameters **
// ****************
@description('Org Project Name')
param projectName string
@description('Application Name sitting within the project')
param applicationName string
@description('location name')
param location string = resourceGroup().location
@description('Environment name')
param env string = 'dev'
@description('App Service Plan sku name')
param appServicePlanSkuName string
@description('App Service Plan sku tier')
param appServicePlanSkuTier string
@description('Name of the deployment slot')
param deploymentSlotName string = 'staging'
// ** Variables **
// ***************
var appServicePlanName string = 'asp-${applicationName}-${env}-${location}'
var appServiceName string = 'app-${applicationName}-${env}-${location}'
// ** Resources **
// ***************
@description('Deploy an Azure App Service Plan')
resource AppServicePlan 'Microsoft.Web/serverfarms@2024-11-01' = {
name: appServicePlanName
location: location
tags: {
project: projectName
application: applicationName
environment: env
version: deployment().properties.template.contentVersion
}
sku: {
name: appServicePlanSkuName
tier: appServicePlanSkuTier
}
}
@description('Deploy an Azure App Service')
resource AppService 'Microsoft.Web/sites@2024-11-01' = {
name: appServiceName
location: location
tags: {
project: projectName
application: applicationName
environment: env
version: deployment().properties.template.contentVersion
}
properties: {
serverFarmId: AppServicePlan.id
httpsOnly: true
}
resource AppServiceAppSettings 'config@2024-11-01' = {
name: 'appsettings'
properties: {
...
}
}
}
@description('Deploy a deployment slot for the App Service')
resource deploymentSlot 'Microsoft.Web/sites/slots@2024-11-01' = if (env == 'prod') {
name: deploymentSlotName
parent: AppService
location: location
tags: {
project: projectName
application: applicationName
environment: env
version: deployment().properties.template.contentVersion
}
properties: { }
}
// ** Outputs **
// *************
@description('Outputs the App Service Name')
output appServiceName string = appServiceName
Important Note: Avoiding AutoSwap for Manual Swaps
Make sure that the DeploymentSlot resource does not have AutoSwap setup as this will counteract when Manual Swap is used. Such that the following process would be observed:
- Deploy to Staging slot.
- AutoSwap kicks in and swaps Staging with Production.
- After Manual Approval, Slots are swapped again
- You are now in the originating position with the new site still in Staging and Production with the old.
Designing the Azure DevOps Pipeline
Next we will setup our Azure DevOps Yaml pipeline. The pipeline’s goal is to orchestrate artifact building and environment deployments while adhering to DevOps best practices, such as binary promotion.
Pipeline Stages Explained
The pipeline has the following stages:
Build_Packages. Build a single set of artifacts that have been tested and versioned. (Best practice of Binary Promotion)
The stage has two jobs to handle both Azure Bicep Templates and the App Service Web Package.
Development. Deploy to the Development Environment using an environment agnostic pipeline template*.
Staging. Deploy to the Staging Environment using an environment agnostic pipeline template*.
Production. Deploy to the Production Environment using an environment agnostic pipeline template*.
*The environment agnostic pipeline template is a reusable template for all environments.
Pipeline YAML: Build and Environment Deployments
# Yaml
# Orchestrating Template: Build And Environment Deployments
...
stages:
- stage: 'Build_Packages'
jobs:
- job: 'Test_Build_And_Version_Bicep_Templates'
steps:
...
- task: PublishPipelineArtifact@1
displayName: 'Publish ARMOutput as template build artifact'
inputs:
targetPath: '$(Build.ArtifactStagingDirectory)/templates'
publishLocation: 'pipeline'
artifactName: 'ARMtemplates'
- job: 'Build_and_Version_AppService'
steps:
...
- task: PublishPipelineArtifact@1
displayName: 'Publish App Service Build Artifact'
inputs:
targetPath: '$(Build.ArtifactStagingDirectory)/AppService.zip'
publishLocation: 'pipeline'
artifactName: 'AppService'
- stage: 'Development'
displayName: 'Deploy to Dev Environment'
...
jobs:
- deployment: Deploy_to_Dev
...
steps:
- template: EnvironmentDeploy.azurepipelinetemplate.yml
...
- stage: 'Staging'
displayName: 'Deploy to Staging Environment'
...
jobs:
- deployment: Deploy_to_Staging
...
steps:
- template: EnvironmentDeploy.azurepipelinetemplate.yml
...
- stage: 'Production'
displayName: 'Deploy to Production Environment'
...
jobs:
- deployment: Deploy_to_Production
...
steps:
- template: EnvironmentDeploy.azurepipelinetemplate.yml
...
Once the Build_Packages stage has completed successfully, we now have two pipeline artifacts, ARMtemplates and AppService. In each environment deploy these two artifacts will be downloaded and used alongside environment specific Library Variable Groups to create specific environment deployments.
Environment-Agnostic Deployment Template
The environment agnostic template is shown below and has the following steps:
- Artifact Downloads. Downloads the two pipeline artifacts, ARMtemplates and AppService.
- Azure Resource Group Deployment. Using the built ARM template, specific environment library variable group and ARM Connection.
- Custom PowerShell Script to Obtain Azure Deployment Outputs. Deployment output such as appServiceName will be created as job variables ready for future tasks to access.
- Azure Web App Deploy (Specific for Dev and Staging). This task deploys straight to the production slot for non production environments.
- Azure Web App Deploy (Specific for Prod). This task deploys to the staging slot ready for a manual slot swap.
Pipeline YAML Template: Reusable Environment-Agnostic Deployments
# Yaml
# Environment Deploy Template: Deploy Azure Resources and Obtain Outputs, Deploy App Service
steps:
- download: current
displayName: 'Download ARM templates'
artifact: ARMtemplates
- download: current
displayName: 'Download the App Service Web package'
artifact: AppService
- task: AzureResourceManagerTemplateDeployment@3
displayName: 'Deploy App Service ARM template'
inputs:
deploymentScope: 'Resource Group'
azureResourceManagerConnection: ${{ parameters.ARMConn}}
subscriptionId: '$(subscriptionId)'
action: 'Create Or Update Resource Group'
resourceGroupName: '$(resourceGroupName)'
location: '$(location)'
templateLocation: 'Linked artifact'
csmFile: '$(Pipeline.Workspace)/ARMtemplates/AppService.azuredeploy.json'
deploymentMode: 'Incremental'
deploymentOutputs: deploymentOutputs
overrideParameters: >-
...
- task: PowerShell@2
name: armOutput
displayName: 'Obtain Azure Deployment outputs'
inputs:
targetType: 'inline'
script: |
if (![string]::IsNullOrEmpty( '$(deploymentOutputs)' )) {
$DeploymentOutputs = convertfrom-json '$(deploymentOutputs)'
$DeploymentOutputs.PSObject.Properties | ForEach-Object {
$keyname = $_.Name
$value = $_.Value.value
Write-Host "The value of [$keyName] is [$value]"
Write-Host "##vso[task.setvariable variable=$keyname;isOutput=true]$value"
}
}
- task: AzureWebApp@1
displayName: 'Deploy App Service - Dev and Staging Environment Only'
condition: ne(variables['env'], 'prod')
inputs:
azureSubscription: ${{ parameters.ARMConn }}
appType: 'webApp'
appName: '$(armOutput.appServiceName)'
package: '$(Pipeline.Workspace)/AppService/AppService.zip'
- task: AzureWebApp@1
displayName: 'Deploy App Service to Staging Slot - Prod Environment Only'
condition: eq(variables['env'], 'prod')
inputs:
azureSubscription: ${{ parameters.ARMConn }}
appType: 'webApp'
appName: '$(armOutput.appServiceName)'
deployToSlotOrASE: true
resourceGroupName: '$(resourceGroupName)'
slotName: '$(slotName)'
package: '$(Pipeline.Workspace)/AppService/AppService.zip'
Accessing ARM Output Variables Across Jobs
Given that I will also need to access the appServiceName ARM Output that has been outputted from the armOutput task as a variable in future jobs, I have used the Output Variable for use in future jobs
configuration.
Rather than job only:
##vso[task.setvariable variable=$keyname]$value
It uses the IsOutput so we can setup dependencies and access later:
##vso[task.setvariable variable=$keyname;
isOutput=true
]$value
Note: Accessing a Output Variable is different to a non-output variable:
Non-output Variable:
'$(appServiceName)'
Output Variable:
'$(armOutput.appServiceName)'
, you will need to reference the task name prior to the variable.
Manual Slot Swap with Azure DevOps Approvals
At this point, we now have our App Service resources deployed and the Site deployed to the production slots for Dev and Staging, and deployed to the Staging slot for Production.
But now down to the real question, how do I manually swap the slots using some Azure DevOps governance goodness?
To do this we are going to use the Azure DevOps Environment Approvals which will allow specific groups or users to approve the slot swap. Following this we will use the AzureAppServiceManage@0 task to conduct the slot swap.
Setting Up Azure DevOps Environments and Approvals
To setup an environment and approvals:
- Sign in to your Azure DevOps organisation and open your project.
- Select Pipelines > Environments > Create Environment.
- Enter information for the environment, and then select Create. (For our purposes leave Resource as None)
- In your Environment, Select Approvals and checks tab, and then select the + sign to add a new check.
- Select Approvals, and then select Next.
- Add users or groups as your designated Approvers, and if desired the following
- Instructions to approvers
- Allow approvers to approve their own runs
- Make sure to Save.
Environments can be assigned to stages and jobs, but not to individual tasks. This means the slot swap task cannot reside within the environment-agnostic deployment template; it must be a separate, follow-on job.
To cater for the slot swap and environment approval, we are going to amend the orchestrating template to add another Job after the deployment Job in the Production deploy stage. This is shown below.
Adding the Slot Swap Job to the Pipeline
# Yaml
# Orchestrating Template: Build And Environment Deployments
...
- stage: 'Production'
...
jobs:
- deployment: Deploy_to_Production
...
steps:
- template: EnvironmentDeploy.azurepipelinetemplate.yml
...
- deployment: Slot_Swap_to_Production
dependsOn: Deploy_to_Production
condition: succeeded()
environment: ProductionEnv
variables:
appServiceName: $[ dependencies.Deploy_to_Production.outputs['Deploy_to_Production.armOutput.appServiceName'] ]
strategy:
runOnce:
deploy:
steps:
- task: AzureAppServiceManage@0
displayName: 'Swap App Service Staging Slot to Production'
inputs:
azureSubscription: 'XXXYYYZZZ'
action: 'Swap Slots'
WebAppName: '$(appServiceName)'
ResourceGroupName: '$(resourceGroupName)'
SourceSlot: '$(slotName)'
SwapWithProduction: true
As can be seen in the amended template above, the Slot_Swap_to_Production job runs after the Deploy_to_Production job and expects it to have succeeded as a condition to running. The Slot_Swap_to_Production job also references the environment ProductionEnv for Approval prior to running.
Note: The first time the environment is used on the pipeline, the pipeline will ask for permission to use the resource.
Referencing Output Variables in the Slot Swap Job
The Azure App Service Manage task requires the WebAppName
, which is set as an output variable in the environment-agnostic YAML pipeline template. To use this value in the Slot_Swap_to_Production
job, define a new variable that references the output variable from the previous job. Once defined, you can use it within the current job as a standard variable (e.g., $(appServiceName)
).
The variable reference uses the following syntax:
[dependencies.
JobName
.outputs[’JobName
.TaskName
.VariableName
]]
You now have a pipeline that at the point of the slot swap job will wait for an appropriate approval prior to running and swapping your staging slot to production.
Summary
By combining Azure App Service deployment slots, Infrastructure as Code, and Azure DevOps environment approvals, you can create a robust, auditable deployment pipeline. This approach ensures that production releases are gated by manual approval, reducing risk and enabling rapid rollback if needed.
Ready to take your deployments to the next level? Try setting up manual slot swaps with approvals in your next release pipeline, and let your team experience stress-free, governed production launches.
For the original version of this post see Andrew Wilson's personal blog at How to Set Up Manual Approval for Azure App Service Slot Swaps in Azure DevOps Pipelines