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