Learn Modules In Azure Bicep - Basics To Advanced, How It Works, Nested Modules, Outputs, Scopes

In my opinion, modules in Azure Bicep is one of the most exciting and requested features for ARM. It provides an easy and concise way to modularize your ARM templates, and in conjunction with Bicep syntax and VS Code extension, the process of creating Bicep modules becomes quite fast and enjoyable.

Module is a functionality in Azure Bicep which allows to split a complex template into smaller components. Each module contains one or more resources to be deployed together and can be reused across multiple templates.

Even though ARM templates already have such features as linked and nested templates as well as template specs, Bicep modules are nevertheless useful because they are being processed on the client’s and not on Azure Resource Manager’s side. Let’s see what it means.

Contents:

Overview

We begin this post with a short discussion about “When To Use Modules?”, there you can find some thoughts on whether modules are worth using in your template.

Next, Bicep Modules 101 section shows how to create and consume a very simple module which consists of a storage account deployment. Later, we will be using the module from this example in the following sections.

In my opinion, it is always useful to have some understanding of how things work. That’s why “How Bicep Modules Work?” demonstrates a transpiled ARM template from the previous section and highlights some important aspects.

Deployment Scopes are a very important part of the module functionality, we discuss how to deploy modules at different scopes and illustrate it with a simple example.

The rest of the post covers other slightly more advanced topics such as Module Outputs, Loops, Conditional Deployment, and Nested Modules which you might need if you work with modules more closely.

NOTE: Related Posts section contains many links to other posts about Azure Bicep in case you are interested in learning more.

When To Use Modules?

Most likely, if you are reading this post, then you have already decided that you need Bicep modules. But in any case, let’s briefly discuss why it is worth using this Bicep feature.

In general, Bicep language is the future of ARM templates in Azure. If you are using ARM templates to create and manage your resources in Azure, then you should definitely consider Azure Bicep.

When talking about Bicep modules specifically, here are my main reasons why one should use modules:

Bicep Modules 101

To start with, let’s look at a simple example of creating and consuming a module. This will illustrate the main idea what modules are about, and subsequent sections will cover more advanced topics.

As in other posts about Bicep, we are going to use a simple storage account deployment as an example. Storage resource is a good choice because it is known by almost anyone working with Azure and its template is quite succinct.

Creating Module

Defining a module is extremely simple and convenient, we just need to create a regular Bicep file. The code snippet below is all we need to define a module we will be using throughout the post.

NOTES:

// ========== storage.bicep ==========

// targetScope = 'resourceGroup'  -  default value

param storageAccountName string
param location string = resourceGroup().location

resource stg 'Microsoft.Storage/storageAccounts@2021-04-01' = {
  name: storageAccountName
  location: location
  sku: {
    name: 'Standard_LRS'
  }
  kind: 'StorageV2'
}

Consuming Module

From a consumer’s perspective, any Bicep file is potentially a module. Below is an example of how to use the module we defined in the previous section.

Often, we would need to deal with more advanced cases of modules, those are covered in other sections of the post, see overview for details.

NOTES:

// ========== main.bicep ==========

module stg './storage.bicep' = {
  name: 'myStorageDeployment'
  params: {
    storageAccountName: 'stcontoso'
  }
}

How Bicep Modules Work?

Great, now we know how to create and use a simple module in Bicep. But how does Bicep handle it and what is the resulting ARM template? These are good questions worth discussing.

The main point is that Bicep module gets compiled into a nested deployment resource with type Microsoft.Resources/deployments and Incremental deployment mode.

The ARM template below is the result of building main.bicep file from the previous section Bicep Modules 101. As a result, we get a template with Microsoft.Resources/deployments resource. Here are a few notes about it:

So, as a result, the main Bicep file along and all its module files are transpiled into one ARM template. This means that the final ARM template is subject to the template limits.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
{
  "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
  "contentVersion": "1.0.0.0",
  "functions": [],
  "resources": [
    {
      "type": "Microsoft.Resources/deployments",
      "apiVersion": "2019-10-01",
      "name": "myStorageDeployment",
      "properties": {
        "expressionEvaluationOptions": {
          "scope": "inner"
        },
        "mode": "Incremental",
        "parameters": {
          "storageAccountName": {
            "value": "stcontoso"
          }
        },
        "template": {
          "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
          "contentVersion": "1.0.0.0",
          "parameters": {
            "storageAccountName": {
              "type": "string"
            },
            "location": {
              "type": "string",
              "defaultValue": "[resourceGroup().location]"
            }
          },
          "functions": [],
          "resources": [
            {
              "type": "Microsoft.Storage/storageAccounts",
              "apiVersion": "2021-04-01",
              "name": "[parameters('storageAccountName')]",
              "location": "[parameters('location')]",
              "sku": {
                "name": "Standard_LRS"
              },
              "kind": "StorageV2"
            }
          ]
        }
      }
    }
  ]
}

Module Outputs

Each Bicep file can have outputs which are optional and available after the template is deployed. In case with modules, a consumer template can retrieve and use these values in other parts of the file.

Retrieving an output value is easily achieved by using a symbolic name of the module as shown in the code snippet below. Moreover, VS Code extension provides nice autocomplete for properties of outputs.

The format for getting an output value is the following: <symbolic_name>.outputs.<output_name>.

// ========== main.bicep ==========

module stg './storage.bicep' = {
  name: 'myStorageDeployment'
  params: {
    storageAccountName: 'stcontoso'
  }
}

// Getting module output
output x string = stg.outputs.primaryBlobEndpoint

Our storage.bicep from Creating Module section only needs a minor modification - we just add an output statement at the end of the file which is then consumed from the main.bicep.

// ========== storage.bicep ==========

// resource stg ...
// ...

output primaryBlobEndpoint string = stg.properties.primaryEndpoints.blob

Deployment Scopes

Each deployment in ARM is performed at a particular scope, this determines where resources are created and which context is used for the template validation.

In Bicep, a template file can have targetScope property set to one of these values (multiple target scopes are not supported as of July 2021): resourceGroup, subscription, managementGroup, and tenant. Note that resourceGroup is the default value.

When consuming a module, the definition has an optional scope property which allows specifying the scope of the module deployment. If no scope is provided, then by default the scope of the main template is assumed.

NOTE: Sometimes there is a need to deploy multiple resources at different scopes all during one ARM deployment. In such cases, Bicep modules are extremely helpful since they allow to deploy each module at a separate scope.

In Bicep Modules 101 we saw an example where both main and module templates have the same resourceGroup target scope. Now, let’s take a look at a different example which can give you an idea how to use Bicep modules with different deployment scopes.

Also, one more example is available in Nested Modules section where three target scopes are used within one deployment (managementGroup > subscription > resourceGroup).

When working with modules and different deployment scopes, you might find Bicep scope functions useful.

Target Scopes “subscription” (main) and “resourceGroup” (module)

Here, we are going to have main.bicep file with subscription target scope. At the same time, storage.bicep file from Creating Module remains unchanged.

In the following example, we first create a resource group and then deploy a module at this resource group.

NOTES:

Alternatively, if our resource group already existed, then we could specify module scope using resourceGroup('rg-bicep') function (line X) or existing keyword as described in Reference New Or Existing Resource In Azure Bicep post.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// ========== main.bicep ==========

targetScope = 'subscription'

resource rg 'Microsoft.Resources/resourceGroups@2021-04-01' = {
  name: 'rg-bicep'
  location: 'westus'
}

module stg './storage.bicep' = {
  name: 'myStorageDeployment'
  params: {
    storageAccountName: 'stcontoso'
  }
  scope: rg
}

Loops

As with any other resource, it is sometimes needed to deploy multiple instances of a module with similar properties. To make this simple and avoid code duplication, it is better to use for-expression.

Using a for loop to create multiple deployments of a module is similar to doing that for a resource. Learn more about loops in Bicep.

For simplicity, the example below uses range function, but this could be any other array as well, which comes from a parameter or a variable.

NOTE: In our example, the module contains only one resource which doesn’t make much sense since we could just create multiple storage accounts using a loop. However, in cases when modules are more complicated or we need to deploy at different deployment scopes, modules might be necessary. Here’s an example.

// ========== main.bicep ==========

// Creating 5 module deployments using a loop
module stgs './storage.bicep' = [for i in range(0, 5): {
  name: 'myStorageDeployment-${i}'
  params: {
    storageAccountName: 'stcontoso${i}'
  }
}]

// stgs is now an array of modules, use index to access a particular value

Conditional Deployment

Some resources can be optionally deployed based on a condition which is calculated at the start of the deployment, note that this condition has to be evaluated before the deployment begins.

Similarly, one might want to deploy a module only if the some condition evaluates to true. In the example below, it is as simple as looking at the value of parameter.

Also, conditional deployment for modules may be combined with looping constructs and other features discussed in this post.

// ========== main.bicep ==========

param shouldDeploy bool

module stg './storage.bicep' = if(shouldDeploy) {
  name: 'myStorageDeployment'
  params: {
    storageAccountName: 'stcontoso'
  }
}

Nested Modules

If any Bicep file can be consumed as a module and also can consume other modules, then it means that we can nest modules if needed.

The example below features three files:

So, our main file deploys a module which in turn deploys another module. When compiled, the final ARM template looks quite bulky since everything is put into one JSON file. Undoubtedly, Bicep modules are much more easier to create and understand.

// ========== main.bicep ==========

targetScope = 'managementGroup'

module storageRg './resource-group.bicep' = {
  name: 'myStorageResourceGroupDeployment'
  params: {
    resourceGroupName: 'rg-bicep'
    location: 'westus'
  }
  scope: subscription('a3fe6943-836d-4d42-937d-082bc218a7ea')
}
// ========== resource-group.bicep ==========

targetScope = 'subscription'

param resourceGroupName string
param location string

resource rg 'Microsoft.Resources/resourceGroups@2021-04-01' = {
  name: resourceGroupName
  location: location
}

module stg './storage.bicep' = {
  name: 'myStorageDeployment'
  params: {
    storageAccountName: 'stcontoso'
    location: location
  }
  scope: rg
}