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.

๐Ÿค” Sound familiar?
  • 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.blob

No $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.blobEndpoint

Module 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.json

Bicep 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 on error-level violations
  • Pin module versions from ACR โ€” don't reference :latest
  • Run az deployment group what-if in CI before merging infra PRs
  • Store .bicepparam files per environment; never hard-code environment values in .bicep
  • Consider Azure Verified Modules before writing custom modules for common resources