Azure, Professional

Walkthrough of Working with Bicep Modules

Update: Check out my Cloud Lunch and Learn Marathon presentation w/ Bicep 0.3 for newer information


Version 0.2.3 of Azure Bicep introduced the ability to decompose .bicep files into separate modules. Paired with this update is the functionality to set the scope of each modular deployment. This translates into the ability to scope individual resource deployments into geo specific resources groups. We now can call the same module and pass in the region specific parameters AND set the scope for the resource group deployment.

There is some bicep documentation on how to create modules. This documentation uses the example of a storage account; however, what I decided to do was use the same bicep example I committed in my previous post “Translating An Azure Quickstart to bicep” and the example hosted in the bicep playground

This specific ARM equivalent is the 201-Web-App-Loganalytics quickstart What this ARM template creates is a:

  • App Service Plan
  • App Service
  • Application Insights
  • Log Analytics Workspace


These components are a good starting point on how to divide up our bicep file into modules. With this breakdown our bicep structure will be:

  • app-insight.bicep
  • app-service-plan.bicep
  • app-service.bicep
  • log-analytics.bicep
  • main.bicep

The main.bicep will be the driver that will call each module to generate the ARM template. The one thing we need to consider when mapping out is the dependencies each module will need or depend on. Why this is important is due to the fact we are breaking out our components into separate modules and they need to be aware of the existence of any dependencies. If a module has a dependency then the dependent object must define the required output. In English this means things like our App Service will need to know the App Service Plan ID to attach to, our App Service needs to know the Instrumentation Key of the App Insights instance, etc….


Logically speaking the first item that should be deployed is the App Service Plan so a call this module would look like:

module appServicePlanModule './app-service-plan.bicep' = {
  name: 'appServicePlanDeploy'
  params: {
    appName: appName

Alright, App Service Plan has been created so the next step might be to include the App Service

module appServiceModule './app-service.bicep' = {
  name: 'appServiceDeploy'
  params: {
    appName: appName
    appServicePlanID: appServicePlanModule.outputs.appServicePlanID

Take note that there are parameters being passed into the module. Which makes sense when considering the App Service will need to know which App Service Plan to attach to and which Application Insights to hook into. Let’s focus on the App Service Plan requirement at the moment.

Notice the ‘appServicePlanID’ is being passed in ‘appServicePlanModule.outputs.appServicePlanID’ The appServicePlanModule matches the name given to the preceding call to create the App Service Plan. ‘.outputs’ indicates that this is something that will be handed off between the specific module and the main.bicep file. the appServicePlanID is the name of the specified output. So without further ado here is what the app-service-plan.bicep looks like:

param skuName string = 'S1'
param skuCapacity int = 1
param location string = resourceGroup().location
param appName string
var appServicePlanName = toLower('asp-${appName}')
resource appServicePlan 'Microsoft.Web/serverfarms@2020-06-01' = {
  name: appServicePlanName // Globally unique storage account name
  location: location // Azure Region
  sku: {
    name: skuName
    capacity: skuCapacity
  tags: {
    displayName: 'HostingPlan'
    ProjectName: appName
output appServicePlanID string =

Note that the output is defined and it is defined within the scope of the module file. Additionally, there are parameters and variables similar to what would normally be defined in the main.bicep file. The difference being that if desired the parameters can be overwritten at the time the main.bicep file is calling the module. For sake of simplicity these have been defaulted.

The rest of the modules will follow this behavior and can be found in the bicep github repository.

Deployment Scopes

One topic that isn’t in there that is important for understanding is the ability to deploy modules at different scopes. This ability will allow for the same bicep file to potentially provide the ARM template to deploy identical resources to two different resource groups, one thought on why this is a good feature is take this template that deploys an App Service Plan, App Service, Application Insights, and Log Analytics Workspace one step further.

Sometimes there is a need for geo redundancy for applications. There is a misnomer out there that deploy PaaS resources in the same resource group but in different regions then it’s geo redundant. This is only partial true as if the hosting region of the resource group has a failure the resources in the other region will still be available but depending on the outage undeployable. That’s because resource groups contain meta data including the tracking of deployment information

Long story short to make an application truly geo redundant the need arises for an application to reside in two different resource groups hosted in two different regions. Well with the ability to set scope bicep provides the ability to re-use each module and pass in the appropriate parameters including location so this helps with reuse.

module stg './storage.bicep' = {
  name: 'storageDeploy'
  scope: resourceGroup('another-rg') // this will target another resource group in the same subscription
  params: {
    namePrefix: 'contoso'

output storageName string = stg.outputs.computedStorageName

Conclusion and Source Code

Talking through these examples are great! However with everything it’s even better to contribute back to the opensource community. As such if you have any questions or what to see the entire template can locate it at the Azure Bicep GitHub project.

As always free to add any comments or feedback on this post!