Creating Reusable Infrastructure as Code Part 2: Creating Modules with Azure Bicep

In part 2 of our series on creating reusable infrastructure as code, we will look at how you can create modules in the Azure Bicep language. By using Bicep modules we can create self contained units of infrastructure that can be reused and shared with your team.

If you missed it, you can read part 1 here and learn why you might want to create reusable IaC modules and what the benefits are. You can also watch my session at the Cloud Engineering Summit, where I talk about this topic.

How do modules work in Bicep?

Modules are a top-level resource in Bicep and allow you to wrap multiple Bicep resources, parameters and variables into a package that you can call from inside your top-level file.

You may recall that under the hood, Bicep compiles into ARM templates which are the things that are deployed. Modules are no different; they are an abstraction on the nested template tooling in ARM templates. Nested templates could be used in ARM to create reusable code, but they were a pain to write and debug, particularly when you have multiple levels of nesting. Bicep modules compile into inline nested templates, so they are not separate ARM code files but stored inline. This will result in your ARM templates becoming quite large and complex. However, the significant benefit of Bicep is that you don’t need to care what the ARM looks like, as it’s being generated for you. You can focus on creating the Bicep side of things and rely on the Bicep engine to compile it.

Creating a Bicep Module

A Bicep module isn’t anything special in terms of code; it’s just a standard set of Bicep code. The main difference is that you will create it in a separate file, and you need to pay attention to the parameters and outputs in that file. When creating a Bicep module, the critical thing is how you define the interface for your module - what parameters it requires and what outputs it provides. This is what will determine how the users of your module will interact with it.

The code below is a simple module for creating a storage account, and we have used parameters and outputs to define our interface.

  • For inputs, we allow the user to define the prefix used in the name, the SKU of the storage account and the location. Everything else is pre-defined inside the module
  • The outputs provide information that may be useful for the person consuming the module, which they may want to pass on to other parts of their code. Here we provide the storage account name and ID and the primary endpoint object. We can pass both simple strings but also more complex objects. By passing objects, we can avoid having to define separate outputs for every item. We could even pass out the whole storage account object if we wanted to.
param storagePrefix string

param storageSKU string = 'Standard_LRS'
param location string

var uniqueStorageName = '${storagePrefix}${uniqueString(resourceGroup().id)}'

resource stg 'Microsoft.Storage/storageAccounts@2019-04-01' = {
  name: uniqueStorageName
  location: location
  sku: {
    name: storageSKU
  kind: 'StorageV2'
  properties: {
    supportsHttpsTrafficOnly: true

output storageAccountId string =
ouptut storageAccountName string =
output storageEndpoint object =

As mentioned in part 1, an essential thing for creating modules is to ensure you are adding value, not just wrapping already existing resources. This is a relatively simple example, and you would probably expand it some more in actual use, but here the value is that we are defining some key values that ensure any storage account deployed complies with our policy:

  • Only allow specific SKU sizes - Standard, not premium and only LRS, GRS, RAGRS and ZRS
  • Enforce HTTPS traffic only

We’ll save this in a file called StorageModule. Bicep (you can call it whatever you want).

Consuming Bicep Modules

Consuming modules in Bicep is where we see some new concepts. Modules are a new top-level resource in Bicep. Consuming the above module would look like this:

module stgModule '../StorageModule.bicep' = {
  name: 'storageDeploy'
  params: {
    storagePrefix: 'scStg'
    storageSKU: 'Standard_LRS'
    location: 'WestEurope'    

Let’s break this down:

  • module - this is the type, indicating that it is a module
  • stgModule - symbolic name for the instance of the module in your code, can be anything
  • ‘../StorageModule.bicep’ path to the module file created above. This can be either a path to the local file on your machine or a location in a registry (we’ll cover registries below)
  • name - name for the instance of the module, this becomes the name of the nested template in ARM
  • params - list of parameter values to be provided to the module

Outputs are available on the created module object on the outputs property, so to get the ID of the deployed storage account, we would do:


We can use that to pass into other modules, resources or pass as its output from our top-level script.

Distributing Modules

For a module to be successful, you need to make it easy to obtain and consume. There are a few ways you can do this with Bicep. As we saw above, you can reference the file using a local path on your machine, so you can pass the file around using email, file share etc. That would work but would be pretty painful, especially when it comes to updates and versioning.

You could store your Bicep modules in a Git repo and have users clone this repo to their local machine and then reference that locally. I’d recommend keeping your modules in a source control repo anyway to aid your development process.

However, the most straightforward approach, at least for your end-users, is going to be using a Bicep registry. Bicep registries are built on top of Azure Container registries (ACR) and allow you to publish Bicep modules using OCI artefacts. The registry will enable you to publish your modules into ACR and then have your end-users retrieve them directly from there. There is no need to send modules to users or check out a Git repo; all they need is permissions to read the ACR and the URL to use.

Note that Bicep Registries are currently in preview

Because the registry is built on ACR, you get all the features of ACR, such as Private Endpoints, IAM permissions, global replication etc.

Publish a Module

To publish a module to the registry, you use the Azure CLI Bicep Publish command:

az bicep publish StorageModule.bicep --target br:<registry-name>

the target value defines a few things:

  • br - This is the schema name for bicep registry files
  • registry URL - full URL for your container registry
  • StorageModule - the name of the module in the registry
  • :1.0 version number of module

Having a version number means you can increment this each time you publish a new version, and the end-user can choose which version to use.

Once you run this command, your module is available to anyone with read access to ACR.

Consume a Module

To consume a module from a registry, we swap out the local path we used above for a registry URL. This is the same URL we used for the publish operation.

module stgModule 'br:<registry-name>' = {
  name: 'storageDeploy'
  params: {
    storagePrefix: 'scStg'
    storageSKU: 'Standard_LRS'
    location: 'WestEurope'    

This will now pull the module from the registry at the time it is compiled into ARM. If you want to upgrade to a newer version, you would change the version number from 1.0 to 1.1 and then re-compile and deploy.


Creating modules in Bicep is pretty straightforward and much easier than doing it in ARM. The recent addition of the Bicep Registry makes it even easier to distribute your modules to users and make it easy to get started.

Having a reusable library of Bicep content can help you get more people using IaC, apply standards throughout your organisation and spread knowledge better.