Teams Keep Rebuilding the Same Azure Patterns? Use Azure Verified Modules
Each team can build its own storage account, Blob service, containers, security settings, diagnostics, and role assignments. The result is repeated Bicep code with different defaults, inconsistent security, and more code to maintain.
Azure Verified Modules (AVM) are versioned, Microsoft-maintained modules for common Azure resources and patterns. Replace the standard storage implementation with AVM, while keeping the CloudTrips wrapper responsible for its naming, tags, environment inputs, containers, and outputs.
Find the Storage Module
Open the Azure Verified Modules Bicep index and select the Storage Account resource module. Its page documents the supported parameters, outputs, examples, and versions.
This implementation uses:
br/public:avm/res/storage/storage-account:0.32.1
br/publicis the alias for Microsoft’s public Bicep registry.avm/res/storage/storage-accountidentifies the resource module.0.32.1pins the version tested by CloudTrips.
Always specify a version. Upgrading an AVM is an infrastructure change that
must pass review and what-if; it should not happen silently.

Replace the Custom Resource Implementation
The existing main.bicep already calls a local
modules/storage.bicep wrapper. Keep that boundary, but replace the direct
storage resources inside the wrapper with the verified module:
param applicationName string
param containerNames array
param costCenter string
param deployAuditContainer bool
param environment string
param location string
param owner string
param storageSku string
var normalizedEnvironment = toLower(environment)
var storageAccountName = 'stct${normalizedEnvironment}${uniqueString(resourceGroup().id)}'
var commonTags = {
Application: applicationName
CostCenter: costCenter
Environment: toUpper(normalizedEnvironment)
ManagedBy: 'Bicep'
Owner: owner
}
var appContainers = [
for containerName in containerNames: {
name: containerName
defaultEncryptionScope: '$account-encryption-key'
denyEncryptionScopeOverride: false
publicAccess: 'None'
}
]
var auditContainers = deployAuditContainer
? [
{
name: 'audit'
defaultEncryptionScope: '$account-encryption-key'
denyEncryptionScopeOverride: false
publicAccess: 'None'
}
]
: []
var blobContainers = concat(appContainers, auditContainers)
module storageAccount 'br/public:avm/res/storage/storage-account:0.32.1' = {
name: '${deployment().name}-avm-storage'
params: {
name: storageAccountName
location: location
tags: commonTags
skuName: storageSku
kind: 'StorageV2'
allowBlobPublicAccess: false
allowCrossTenantReplication: false
allowSharedKeyAccess: true
blobServices: {
containerDeleteRetentionPolicyEnabled: true
containerDeleteRetentionPolicyDays: 7
deleteRetentionPolicyEnabled: true
deleteRetentionPolicyDays: 7
deleteRetentionPolicyAllowPermanentDelete: false
containers: blobContainers
}
defaultToOAuthAuthentication: false
enableTelemetry: false
minimumTlsVersion: 'TLS1_2'
networkAcls: {
bypass: 'None'
defaultAction: 'Allow'
}
requireInfrastructureEncryption: false
supportsHttpsTrafficOnly: true
}
}
output storageAccountName string = storageAccount.outputs.name
output storageAccountId string = storageAccount.outputs.resourceId
output blobEndpoint string = storageAccount.outputs.primaryBlobEndpoint
output containerIds array = [
for containerName in containerNames: resourceId(
'Microsoft.Storage/storageAccounts/blobServices/containers',
storageAccountName,
'default',
containerName
)
]
output auditContainerId string? = deployAuditContainer
? resourceId(
'Microsoft.Storage/storageAccounts/blobServices/containers',
storageAccountName,
'default',
'audit'
)
: null
The local wrapper still exposes the same parameters and outputs to
main.bicep. It converts the CloudTrips container arrays into the AVM input
format and maps AVM outputs back to the existing contract.
The firewall bypass and container-encryption properties are explicit because they already exist in TEST. AVM has its own defaults; relying on them during a refactor could change a live setting. Telemetry is disabled for this learning deployment.
Restore and Validate the Module
From cloudtrips-bicep, run:
az bicep restore \
--file modules/storage.bicep
az bicep lint \
--file modules/storage.bicep
az bicep build-params \
--file environments/test.bicepparam \
--stdout > /dev/null
restore downloads the pinned module into Bicep’s local cache. The repository
stores only the versioned module reference, not a copied AVM implementation.
In VS Code, use Go to Definition on the module reference to inspect the
restored source.
Review the Refactor Before Accepting It
The committed snapshot still describes the hand-written implementation. Run the snapshot validation first:
"$HOME/.azure/bin/bicep" snapshot environments/test.bicepparam \
--mode validate \
--subscription-id 00000000-0000-0000-0000-000000000000 \
--resource-group rg-cloudtrips-bicep-test-weu \
--location westeurope \
--deployment-name deploy-cloudtrips-storage-test
It should fail because AVM compiles to a different nested template with more explicit provider properties. This is expected, but it is not yet proof that the change is safe.
Select CloudTrips TEST and compare the compiled template with Azure:
az account set \
--subscription "<CloudTrips TEST subscription name or ID>"
az deployment group what-if \
--resource-group rg-cloudtrips-bicep-test-weu \
--parameters environments/test.bicepparam \
--validation-level Provider
The existing storage account should keep the same name, resource ID, SKU, tags, TLS configuration, firewall behavior, and public-access setting. The three containers should report NoChange. The storage account and Blob service can report Modify because AVM sends explicit default properties that the original template omitted, and Azure normalizes some optional properties. There must be no resource deletion, creation, or replacement.

After reviewing what-if, approve the new compiled baseline:
"$HOME/.azure/bin/bicep" snapshot environments/test.bicepparam \
--mode overwrite \
--subscription-id 00000000-0000-0000-0000-000000000000 \
--resource-group rg-cloudtrips-bicep-test-weu \
--location westeurope \
--deployment-name deploy-cloudtrips-storage-test
Run the same command again with --mode validate. It should now finish without
a difference.
Deploy the Reviewed Version
Commit the wrapper and refreshed snapshot:
git add modules/storage.bicep \
environments/test.snapshot.json
git commit -m "Use AVM for the CloudTrips storage account"
Push the feature branch and open a pull request. The configured GitHub Actions
or Azure DevOps pipeline restores the public dependency, validates the
snapshot, runs live preflight and what-if, and deploys the approved version.

CloudTrips now reuses a reviewed Azure implementation without losing its own
standards. AVM reduces repeated resource code; it does not replace architecture
decisions, pinned-version review, what-if, or testing.