Eduard Keilholz

Hi, my name is Eduard Keilholz. I'm a Microsoft developer working at 4DotNet in The Netherlands. I like to speak at conferences about all and nothing, mostly Azure (or other cloud) related topics.
LinkedIn | Twitter | Mastodon | Bsky


I received the Microsoft MVP Award for Azure

Eduard Keilholz
HexMaster's Blog
Some thoughts about software development, cloud, azure, ASP.NET Core and maybe a little bit more...

Centralizing your Bicep templates in ACR

Even the most skilled developer sometimes repeats him/herself although we always try to prevent that. When you’re writing Infrastructure as Code for an organization, you often find that some resources are deployed multiple times for different projects. Assume you’re writing a Microservices solution using C# and host these services as Web Apps in Microsoft Azure. Probably 80 to 90% of the cloud resources are similar if not exactly the same for every service. When using Bicep for your IaC, you can write your infrastructure in modules. These modules, often independent deployable bicep files, can be copied from one project to another to share the code. But there is a better way, the Azure Container Registry.

Say What?

Yes, the Azure Container Registry (ACR) was found very suitable for storing Bicep files. This comes with some advantages.

  • You now have version control over your modules
  • You don’t copy the modules from one project to another
  • You have full control over who can access the registry

To follow along, you need access to at least one Azure Subscription. Provisioning the resources described in this post may result in an invoice.

In the Azure Portal, I created a resource group and an ACR in that resource group. The URL to your ACR registry is <acr-registry>.azurecr.io. Remember this URL, you will need it later on.

Then I started writing a couple of Bicep modules. My personal strategy is that a Bicep module should be as re-usable as possible. This is sometimes hard because it makes your templates more complex and therefore harder to consume. To I came up with the idea to write a module per resource and call that ‘core’ modules. Just for myself.

A practical example

Se let’s say you want to create a reusable template for the deployment of an App Service. This App Service requires an App Service Plan to run on. In this case, I create two module files. One for the App Service Plan, and one for the Web App.

This snippet could be a good starting point for a module creating an App Service Plan.

The templates shown in this post are not real-world example templates. They work properly and will do the job, but you may want to consider some changes in case you want to use these templates for production workloads.

// core/web/serverfarms.bicep
@description('Default resource name for DIVA deployments')
param defaultResourceName string
@description('Location of this deployment (default resourceGroup().location)')
param location string = resourceGroup().location

@description('Kind of App Service Plan to create (default app)')
@allowed([
  'functionapp'
  'linux'
  'app'
])
param kind string = 'app'

@description('Sku (Stock Keeping Unit) of the App Service Plan (default S1 with a capacity of 1)')
param sku object = {
  name: 'S1'
  capacity: 1
}

var resourceName = '${defaultResourceName}-plan'

resource appFarm 'Microsoft.Web/serverfarms@2020-12-01' = {
  name: resourceName
  location: location
  kind: kind
  sku: {
    name: sku.name
    capacity: sku.capacity
  }
}

output name string = appFarm.name
output id string = appFarm.id

And this snippet would create a Web App on top of a given service plan:

// core/web/sites.bicep
@description('Default resource name for DIVA deployments')
param defaultResourceName string
@description('Location of this deployment (default resourceGroup().location)')
param location string = resourceGroup().location

@description('Resource ID of the app service plan to run this web app on')
param appServicePlanId string

@description('Kind of web app (default app)')
@allowed([
  'app'
  'linux'
  'functionapp'
])
param kind string = 'app'

var resourceName = '${defaultResourceName}-${kind}'

resource webApp 'Microsoft.Web/sites@2020-12-01' = {
  name: resourceName
  location: location
  kind: kind
  properties: {
    serverFarmId: appServicePlanId
    httpsOnly: true
    clientAffinityEnabled: false
    siteConfig: {
      alwaysOn: true
      ftpsState: 'Disabled'
      http20Enabled: true
    }
  }
  identity: {
    type: 'SystemAssigned'
  }
}

output servicePrincipal string = webApp.identity.principalId
output webAppName string = webApp.name
output targetUrl string = webApp.properties.hostNames[0]

As you can see in the snippets above, Azure Resources are structured using a namespace that always starts with Microsoft. and ends with something that describes the area the given resource belongs to, in this case, Web. I always use this last part of the namespace to structure my own bicep files. So in this case, I created a folder core with a child folder called web to structure my bicep files.

Publishing to the ACR

To be able to consume these modules, the Bicep files must be published to the ACR. To do this, I use an Azure CLI command. You must be logged in with the Azure CLI to do so. If you are not logged in, type az login to log in, and az account set --subscription <your-subscription> to set the proper subscription.

Now to publish a Bicep file as a module to your ACR, type az bicep publish <your-bicep-file> --target "br:<acr-registry>.azurecr.io/<path>/<name>:<version>". Let’s now publish the bicep files created earlier to the ACR:

az bicep publish --file core/web/serverfarms.bicep --target "br:<acr-registry>.azurecr.io/modules/core/web/serverfarms:v0.1"
az bicep publish --file core/web/sites.bicep --target "br:<acr-registry>.azurecr.io/modules/core/web/sites:v0.1"

Now navigate to your ACR in the portal and open the ‘Repositories’. You will see the two templates listed. When you open either one of them, you can see that the templates have a version (in this case v0.1). When you re-deploy a template with a different version, the template will become available in multiple versions.

Make smarter templates

I realize that these modules are not as powerful as you would like, but hey… you’re only halfway through this post. Next up, are the (what I call) ‘shared’ modules. These shared modules consume and combine core modules. So I created a new folder called shared containing a file webapp.bicep. This template file will combine the core modules into a smarter, more powerful module.

// shared/webapp.bicep
@description('Name of the service, limited to 10 characters to prevent too long resource names')
@maxLength(10)
param serviceName string

@description('Name of the target environment')
@allowed([
  'Dev'
  'Tst'
  'Prd'
])
param environmentName string

@description('Abbreviation of the target location for DIVA most often weu (default weu)')
param locationAbbreviation string = 'weu'

@description('Azure location to deploy the infrastructure to (default resourceGroup().location)')
param location string = resourceGroup().location

@description('Kind of App Service Plan to create (default app)')
@allowed([
  'functionapp'
  'linux'
  'app'
])
param kind string = 'app'

@description('Sku of the web application to create, defaults to S1 with a capacity of 1')
param webAppSku object = {
  name: 'S1'
  capacity: 1
}

var defaultResourceName = toLower('${serviceName}-${environmentName}-${locationAbbreviation}')

module webApplicationServerFarmModule 'br:armbicep.azurecr.io/modules/core/web/serverfarms:v0.1' = {
  name: 'webApplicationServerFarmModule'
  params: {
    defaultResourceName: defaultResourceName
    location: location
    kind: kind
    sku: webAppSku
  }
}

module webApplicationModule 'br:armbicep.azurecr.io/modules/core/web/sites:v0.1' = {
  name: 'webApplicationModule'
  params: {
    defaultResourceName: defaultResourceName
    location: location
    appServicePlanId: webApplicationServerFarmModule.outputs.id
    kind: kind
  }
}

output appServicePlanId string = webApplicationServerFarmModule.outputs.id
output webAppName string = webApplicationModule.outputs.webAppName
output webAppPrincipalId string = webApplicationModule.outputs.servicePrincipal

As you can see, the template now consists of mostly parameters and consumes two modules. These modules are the core modules created earlier.

Publish the product

Now it’s time to publish the shared/webapp.bicep file, to also make this available for re-use:

az bicep publish --file shared/webapp.bicep --target "br:<acr-registry>.azurecr.io/modules/shared/webapp.bicep:v0.1"

A public repository

The Azure team is now working on a public repository with a public ACR with modules that you can consume. The source for this public repository can be found here.