Skip to content

Deploy a containerized app to Azure App Service using Bicep

Published:

In this post we will explore how to use Azure Bicep to deploy a containerized app to Azure App Service. It will use a System Managed Identity to pull an image from Azure Container Registry and finally setup a trigger to redeploy the container when a new image is pushed to the registry.

Introduction

I’ve written (in Dutch) about Infrastructure as Code (IaC) and Azure Bicep a few years ago and it’s still a tool I use on a regular basis. IaC prevents manual configuration and deployment of (cloud) infrastructure in order to reduce human error, make environments more consistent and speed up the deployment of new environments.

Container registry

Let’s assume there is an Azure Container Registry called crbicepdemo01 in resource group rg-bicepdemo-01. The container registry has admin user access disabled because we only want to use Managed Identities to access the registry.
The container registry has an image called exampleimage with tag latest.

App Service

To make the Bicep more manageable the example will use modules to seperate the various bits of the deployment. The first module deploys the App Service Plan and the App Service itself:

param location string
param appServiceName string
param dockerImage string

resource appServicePlan 'Microsoft.Web/serverfarms@2022-03-01' = {
  name: toLower('asp-${appServiceName}')
  location: location
  kind: 'linux'
  sku: {
    name: 'B1'
  }
  properties: {
    reserved: true
  }
}

resource appService 'Microsoft.Web/sites@2022-03-01' = {
  name: appServiceName
  location: location
  kind: 'app,linux,container'
  identity: {
    type: 'SystemAssigned'
  }
  properties: {
    serverFarmId: appServicePlan.id
    siteConfig: {
      linuxFxVersion: 'DOCKER|${dockerImage}'
      acrUseManagedIdentityCreds: true
    }
  }
}

output principalId string = appService.identity.principalId
output appServiceResourceName string = appService.name

The Bicep creates a new App Service Plan and App Service. The App Service Plan is a Linux plan with SKU B1 and the App Service uses a Linux container.
The App Service has a System Managed Identity and the acrUseManagedIdentityCreds property is set to true to enable the use of the Managed Identity to pull images from the container registry. The dockerImage parameter is the full image name including the tag: crbicepdemo01.azurecr.io/exampleimage:latest. The App Service has no access to the container registry yet, but we will fix that in the next module.

The module outputs the principalId and the name of the App Service. The next module will use the principalId to grant the Managed Identity access to the container registry and the name is used so there is an implicit dependency between the modules, causing the next module to wait for the App Service to be deployed before it starts.

Container registry access

The second module will give the Managed Identity of the App Service access to the container registry:

param containerRegistryName string
param appServicePrincipalId string

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

// acrPullDefinitionId is AcrPull: https://learn.microsoft.com/en-us/azure/role-based-access-control/built-in-roles#acrpull
var acrPullDefinitionId = '7f951dda-4ed3-4680-a7ca-43fe172d538d'

resource roleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
  name: guid(containerRegistry.name, appServicePrincipalId, 'AcrPullSystemAssigned')
  scope: containerRegistry
  properties: {
    principalId: appServicePrincipalId
    principalType: 'ServicePrincipal'
    roleDefinitionId: resourceId('Microsoft.Authorization/roleDefinitions', acrPullDefinitionId)
  }
}

We assign a built-in role called AcrPull to the Managed Identity. The role can only pull images and is scoped to the container registry so it only has access to that specific resource.

Putting the modules together

The modules are glued modules together in a main Bicep file:

param location string = resourceGroup().location
param appServiceName string
param containerRegistryName string
param dockerImageNameTag string

var dockerImage = '${containerRegistryName}.azurecr.io/${dockerImageNameTag}'

module appService 'appservice.bicep' = {
  name: '${appServiceName}-app'  
  params: {
    appServiceName: appServiceName    
    location:	location
    dockerImage: dockerImage
  }
}

module roleAssignment 'roleassignment.bicep' = {
  name: '${appServiceName}-roleassignment'  
  params: {
    appServicePrincipalId: appService.outputs.principalId
    containerRegistryName: containerRegistryName
  }
}

To execute the Bicep use this Azure CLI command in PowerShell:

az deployment group create `
    --resource-group rg-bicepdemo-01 `
    --template-file main.bicep `
    --parameters `
        appServiceName='bicepdemo01' `
        containerRegistryName='crbicepdemo01' `
        dockerImageNameTag='exampleimage:latest'

Redeploy on image push

Let’s setup a trigger to redeploy the container when a new image is pushed to the container registry. In order to do this a webhook is added to the container registry. The webhook will call the App Service and trigger a redeploy.

Create a new module that registers the trigger:

param appServiceName string
param containerRegistryName string
param location string = resourceGroup().location
param triggerScope string

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

resource publishingcreds 'Microsoft.Web/sites/config@2022-03-01' existing = {
  name: '${appServiceName}/publishingcredentials'
}

var webhookUri = publishingcreds.list().properties.scmUri

resource hook 'Microsoft.ContainerRegistry/registries/webhooks@2021-09-01' = {
  name: appServiceName
  parent: containerRegistry
  location: location
  properties: {
    serviceUri: '${webhookUri}/api/registry/webhook'
    scope: triggerScope
    status: 'enabled'
    actions: [
      'push'
    ]
  }
}

First the scmUri (Source Control Management URI) is retrieved from our app service. This is a URI which contains a username, secret and the SCM endpoint of the app service in the following format: https://$bicepdemo01:<secret>@bicepdemo01.scm.azurewebsites.net/. Then /api/registry/webhook is added to the URI to get the webhook URI that can be used in the trigger.
In this case the trigger is only scoped to the image and latest tag: exampleimage:latest.

After that, add the module to the main Bicep file:

module ciTrigger 'citrigger.bicep' = {
  name: '${appServiceName}-citrigger'  
  params: {    
    appServiceName: appService.outputs.appServiceResourceName
    location: location
    containerRegistryName: containerRegistryName
    triggerScope: dockerImageNameTag
  }
}

Finally, add an app setting to enable continuous integration in the App Service:

      appSettings: [
        {
          name: 'DOCKER_ENABLE_CI'
          value: 'true'
        }
      ]

Any new version of the exampleimage:latest Docker image in the Container Registry will now trigger a redeploy of the App Service.

Full code

Full code including Bicep for the Container Registry and an example custom docker image is available on GitHub:
https://github.com/mawax/appservice-bicep