Stage 4 · Provision
Bicep & ARM Templates for Azure
Bicep Modules & Registries
Private registries, versioning, and module composition — building reusable Azure infrastructure components.
Module Basics
Bicep modules are .bicep files that encapsulate related resources. They accept parameters and return outputs. Modules are the primary mechanism for reuse and abstraction in Bicep.
@description('Storage account name')
param storageAccountName string
@description('Azure region')
param location string = resourceGroup().location
@description('SKU name')
@allowed(['Standard_LRS', 'Standard_GRS', 'Standard_ZRS'])
param skuName string = 'Standard_LRS'
resource storageAccount 'Microsoft.Storage/storageAccounts@2023-01-01' = {
name: storageAccountName
location: location
sku: {
name: skuName
}
kind: 'StorageV2'
properties: {
supportsHttpsTrafficOnly: true
minimumTlsVersion: 'TLS1_2'
}
}
output storageAccountId string = storageAccount.id
output primaryEndpoint string = storageAccount.properties.primaryEndpoints.blobThe module file defines parameters, resources, and outputs. The output keyword exposes values to the calling module. Outputs are the interface between modules — they allow data to flow upward.
Module Outputs
Module outputs are how modules communicate with their callers. They expose resource IDs, connection strings, names, and any other value that callers need. Outputs are stored in the deployment history.
param location string
param environment string
module storage 'modules/storage.bicep' = {
name: 'storageDeploy'
params: {
storageAccountName: 'stg${environment}${uniqueString(resourceGroup().id)}'
location: location
}
}
module keyvault 'modules/keyvault.bicep' = {
name: 'keyvaultDeploy'
params: {
vaultName: 'kv-${environment}'
location: location
storageAccountId: storage.outputs.storageAccountId
}
}The module call returns outputs accessible via module.name.outputs.outputName. The storage module's output is passed as a parameter to the keyvault module. This creates an implicit dependency — keyvault deploys after storage.
Azure Container Registry
Azure Container Registry (ACR) hosts Bicep modules as OCI artifacts. The br: prefix references modules from a private registry. This is the official way to share modules across teams in Azure.
module storage 'br:myregistry.azurecr.io/bicep/modules/storage:v1.0.0' = {
name: 'storageDeploy'
params: {
storageAccountName: 'myStorage'
location: location
}
}
module storage 'br:myregistry.azurecr.io/bicep/modules/storage:latest' = {
name: 'storageDeploy'
params: {
storageAccountName: 'myStorage'
location: location
}
}The br: prefix tells Bicep to fetch the module from an OCI registry. The version is specified after the colon. Use specific versions (v1.0.0) for production and latest for development.
$ az acr build --registry myregistry --image bicep/modules/storage:v1.0.0 .
# Or publish directly from a Bicep file
$ az bicep publish --file modules/storage.bicep --target br:myregistry.azurecr.io/bicep/modules/storage:v1.0.0The publish command uploads a Bicep file to an OCI registry. The target path must follow the br:registry/path:version format. Tags can be updated to point to newer versions.
Module Versioning
Bicep modules in registries use semantic versioning. Pinning to specific versions ensures reproducible deployments. Updating versions is a deliberate action — not automatic.
| Version Format | Behavior | Use Case |
|---|---|---|
| :v1.0.0 | Exact version, never changes | Production deployments |
| :v1.0 | Latest patch (1.0.x) | Accept patch updates |
| :v1 | Latest minor and patch | Accept feature updates |
| :latest | Always pulls newest | Development only |
Module Composition
Modules compose higher-level abstractions from lower-level modules. A networking module might contain submodules for VNet, subnets, and NSGs. This layering keeps individual modules focused and composable.
// modules/networking.bicep
param vnetName string
param addressPrefix string
param subnets array
param location string
module vnet 'virtual-network.bicep' = {
name: vnetName
params: {
vnetName: vnetName
addressPrefix: addressPrefix
location: location
}
}
module subnetDeploy 'subnet.bicep' = [for subnet in subnets: {
name: 'subnet-${subnet.name}'
params: {
subnetName: subnet.name
addressPrefix: subnet.cidr
vnetName: vnet.outputs.vnetName
}
}]
output vnetId string = vnet.outputs.vnetId
output subnetIds array = [for (subnet, i) in subnets: subnetDeploy[i].outputs.subnetId]The networking module composes a VNet and subnets using child modules. The loop creates multiple subnet instances. Outputs aggregate results from loop-created modules into arrays.
Testing Modules
Bicep modules can be validated and tested using what-if operations and the Bicep test framework. The what-if command shows what would change without making changes.
$ az deployment group what-if \
--resource-group myRG \
--template-file main.bicep \
--parameters environment=dev
# Build to ARM and validate
$ az bicep build --file main.bicep
$ az deployment group validate \
--resource-group myRG \
--template-file main.jsonThe what-if command previews changes without applying them. The build and validate commands catch syntax errors before deployment. Both are essential for CI pipelines.
During development, reference modules with relative paths ('modules/storage.bicep'). Switch to br: references for production. This avoids publishing every change during iteration.
Modules in ACR require authentication. Ensure your deployment identity has AcrPull permissions. Service connections in Azure DevOps or managed identities in GitHub Actions handle this automatically.
Mark this lesson complete to store local progress and unlock a cleaner resume path the next time you visit.