Azure, Bicep, Professional

Azure Bicep Registries

Introduction

Bicep registries allows development and/or operation teams to create a set of resource templates and centrally host these for additional teams or projects to consume. This post will cover what my developer experience has been with them and furthermore provide some guidance on scoping and creating the modules. A complete overview on this topic is available on MS Learn.

Why?

Publishing reusable modules to a central registry is a great way to control the enterprise’s definition of a resource. This may include certain security settings such as HTTPS only or minTLS versions are always defined as part of the template. For the record I still would highly encourage these settings to be configured via Azure Policy; however, the goal is to provide templates with these settings baked in to promote consistency and good practice.

By Publishing to a registry we also gain the ability to have multiple versions supported as each module can be versioned and consumed based on that version.

We also can do this as a way to empower developer velocity, and coupled with Azure Deployment Environments we can push that conversation further left.

Design

The first question when creating modules to be part of a registry, regardless if using Bicep, Terraform, or even AWS, is what is the intent and design pattern for the modules. Is the desire to create a full stack module which is being defined as an app and all it’s necessary components (App Service, App Service Plan, App Insights, etc.. contained in one file) or modules for an individual service (separate file for each Azure service)?

Though both sides of the question have their merits; I prefer to design for each individual service. This will optimize for reusability; however, the downside is overhead in number of files and accounting for more numbers of modules.

Coding a Module

When writing a module scoped to the individual resource it is important to try and account for a variety of inputs. If we need to add an input as a parameter later it is not the end of the world. All you have to do is defined as a paramter and defaulted to the previous value. This would ensure that the files consumign the module will not be impacted by the introduction of a new parameter.

For this example we will account for a scenario of spinning up an App Service. We are going to account for the ability to assign either a User and System Assigned Identity, or just a System Assigned Identity. I did skip the use case of just a User Assigned Identity as having the additional System Assigned Identity does not come with a downside as this is attached to the lifecycle of the resource:

@description('User Asisgned Identity for App Service')
param principalId string = ''

......
 
identity: empty(principalId) ? {
    type: 'SystemAssigned'

  } : {
    type: 'SystemAssigned, UserAssigned'
    userAssignedIdentities: {
      '${principalId}': {}

    }
  }

This code will detect if a principal id was supplied, if not then we will just leverage the System Assigned Identity.

When breaking down our modules to individual services a few important items to remember will be to validate our parameters. This can be done with the @allowed keyword when declaring them. Here is a great example where perhaps we want to limit what type of App Service Plans we will allow. In this case we want just windows:

@description('AppService Plan Kind')
@allowed([ 'windows', 'windowscontainer' ])
param appServiceKind string = 'windows'

This can be important when we are wanting to control how our template can be used.

Just as important as parameters will be the outputs of our modules. A good idea to just help will be to output names of resources and or resource IDs. A lot of time these are the two most common properties that will be leveraged by other resources in this approach. If you decide later to add an output it is not an issue as it is a non breaking change, unless you are making changes to an already defined output.

Publishing a Module

If we have a module we’d like to publish we just need to run the following command:

az bicep publish --file <module_file_name>.bicep --target br:exampleregistry.azurecr.io/bicep/modules/storage:v1

Main Bicep File

The Bicep file will call modules in the Azure Container Registry (ACR). The module keyword and approach is the same as if the module file was local to the main.bicep the only difference is the location will be the ACR registry URL and the br keyword. For this to work the developer and the deployment tooling will need to have ACR pull abilities.

Here is an example module block:

module userAssignedIdentity 'br:acrbicepregistrydeveus.azurecr.io/bicep/modules/userassignedidentity:v1' ={
  name: 'userAssignedIdentityModule'
  params:{
    location: location
    userIdentityName: nameSuffix
  }
}

And what the file looks like in the bicep registry:

There are a few considerations I wish I was aware of earlier when working in my local VS Code and modules hosted in a remote registry.

First is that intellisense works great; however, understand what it is really doing is in fact downloading the registry definition of the module and caching that locally. It will not build/validate/deploy from the remote definition rather the cached one.

This can be very frustrating if there has been a change that has been published to the ACR. Any local builds, deployments, or validations will be against the cached version. For me, this lead to a scenario where changes made to the module was not being reflected when I did a build/deploy from my local machine. The reason being is it had no idea the file in the ACR had changed.

To correct this I just needed to run:

az bicep restore --file main.bicep [--force]

After doing this my cache was rebuilt and repulled the necessary modules and all was well. This is important if you are building and deploying the resulting ARM .json template. Which I also happened to be doing since Azure Deployment Environments only supports ARM at the time of this writing.

Bicep Build

Running a bicep build command will produce an ARM template. The cool thing here is the ARM template is fully expanded from the modules defined in the registry. Again note if running this locally it will be based off your local cache definition of the modules, if through tooling such as GitHub or ADO it will be the latest version in the registry.

Conclusion

Bicep modules can be a great way to define your bicep templates and use them across projects and teams. However, careful thought should be put into thinking what all will be defined in our module. What parameters will it take, what will it output, and how should the module be versioned are all questions any organization should evaluate when implementing.