Implementing Blue/Green Deployments with Azure Web Apps for Containers

  • Creating our Azure resources with Bicep.
  • Deploying our resources with GitHub Actions.
  • Pushing our container image to Azure Container Registry.
  • Pulling our container image into our App Service Blue Slot.
  • Swapping from the Blue slot into our Green Slot.

What are Blue/Green Deployments?

As I mentioned, ensuring that our applications are highly available is critical for applications running in the cloud. There are a couple of strategies that we can implement to ensure HA for our apps, such as spinning up our application in a different region in case of a regional-disaster affecting the datacenter where our app is hosted or using high availability configurations. With both of these options, there are time and cost implications that we would need to consider.

Preparing our infrastrucutre

Let’s start off by setting up our infrastructure pipeline. Here, I want to create my App Service that has both a blue and green slot, an Azure Container Registry to store my images, then I want to be able to deploy my Bicep template using GitHub Actions.

Creating our Azure Resources

First, let’s create the Azure Container Registry that we need to store our container images. We can do this in Bicep like so:

param registryName string
param registryLocation string
param registrySku string

resource containerRegistry 'Microsoft.ContainerRegistry/registries@2021-09-01' = {
name: registryName
location: registryLocation
sku: {
name: registrySku
}
identity: {
type: 'SystemAssigned'
}
}
param appServicePlanName string
param appServicePlanLocation string
param appServicePlanSkuName string
param appServicePlanCapacity int

resource appServicePlan 'Microsoft.Web/serverfarms@2021-02-01' = {
name: appServicePlanName
location: appServicePlanLocation
sku: {
name: appServicePlanSkuName
capacity: appServicePlanCapacity
}
kind: 'linux'
properties: {
reserved: true
}
}

output appServicePlanId string = appServicePlan.id
@description('Name of the app service plan')
param appServiceName string

@description('Location of the app service plan')
param appServiceLocation string

@description('Name of the slot that we want to create in our App Service')
param appServiceSlotName string

@description('The Server Farm Id for our App Plan')
param serverFarmId string

@description('Name of the Azure Container Registry that this App will pull images from')
param acrName string

@description('The docker image and tag')
param dockerImageAndTag string = '/hellobluegreenwebapp:latest'

// This is the ACR Pull Role Definition Id: https://docs.microsoft.com/en-us/azure/role-based-access-control/built-in-roles#acrpull
var acrPullRoleDefinitionId = subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '7f951dda-4ed3-4680-a7ca-43fe172d538d')

var appSettings = [
{
name: 'WEBSITES_ENABLE_APP_SERVICE_STORAGE'
value: 'false'
}
{
name: 'WEBSITES_PORT'
value: '80'
}
{
name: 'DOCKER_REGISTRY_SERVER_URL'
value: 'https://${containerRegistry.properties.loginServer}'
}
]

resource containerRegistry 'Microsoft.ContainerRegistry/registries@2021-09-01' existing = {
name: acrName
}

resource appService 'Microsoft.Web/sites@2021-02-01' = {
name: appServiceName
location: appServiceLocation
kind: 'app,linux,container'
properties: {
serverFarmId: serverFarmId
siteConfig: {
appSettings: appSettings
acrUseManagedIdentityCreds: true
linuxFxVersion: 'DOCKER|${containerRegistry.properties.loginServer}/${dockerImageAndTag}'
}
}
identity: {
type: 'SystemAssigned'
}

resource blueSlot 'slots' = {
name: appServiceSlotName
location: appServiceLocation
kind: 'app,linux,container'
properties: {
serverFarmId: serverFarmId
siteConfig: {
acrUseManagedIdentityCreds: true
appSettings: appSettings
}
}
identity: {
type: 'SystemAssigned'
}
}
}

resource appServiceAcrPullRoleAssignment 'Microsoft.Authorization/roleAssignments@2020-08-01-preview' = {
scope: containerRegistry
name: guid(containerRegistry.id, appService.id, acrPullRoleDefinitionId)
properties: {
principalId: appService.identity.principalId
roleDefinitionId: acrPullRoleDefinitionId
principalType: 'ServicePrincipal'
}
}

resource appServiceSlotAcrPullRoleAssignment 'Microsoft.Authorization/roleAssignments@2020-08-01-preview' = {
scope: containerRegistry
name: guid(containerRegistry.id, appService::blueSlot.id, acrPullRoleDefinitionId)
properties: {
principalId: appService::blueSlot.identity.principalId
roleDefinitionId: acrPullRoleDefinitionId
principalType: 'ServicePrincipal'
}
}
  • Let’s start with our parameters. We’ll need to import both the App Service Plan and the Docker Registry into this Bicep file, so I’ve added some parameters that we can use in the template. This may be scope creep for this tutorial, but say we have Container Registries across our different environments (DEV, UAT, Prod) and different app service plans, we can provision new App Services that use our different resources across our different environments taking this approach.
  • We then create a variable for our AcrPull role definition. We do this by getting the unique identifier for a resource using the subscriptionResourceId function. - We create another variable for our App Settings. For our example, both our Blue and Green slot will need to have the same app settings. We could use different app settings for our slots if we needed/wanted to, but for simplicity, I've created a variable and applied them to both slots just so I don't have to repeat myself. - Our Container Registry needs to be imported so I can reference it in our App Service. All we need to do here is to create a resource block for our Container Registry and use the keyword existing. We reference the name of our registry using a parameter.
  • First we set the kind property to app,linux,container. This tells the App Service that we want to host Linux Containers.
  • In the properties section, we set the serverFarmId to the Id of our Server Farm. We're setting this as a parameter, so if we wanted to use this module for other App Services, we could deploy it to another App Service Plan.
  • For our site config section, we set the appSettings to the App Settings we defined earlier, the acrUseManagedIdentityCreds property tells the App Service that we want to use the managed identity credentials to pull images from our container registry. We finally set the linuxFxVersion to DOCKER|${containerRegistry.properties.loginServer}/${dockerImageAndTag}. This sets the Linux App Framework and version.
  • We finally create a System Managed Identity for this App Service and the Blue slot. We’ll need this for our role assignments.
param webAppName string = uniqueString(resourceGroup().id)
param acrName string = toLower('acr${webAppName}')
param acrSku string
param appServicePlanName string = toLower('asp-${webAppName}')
param appServiceName string = toLower('asp-${webAppName}')
param appServicePlanSkuName string
param appServicePlanInstanceCount int

var appServiceSlotName = 'blue'

param location string = resourceGroup().location

module containerRegistry 'containerRegistry.bicep' = {
name: 'containerRegistry'
params: {
registryLocation: location
registryName: acrName
registrySku: acrSku
}
}

module appServicePlan 'appServicePlan.bicep' = {
name: 'appServicePlan'
params: {
appServicePlanLocation: location
appServicePlanName: appServicePlanName
appServicePlanSkuName: appServicePlanSkuName
appServicePlanCapacity: appServicePlanInstanceCount
}
}

module appService 'appService.bicep' = {
name: 'appService'
params: {
appServiceLocation: location
appServiceName: appServiceName
serverFarmId: appServicePlan.outputs.appServicePlanId
appServiceSlotName: appServiceSlotName
acrName: acrName
}
}
{
"$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#",
"contentVersion": "1.0.0.0",
"parameters": {
"acrSku":{
"value": "Basic"
},
"appServicePlanSkuName": {
"value": "S1"
},
"appServicePlanInstanceCount": {
"value": 1
}
}
}

Deploying our resources with GitHub Actions

Now that our Bicep code has been written, we can deploy it using GitHub Actions. Before we can do this, we’ll need to set up a couple of things.

az group create -n <resource-group-name> -l <location>
az ad sp create-for-rbac --name yourApp --role owner --scopes /subscriptions/{subscription-id}/resourceGroups/exampleRG --sdk-auth
{
"clientId": "<GUID>",
"clientSecret": "<GUID>",
"subscriptionId": "<GUID>",
"tenantId": "<GUID>",
}
name: Deploy Azure Infrastructure

on:
push:
paths:
- 'deploy/*'
workflow_dispatch:

jobs:

lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Run Bicep Linter
run: az bicep build --file ./deploy/main.bicep

validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: azure/login@v1
name: Sign in to Azure
with:
creds: ${{ secrets.AZURE_CREDENTIALS }}

- uses: azure/arm-deploy@v1
name: Run preflight validation
with:
deploymentName: ${{ github.run_number }}
resourceGroupName: ${{ secrets.AZURE_RG }}
template: ./deploy/main.bicep
parameters: ./deploy/parameters.json
deploymentMode: Validate

preview:
runs-on: ubuntu-latest
needs: [lint, validate]
steps:
- uses: actions/checkout@v2
- uses: azure/login@v1
name: Sign in to Azure
with:
creds: ${{ secrets.AZURE_CREDENTIALS }}
- uses: Azure/cli@v1
name: Run what-if
with:
inlineScript: |
az deployment group what-if --resource-group ${{ secrets.AZURE_RG }} --template-file ./deploy/main.bicep --parameters ./deploy/parameters.json
deploy:
runs-on: ubuntu-latest
environment: Dev
needs: preview
steps:
- uses: actions/checkout@v2

- uses: azure/login@v1
with:
creds: ${{ secrets.AZURE_CREDENTIALS }}

- name: Deploy Bicep File
uses: azure/arm-deploy@v1
with:
subscriptionId: ${{ secrets.AZURE_SUBSCRIPTION }}
resourceGroupName: ${{ secrets.AZURE_RG }}
template: ./deploy/main.bicep
parameters: ./deploy/parameters.json
failOnStdErr: false
  • We first validate our file to ensure that it’s a valid Bicep template.
  • We then run a preview job on our Bicep template to see what will be deployed as part of our workflow.
  • We then run a deploy stage that uses a manual approval step. This is done by putting the environment parameter in this stage. When we approve this deployment, the stage deploys our Bicep template using the parameters file to deploy our resources to Azure.

Deploying our container to App Service

We have our Container Registry and App Service ready to go in Azure! We just need to allow our GitHub Action to push and push images to our Azure Container Registry.

registryId=$(az acr show --name <registry-name> --query id --output tsv)
az role assignment create --assignee <ClientId> --scope $registryId --role AcrPush

Pushing our container image to Azure Container Registry

We can now create our workflow file. This file will do quite a bit so let’s focus on pushing our Container Image to Azure Container Registry to begin with.

name: Build and Deploy Container Image to App Service

on:
workflow_dispatch:

defaults:
run:
working-directory: ./src

# Note: The use of :latest for the container image is not recommeded for production environments.
jobs:
build-container-image:
runs-on: ubuntu-latest
steps:
- name: 'Checkout GitHub Action'
uses: actions/checkout@main

- name: 'Login via Azure CLI'
uses: azure/login@v1
with:
creds: ${{ secrets.AZURE_CREDENTIALS }}

- name: 'Build and Push Image to ACR'
uses: azure/docker-login@v1
with:
login-server: ${{ secrets.REGISTRY_LOGIN_SERVER }}
username: ${{ secrets.REGISTRY_USERNAME }}
password: ${{ secrets.REGISTRY_PASSWORD }}
- run: |
docker build . -t ${{ secrets.REGISTRY_LOGIN_SERVER }}/hellobluegreenwebapp:latest
docker push ${{ secrets.REGISTRY_LOGIN_SERVER }}/hellobluegreenwebapp:latest

Deploying our App to the Blue slot

Once our image has been pushed to Azure Container Registry, we can deploy it to our Blue slot. In the snippet below, we log into Azure, then retrieve our Application name using a inline script and set it to a output variable that we can use in a later step in this stage.

deploy-to-blue-slot:
needs: build-container-image
runs-on: ubuntu-latest
steps:
- name: 'Checkout GitHub Action'
uses: actions/checkout@main

- name: 'Login via Azure CLI'
uses: azure/login@v1
with:
creds: ${{ secrets.AZURE_CREDENTIALS }}

- name: 'Get App Name'
id: getwebappname
run: |
a=$(az webapp list -g ${{ secrets.AZURE_RG }} --query '[].{Name:name}' -o tsv)
echo "::set-output name=appName::$a"

- name: 'Deploy to Blue Slot'
uses: azure/webapps-deploy@v2
with:
app-name: ${{ steps.getwebappname.outputs.appName }}
images: ${{ secrets.REGISTRY_LOGIN_SERVER }}/hellobluegreenwebapp:latest
slot-name: 'blue'

Verify that the blue slot works

n this sample, we can verify whether or not our deployment to the blue slot was successful by simply navigating to the blue slot of our App Service.

Swap to Green slot

Once we have verified that our deployment to the blue slot has been successful, we can deploy our container image to the green slot. We can do this using the following job in our GitHub Actions workflow:

swap-to-green-slot:
runs-on: ubuntu-latest
environment: Dev
needs: deploy-to-blue-slot
steps:
- name: 'Checkout GitHub Action'
uses: actions/checkout@main

- name: 'Login via Azure CLI'
uses: azure/login@v1
with:
creds: ${{ secrets.AZURE_CREDENTIALS }}

- name: 'Get App Name'
id: getwebappname
run: |
a=$(az webapp list -g ${{ secrets.AZURE_RG }} --query '[].{Name:name}' -o tsv)
echo "::set-output name=appName::$a"

- name: 'Swap to green slot'
uses: Azure/cli@v1
with:
inlineScript: |
az webapp deployment slot swap --slot 'blue' --resource-group ${{ secrets.AZURE_RG }} --name ${{ steps.getwebappname.outputs.appName }}

Conclusion

If you’re deploying containers to Azure App Service, hopefully this has helped you see that you can use Blue/Green deployments to achieve zero-downtime deployments for your container images.

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Will Velida

Will Velida

Customer Engineer at Microsoft working in the Fast Track for Azure team. GitHub: https://github.com/willvelida