How to use Azure Verified Modules (AVM) for Bicep?


Azure Azure Landing Zones Infrastructure-as-code Azure Bicep 💪


Table of contents:

Building infrastructure-as-code from scratch can be time-consuming and error-prone. In a previous article, I covered how to govern Azure resources with Template Specs. Today, I want to introduce you to Azure Verified Modules (AVM) — Microsoft’s official library of pre-built, tested, and supported Bicep (and Terraform) modules that follow best practices out of the box.

What are Azure Verified Modules? #

Azure Verified Modules (AVM) is an initiative by Microsoft to provide a single source of truth for Infrastructure-as-Code modules. These modules are:

The AVM initiative consolidates and supersedes previous efforts like CARML (Common Azure Resource Modules Library) and TFVM (Terraform Verified Modules).

Module Types #

AVM provides two types of modules:

Resource Modules #

Resource modules deploy a single Azure resource with all its extensions and child resources. For example, a Storage Account resource module would include:

Pattern Modules #

Pattern modules combine multiple resource modules to deploy a complete solution or architecture pattern. Examples include:

Getting Started with AVM #

Browsing Available Modules #

Visit the AVM Module Index to explore all available Bicep modules. Each module includes:

Using Modules from the Public Registry #

AVM modules are published to the Microsoft Public Bicep Registry. To use them, reference the module directly in your Bicep file:

1module storageAccount 'br/public:avm/res/storage/storage-account:0.14.0' = {
2  name: 'storageAccountDeployment'
3  params: {
4    name: 'stweekendsprints001'
5    location: location
6    skuName: 'Standard_LRS'
7    kind: 'StorageV2'
8  }
9}

The syntax br/public:avm/res/storage/storage-account:0.14.0 breaks down as:

Configuring bicepconfig.json #

To simplify module references, configure your bicepconfig.json:

 1{
 2  "moduleAliases": {
 3    "br": {
 4      "public": {
 5        "registry": "mcr.microsoft.com",
 6        "modulePath": "bicep"
 7      }
 8    }
 9  }
10}

Practical Example: Deploying a Storage Account with AVM #

Let’s deploy a production-ready storage account with private endpoint and diagnostic settings:

  1targetScope = 'resourceGroup'
  2
  3@description('Location for all resources')
  4param location string = resourceGroup().location
  5
  6@description('Environment name')
  7@allowed(['dev', 'test', 'prod'])
  8param environment string = 'dev'
  9
 10@description('Log Analytics Workspace Resource ID for diagnostics')
 11param logAnalyticsWorkspaceId string
 12
 13@description('Virtual Network Resource ID for private endpoint')
 14param virtualNetworkResourceId string
 15
 16@description('Subnet Resource ID for private endpoint')
 17param privateEndpointSubnetResourceId string
 18
 19var storageAccountName = 'st${environment}weekendsprints'
 20
 21module storageAccount 'br/public:avm/res/storage/storage-account:0.14.0' = {
 22  name: 'storageAccountDeployment'
 23  params: {
 24    name: storageAccountName
 25    location: location
 26    skuName: environment == 'prod' ? 'Standard_GRS' : 'Standard_LRS'
 27    kind: 'StorageV2'
 28    
 29    // Security settings
 30    allowBlobPublicAccess: false
 31    allowSharedKeyAccess: false
 32    minimumTlsVersion: 'TLS1_2'
 33    supportsHttpsTrafficOnly: true
 34    
 35    // Network settings
 36    publicNetworkAccess: 'Disabled'
 37    networkAcls: {
 38      defaultAction: 'Deny'
 39      bypass: 'AzureServices'
 40    }
 41    
 42    // Private endpoint
 43    privateEndpoints: [
 44      {
 45        subnetResourceId: privateEndpointSubnetResourceId
 46        service: 'blob'
 47        privateDnsZoneResourceIds: [
 48          privateDnsZone.outputs.resourceId
 49        ]
 50      }
 51    ]
 52    
 53    // Blob containers
 54    blobServices: {
 55      containers: [
 56        {
 57          name: 'data'
 58          publicAccess: 'None'
 59        }
 60        {
 61          name: 'logs'
 62          publicAccess: 'None'
 63        }
 64      ]
 65    }
 66    
 67    // Diagnostics
 68    diagnosticSettings: [
 69      {
 70        workspaceResourceId: logAnalyticsWorkspaceId
 71        metricCategories: [
 72          {
 73            category: 'AllMetrics'
 74          }
 75        ]
 76      }
 77    ]
 78    
 79    // Tags
 80    tags: {
 81      Environment: environment
 82      ManagedBy: 'Bicep-AVM'
 83    }
 84  }
 85}
 86
 87// Private DNS Zone for blob storage
 88module privateDnsZone 'br/public:avm/res/network/private-dns-zone:0.5.0' = {
 89  name: 'privateDnsZoneDeployment'
 90  params: {
 91    name: 'privatelink.blob.${az.environment().suffixes.storage}'
 92    location: 'global'
 93    virtualNetworkLinks: [
 94      {
 95        virtualNetworkResourceId: virtualNetworkResourceId
 96        registrationEnabled: false
 97      }
 98    ]
 99  }
100}
101
102output storageAccountResourceId string = storageAccount.outputs.resourceId
103output storageAccountName string = storageAccount.outputs.name
104output blobEndpoint string = storageAccount.outputs.primaryBlobEndpoint

Notice how much functionality we get with minimal code! The AVM module handles:

AVM vs Template Specs #

In my Template Specs article, I showed how to share templates within an organization. Here’s how AVM compares:

AspectTemplate SpecsAVM
ScopeOrganization-specificCommunity/Microsoft
MaintenanceYour teamMicrosoft + Community
CustomizationFull controlLimited to parameters
UpdatesManualSemantic versioning
Best practicesYour standardsMicrosoft standards

My recommendation: Use AVM modules as your foundation and wrap them with Template Specs when you need organization-specific customizations or governance.

Integrating AVM with CI/CD #

GitLab CI/CD #

Building on my GitLab CI/CD Components article, here’s how to validate and deploy AVM-based templates:

1include:
2  - component: $CI_SERVER_FQDN/gitlab-cicd-component/gitlab-cicd-component-azure-bicep/azure-bicep@0.0.1
3    inputs:
4      template_file: main.bicep
5      parameters_file: parameters.prod.json
6      resource_group: rg-weekendsprints-prod
7      location: westeurope

Azure DevOps #

 1trigger:
 2  branches:
 3    include:
 4      - main
 5  paths:
 6    include:
 7      - infra/**
 8
 9pool:
10  vmImage: 'ubuntu-latest'
11
12variables:
13  azureServiceConnection: 'WeekendSprints-SPN'
14  resourceGroupName: 'rg-weekendsprints-prod'
15  location: 'westeurope'
16
17stages:
18  - stage: Validate
19    jobs:
20      - job: ValidateBicep
21        steps:
22          - task: AzureCLI@2
23            displayName: 'Bicep Build & Lint'
24            inputs:
25              azureSubscription: $(azureServiceConnection)
26              scriptType: 'bash'
27              scriptLocation: 'inlineScript'
28              inlineScript: |
29                az bicep build --file infra/main.bicep
30                
31          - task: AzureCLI@2
32            displayName: 'What-If Analysis'
33            inputs:
34              azureSubscription: $(azureServiceConnection)
35              scriptType: 'bash'
36              scriptLocation: 'inlineScript'
37              inlineScript: |
38                az deployment group what-if \
39                  --resource-group $(resourceGroupName) \
40                  --template-file infra/main.bicep \
41                  --parameters infra/parameters.prod.json
42
43  - stage: Deploy
44    dependsOn: Validate
45    condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
46    jobs:
47      - deployment: DeployInfra
48        environment: 'production'
49        strategy:
50          runOnce:
51            deploy:
52              steps:
53                - checkout: self
54                - task: AzureCLI@2
55                  displayName: 'Deploy Bicep'
56                  inputs:
57                    azureSubscription: $(azureServiceConnection)
58                    scriptType: 'bash'
59                    scriptLocation: 'inlineScript'
60                    inlineScript: |
61                      az deployment group create \
62                        --resource-group $(resourceGroupName) \
63                        --template-file infra/main.bicep \
64                        --parameters infra/parameters.prod.json

Version Pinning Strategy #

AVM uses semantic versioning. Here’s my recommended approach:

 1// ✅ Good: Pin to specific version
 2module storage 'br/public:avm/res/storage/storage-account:0.14.0' = { }
 3
 4// ⚠️ Caution: Minor version range (may introduce new features)
 5module storage 'br/public:avm/res/storage/storage-account:0.14' = { }
 6
 7// ❌ Avoid: Major version only (breaking changes possible)
 8module storage 'br/public:avm/res/storage/storage-account:0' = { }
 9```$$
10
11For production workloads, always pin to specific versions and test updates in non-production environments first.
12
13## Contributing to AVM
14
15AVM is open source! You can contribute by:
16
171. **Reporting issues**: Found a bug? [Open an issue](https://aka.ms/AVM/BicepResourceModuleIssue)
182. **Suggesting features**: Have an idea? Start a discussion
193. **Contributing code**: Follow the [contribution guide](https://azure.github.io/Azure-Verified-Modules/contributing/bicep/)
20
21## Resources
22
23- [AVM Documentation](https://aka.ms/AVM)
24- [Bicep Module Index](https://azure.github.io/Azure-Verified-Modules/indexes/bicep/)
25- [Public Bicep Registry](https://github.com/Azure/bicep-registry-modules)
26- [AVM GitHub Organization](https://github.com/Azure/Azure-Verified-Modules)
27
28## Conclusion
29
30Azure Verified Modules represent a significant step forward in Infrastructure-as-Code maturity. By leveraging these pre-built, tested modules, you can:
31
32- Accelerate deployments with battle-tested code
33- Implement security best practices by default
34- Reduce maintenance burden on your team
35- Focus on business logic rather than boilerplate
36
37Start exploring the [AVM catalog](https://azure.github.io/Azure-Verified-Modules/indexes/bicep/) today and level up your Bicep deployments! 💪
38
39---
comments powered by Disqus