Bicep Modules
Modules are separate Bicep files that a main template calls like a function. Instead of putting all resource declarations in one giant file, modules split the infrastructure into focused, self-contained pieces — one module for networking, one for compute, one for storage. Each module does one job well and is reusable across projects.
Think of modules like LEGO bricks. Each brick is a self-contained piece with a defined shape and connection points. Complex structures come from assembling multiple bricks, not from carving a single large block. Bicep modules are the LEGO bricks of infrastructure code.
Why Use Modules?
- Separation of concerns – Networking code lives in network.bicep, compute in compute.bicep. Each file is focused and readable.
- Reusability – A storage module written once deploys to dev, staging, and production with different parameters.
- Team collaboration – Different engineers own different modules without stepping on each other's work.
- Testability – Small modules are easier to validate and test than one enormous template.
- Reduced duplication – The same module handles the same resource type for every project.
Module Architecture – How Files Connect
Project Folder Structure
project/
│
├── main.bicep ← Entry point, calls modules
│
└── modules/
├── storage.bicep ← Storage Account module
├── appservice.bicep ← App Service module
├── network.bicep ← Virtual Network module
└── keyvault.bicep ← Key Vault module
Call Flow Diagram
main.bicep
│
├──► module storageModule = './modules/storage.bicep'
│ ├── Params in → location, storageName, sku
│ └── Outputs ← storageId, blobEndpoint
│
├──► module networkModule = './modules/network.bicep'
│ ├── Params in → location, vnetName
│ └── Outputs ← vnetId, subnetId
│
└──► module appModule = './modules/appservice.bicep'
├── Params in → location, appName, subnetId
└── Outputs ← webAppUrl
How to Call a Module – Module Syntax
module <symbolicName> '<pathToFile>' = {
name: '<deploymentName>'
params: {
<paramName>: <value>
}
}
| Part | Purpose | Example |
|---|---|---|
| Symbolic Name | Reference this module elsewhere in main.bicep | storageModule |
| Path to File | Relative path to the module Bicep file | './modules/storage.bicep' |
| name | Deployment name visible in Azure Portal history | 'deploy-storage' |
| params | Values passed into the module's parameters | location: location |
Step-by-Step Example – Storage Module
Step 1 – Write the Module File (modules/storage.bicep)
// modules/storage.bicep
@description('Azure region for the storage account.')
param location string
@description('Name for the storage account.')
@minLength(3)
@maxLength(24)
param storageAccountName string
@description('Storage redundancy SKU.')
@allowed(['Standard_LRS', 'Standard_GRS', 'Standard_ZRS'])
param storageSku string = 'Standard_LRS'
@description('Tags to apply to the storage account.')
param tags object = {}
resource storageAccount 'Microsoft.Storage/storageAccounts@2023-01-01' = {
name: storageAccountName
location: location
tags: tags
sku: {
name: storageSku
}
kind: 'StorageV2'
properties: {
supportsHttpsTrafficOnly: true
minimumTlsVersion: 'TLS1_2'
}
}
// Outputs the calling file can read
output storageAccountId string = storageAccount.id
output storageAccountName string = storageAccount.name
output blobEndpoint string = storageAccount.properties.primaryEndpoints.blob
Step 2 – Call the Module from main.bicep
// main.bicep
param location string = 'eastus'
param environment string = 'prod'
var commonTags = {
environment: environment
managedBy: 'Bicep'
}
var uniqueSuffix = take(uniqueString(resourceGroup().id), 6)
// Call the storage module
module storageModule './modules/storage.bicep' = {
name: 'deploy-storage'
params: {
location: location
storageAccountName: 'st${environment}${uniqueSuffix}'
storageSku: environment == 'prod' ? 'Standard_GRS' : 'Standard_LRS'
tags: commonTags
}
}
// Read the module output
output storageId string = storageModule.outputs.storageAccountId
output blobUrl string = storageModule.outputs.blobEndpoint
Reading Module Outputs
Access module outputs using <symbolicName>.outputs.<outputName>.
// After calling a module, read its outputs like this:
var storageId = storageModule.outputs.storageAccountId
var blobUrl = storageModule.outputs.blobEndpoint
// Pass a module output into another module as a parameter
module appModule './modules/appservice.bicep' = {
name: 'deploy-app'
params: {
location: location
appName: 'mywebapp'
storageConnectionString: storageModule.outputs.blobEndpoint // ← module output passed here
}
}
Complete Multi-Module Example
modules/network.bicep
param location string
param vnetName string
param vnetAddressPrefix string = '10.0.0.0/16'
param subnetName string = 'app-subnet'
param subnetAddressPrefix string = '10.0.1.0/24'
resource vnet 'Microsoft.Network/virtualNetworks@2023-09-01' = {
name: vnetName
location: location
properties: {
addressSpace: {
addressPrefixes: [vnetAddressPrefix]
}
subnets: [
{
name: subnetName
properties: {
addressPrefix: subnetAddressPrefix
}
}
]
}
}
output vnetId string = vnet.id
output subnetId string = vnet.properties.subnets[0].id
modules/appservice.bicep
param location string
param appName string
param environment string = 'dev'
var sku = environment == 'prod' ? 'P1v3' : 'B1'
resource appServicePlan 'Microsoft.Web/serverfarms@2023-01-01' = {
name: 'asp-${appName}-${environment}'
location: location
sku: {
name: sku
}
}
resource webApp 'Microsoft.Web/sites@2023-01-01' = {
name: 'app-${appName}-${environment}'
location: location
kind: 'app'
properties: {
serverFarmId: appServicePlan.id
}
}
output webAppId string = webApp.id
output webAppHostname string = webApp.properties.defaultHostName
output webAppName string = webApp.name
main.bicep – Orchestrating All Modules
param location string = 'eastus'
param appName string = 'ecommerce'
param environment string = 'prod'
var tags = {
application: appName
environment: environment
managedBy: 'Bicep'
}
// Deploy network first
module networkModule './modules/network.bicep' = {
name: 'deploy-network'
params: {
location: location
vnetName: 'vnet-${appName}-${environment}'
}
}
// Deploy storage (independent of network)
module storageModule './modules/storage.bicep' = {
name: 'deploy-storage'
params: {
location: location
storageAccountName: 'st${appName}${environment}001'
tags: tags
}
}
// Deploy app (depends on network and storage via params)
module appModule './modules/appservice.bicep' = {
name: 'deploy-app'
params: {
location: location
appName: appName
environment: environment
}
dependsOn: [networkModule] // wait for network before deploying app
}
// Collect outputs from all modules
output networkVnetId string = networkModule.outputs.vnetId
output storageEndpoint string = storageModule.outputs.blobEndpoint
output applicationUrl string = 'https://${appModule.outputs.webAppHostname}'
Looping Over Modules
Modules combine with loops to deploy the same module multiple times with different inputs.
var environments = ['dev', 'staging', 'prod']
module storageModules './modules/storage.bicep' = [for env in environments: {
name: 'deploy-storage-${env}'
params: {
location: 'eastus'
storageAccountName: 'storage${env}001'
storageSku: env == 'prod' ? 'Standard_GRS' : 'Standard_LRS'
}
}]
// Output all storage IDs as an array
output allStorageIds array = [for i in range(0, length(environments)): storageModules[i].outputs.storageAccountId]
Bicep Registry – Sharing Modules Across Teams
Beyond local file paths, modules load from Azure Container Registry (ACR). Teams publish their modules to a registry and share them organization-wide.
// Call a module stored in Azure Container Registry
module storageModule 'br:myregistry.azurecr.io/bicep/modules/storage:v1.0' = {
name: 'deploy-storage'
params: {
location: location
storageAccountName: 'mystorage001'
}
}
Microsoft also publishes a public Bicep Registry with ready-to-use modules for common Azure services at br/public:avm (Azure Verified Modules).
// Use Microsoft's Azure Verified Modules from the public registry
module storageModule 'br/public:avm/res/storage/storage-account:0.11.0' = {
name: 'deploy-storage'
params: {
name: 'mystorage001'
location: location
}
}
Module Best Practices
| Practice | Why It Matters |
|---|---|
| One resource type per module | Keeps modules focused and reusable |
| Always define outputs | Allows the calling file to use created resources |
| Use @description on all params | Documents the module for other team members |
| Keep modules in a /modules folder | Consistent structure across all projects |
| Version modules in the registry | Prevents breaking changes from affecting existing deployments |
Summary
Bicep modules split large templates into focused, reusable files. A module accepts parameters, deploys resources, and returns outputs. The calling file passes parameters into the module and reads outputs using symbolicName.outputs.outputName. Modules combine with loops for multi-environment deployments. Azure Container Registry enables organization-wide module sharing, and Microsoft's public registry provides production-ready modules for common Azure services.
