Problem Context
ARM JSON templates are notoriously verbose. A simple storage account with a private endpoint and a diagnostic setting can easily reach 300 lines of JSON. Every nested resource requires full parent path repetition. Referencing another resource means constructing resourceId()strings manually. Bugs hide inside string templates that the editor can't type-check.
Bicepis Azure's domain-specific language that compiles to ARM JSON. It eliminates 60โ70% of the boilerplate, adds compile-time type checking, and enables module composition that scales to large, multi-team infrastructure repositories.
- Your ARM templates are 500+ lines and nobody wants to touch them
- You're copy-pasting the same network module across 5 repos with minor variations
- A typo in a
resourceId()string caused a 3am incident - You want reusable, versioned infrastructure components your team can trust
Bicep modules give you composable infrastructure with the safety of a typed language.
Bicep Basics
The same storage account from ARM templates in Bicep:
param storageAccountName string
param sku string = 'Standard_LRS'
var location = resourceGroup().location
resource storageAccount 'Microsoft.Storage/storageAccounts@2023-01-01' = {
name: storageAccountName
location: location
sku: { name: sku }
kind: 'StorageV2'
properties: {
minimumTlsVersion: 'TLS1_2'
allowBlobPublicAccess: false
supportsHttpsTrafficOnly: true
}
}
output blobEndpoint string = storageAccount.properties.primaryEndpoints.blobNo $schema, no contentVersion, no [parameters()] functions โ just declaration. The compiler handles the ARM translation. The VS Code Bicep extension gives you IntelliSense for every property, type checking on assignments, and navigation to resource documentation.
Modules: The Core Abstraction
A Bicep module is just a .bicepfile that you reference from another file. There's no special module keyword โ any Bicep file can be a module.
// modules/storage.bicep
@description('Storage account name โ must be globally unique')
@minLength(3)
@maxLength(24)
param name string
@allowed(['Standard_LRS', 'Standard_GRS', 'Premium_LRS'])
param sku string = 'Standard_LRS'
param location string = resourceGroup().location
resource sa 'Microsoft.Storage/storageAccounts@2023-01-01' = {
name: name
location: location
sku: { name: sku }
kind: 'StorageV2'
properties: {
minimumTlsVersion: 'TLS1_2'
allowBlobPublicAccess: false
supportsHttpsTrafficOnly: true
}
}
output id string = sa.id
output blobEndpoint string = sa.properties.primaryEndpoints.blob// main.bicep โ consuming the module
param environment string = 'dev'
module storage './modules/storage.bicep' = {
name: 'storageDeployment'
params: {
name: 'stapp${environment}westus001'
sku: environment == 'prod' ? 'Standard_GRS' : 'Standard_LRS'
}
}
// Reference module output downstream
output storageEndpoint string = storage.outputs.blobEndpointModule Sources: Local, Registry, and Template Specs
Bicep modules can come from three sources:
Local files
module network './modules/network.bicep' = { ... }Azure Container Registry (Bicep Registry)
// Push module to registry
az bicep publish --file modules/storage.bicep \
--target br:myregistry.azurecr.io/bicep/storage:v1.2
// Consume from registry
module storage 'br:myregistry.azurecr.io/bicep/storage:v1.2' = {
name: 'storageDeployment'
params: { name: 'st-prod-001', sku: 'Standard_GRS' }
}Public Bicep Registry (Microsoft-maintained)
// Use Microsoft's curated modules
module keyVault 'br/public:avm/res/key-vault/vault:0.11.0' = {
name: 'kvDeployment'
params: {
name: 'kv-${environment}-001'
location: location
enableRbacAuthorization: true
}
}The Azure Verified Modules (AVM) library is the recommended starting point for common Azure resources. These modules are maintained by Microsoft, follow security best practices, and are versioned.
Conditions and Loops
// Conditional resource โ only deploy in production
resource diagnostics 'Microsoft.Insights/diagnosticSettings@2021-05-01-preview' = if (environment == 'prod') {
name: 'diag-settings'
scope: storageAccount
properties: {
workspaceId: logAnalyticsWorkspaceId
logs: [{ category: 'StorageRead', enabled: true }]
metrics: [{ category: 'Transaction', enabled: true }]
}
}
// Loop โ create multiple identical resources
param regions array = ['westus', 'eastus', 'westeurope']
resource storageAccounts 'Microsoft.Storage/storageAccounts@2023-01-01' = [for region in regions: {
name: 'stapp${environment}${region}001'
location: region
sku: { name: 'Standard_GRS' }
kind: 'StorageV2'
properties: { minimumTlsVersion: 'TLS1_2', allowBlobPublicAccess: false }
}]bicepconfig.json โ Project Configuration
Place a bicepconfig.json at the root of your project to configure the Bicep compiler, enable linting rules, and set registry aliases:
{
"moduleAliases": {
"br": {
"modules": {
"registry": "myregistry.azurecr.io",
"modulePath": "bicep"
}
}
},
"analyzers": {
"core": {
"enabled": true,
"rules": {
"no-hardcoded-location": { "level": "error" },
"secure-secrets-in-params": { "level": "error" },
"use-recent-api-versions": { "level": "warning" }
}
}
}
}Repository Structure for Bicep at Scale
infra/
โโโ main.bicep # Entry point per environment
โโโ main.dev.bicepparam # Dev parameter values
โโโ main.prod.bicepparam # Prod parameter values
โโโ modules/
โ โโโ network/
โ โ โโโ vnet.bicep
โ โ โโโ private-endpoint.bicep
โ โโโ compute/
โ โ โโโ app-service.bicep
โ โโโ storage/
โ โโโ storage-account.bicep
โโโ bicepconfig.jsonBicep parameter files (.bicepparam) are the modern replacement for ARM .parameters.json. They support expressions, reference Key Vault secrets directly, and provide type checking:
// main.prod.bicepparam
using './main.bicep'
param environment = 'prod'
param storageAccountName = 'stprod001'
param sqlAdminPassword = getSecret('sub-id', 'rg-prod', 'kv-prod', 'sql-admin-password')Production Checklist
- Use
@description()decorators on all params โ they surface in portal deployments - Use
@secure()for passwords; never output secrets - Enable all linting rules in
bicepconfig.json; fail CI onerror-level violations - Pin module versions from ACR โ don't reference
:latest - Run
az deployment group what-ifin CI before merging infra PRs - Store
.bicepparamfiles per environment; never hard-code environment values in.bicep - Consider Azure Verified Modules before writing custom modules for common resources

