Bicep Modules: Refactor, Compose, Reuse
Lessons learnt by refactoring Bicep templates into reusable modules
In my previous post I touched on the things I learnt while migrating ARM templates to Bicep. Bicep also introduces the concept of modules to enable template reuse. I took some time to refactor a composite application that had already been converted from using ARM to Bicep templates, to use Bicep modules. This post will cover the things that I learnt by working through that process.
From the Bicep documentation:
Bicep enables you to break down a complex solution into modules. A Bicep module is a set of one or more resources to be deployed together. Modules abstract away complex details of the raw resource declaration, which can increase readability. You can reuse these modules, and share them with other people. Bicep modules are transpiled into a single ARM template with nested templates for deployment.
One of the traps we fell into with ARM templates was duplicating templates to make composing and deploying the resources we need easier. Any opportunity to make the deployment of infrastructure more readable, more reusable, and more composable are excellent reasons for me to give it a go. My goal when doing this refactor was to
- get rid of any duplication by creating fine-grained modules, designed to be reused
- ensure that all main templates are super easy to use by composing modules together in a way that makes sense for the application
- enable reusability of the fine-grained modules in other projects going forward.
In short: let’s make the infra readable, composable and reusable.
I went with the approach of trying to make a reusable module for each Azure resource type and putting the modules into a folder called
modules and making sub folders for template groupings. For example,
Personally I prefer to have one module to create a storage account and another to add tables to that storage account etc. This level of granularity felt like a good place to start.
Referencing existing resources inside a module
By making fine-grained modules, there were a number of use cases where I would need to reference an existing resource. For example, creating tables in a storage account requires an existing storage account. Referencing an existing resource is really easy – you only need to know its name and can reference it as follows,
I really like the loops feature. This allows you to iterate over an array setting multiple properties or creating multiple resources etc. This came in super handy for a storage account tables module that can create multiple tables in one go. E.g.
Note the use of the different styles of the loops in the
storageAccountTables resource and the outputs.
Functions and Expressions
There are loads of functions and expressions that you can use in your Bicep files and I won’t go into all of them. There were a few that I used regularly, and it’s hopefully useful that I call them out.
union(arg1, arg2, arg3, ...)Returns a single array or object with all elements from the parameters. Duplicate values or keys are only included once.
I used union everywhere I wanted to have fixed (opinionated) defaults in the module, but allow additional parameters to be merged in. For example, with function apps I defined base settings I want all function apps to have and still allow for additional app settings to be passed in and merged with the base.
We can also get rid of all the hardcoded schema API versions when looking up keys against a resource by using
<resource>.apiVersion as well as hardcoded suffixes by using the environment function, in this case using the storage suffix:
Another super useful call out would be to the
listKeys function. It allows you to get connection strings or keys from your resources and is super handy (see the
AzureWebJobsStorage example above). John Reilly went into detail on this over here, please have a read!
I found that with Bicep I use the ternary operator a lot in my main templates when composing the modules. For example, only enabling a secondary region when the environment is
prod but not in
dev can easily be described as,
Much cleaner and readable without all the JSON around it.
Composing modules together
Every Bicep file can be consumed as a module which is an awesome feature. I chose to break my files up as follows:
main.bicep I reference the two local Bicep files as modules
Note that the
app module relies on the outputs of the monitoring module – this helps Bicep figure out the dependencies so that you don’t need to define
dependsOn any more.
monitoring.bicep I can reference fine-grained reusable modules that I’d like to share with other projects in much the same way
Another thing to note is the
name of your module is what is shown in the
Deployments tab of the Azure Portal, so make these make sense to you for easy debugging if deployments are breaking.
At this stage I’ve got different two styles of modules – app specific modules breaking up my
main.bicep, making it easier to read and maintain and fine-grained templates in a
modules folder that I can use across all the apps in my
infra folder. Readable – check! Composable – check! Reusable – only inside this repo, so half a check!
The current version of Bicep (
v0.4.63), does not have a native mechanism to externally share modules across projects. The good news is that this is being looked at and will hopefully be in the v0.5 release. The issues to watch are:
In the interim, I am using Git submodules to solve this, although this will not work with all CI/CD tooling when using private repos. To enable this, I moved the modules in
shared-infra into its own repo and added it back to my project.
To quickly validate the individual modules and main templates on my local machine, I wrote a simple bash script to either
- build the Bicep file, outputting to the terminal rather than writing to file or
- validate the Bicep template against a resource group
The script allows one to test either
shared templates, linting them or validating them:
After the refactor, my project is in a far cleaner state and the infrastructure is much easier to follow. A second plus is that we now have a separate repo of our shared modules that can become a shared asset across our various teams (using Git submodules for now and the Bicep registry in the future).