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>
  }
}
PartPurposeExample
Symbolic NameReference this module elsewhere in main.bicepstorageModule
Path to FileRelative path to the module Bicep file'./modules/storage.bicep'
nameDeployment name visible in Azure Portal history'deploy-storage'
paramsValues passed into the module's parameterslocation: 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

PracticeWhy It Matters
One resource type per moduleKeeps modules focused and reusable
Always define outputsAllows the calling file to use created resources
Use @description on all paramsDocuments the module for other team members
Keep modules in a /modules folderConsistent structure across all projects
Version modules in the registryPrevents 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.

Leave a Comment