How to Create and Use a Bicep Module: Step by Step
Repeating the same block of code for every Storage Account, virtual network or database makes Bicep files long and hard to maintain. A Bicep module solves this: it wraps one or more resources in a separate file that becomes reusable, easier to read and easier to test on its own. It is the same idea as a function in programming: you write it once and reuse it wherever you need it. Next you will create a module, pass parameters to it and read its outputs from a main file.
Prerequisites
- An Azure subscription and an existing Resource Group (for example,
rg-demo). - Azure CLI installed and signed in with
az login. - Visual Studio Code with the Bicep extension (recommended, but optional).
- Basic knowledge of parameters and resources in Bicep.
Step 1: Create the module
A module is just a regular Bicep file. Create a file named storageAccount.bicep with the resources you want to reuse. Pay attention to the input param values and the output values: they are the module's contract with whoever uses it. The @description() and @allowed() decorators document and restrict the accepted values, making the module safer.
@description('Prefixo para o nome da Storage Account')
param storagePrefix string
@description('Localização dos recursos')
param location string = resourceGroup().location
@allowed([
'Standard_LRS'
'Standard_GRS'
])
param sku string = 'Standard_LRS'
var storageName = '${storagePrefix}${uniqueString(resourceGroup().id)}'
resource storage 'Microsoft.Storage/storageAccounts@2023-05-01' = {
name: storageName
location: location
sku: {
name: sku
}
kind: 'StorageV2'
}
output storageName string = storage.name
output storageId string = storage.idThe uniqueString() function generates a deterministic suffix that guarantees a unique, valid name: Storage Accounts require globally unique lowercase names, and this avoids the name-already-taken error.
Tip: keep each module focused on a single goal (one Storage Account, one network, one database). Small, specific modules are far easier to reuse than a giant module that does everything.
Step 2: Consume the module in the main file
Now create the main.bicep file. To call the module you use the module keyword, followed by a symbolic name and the path to the file. The values in params match the param values declared in the module.
@description('Localização dos recursos')
param location string = resourceGroup().location
module stg 'storageAccount.bicep' = {
name: 'storageDeploy'
params: {
storagePrefix: 'dados'
location: location
sku: 'Standard_LRS'
}
}The symbolic name (stg) is used to reference the module within the file. The name property is the name of the nested deployment that appears in Azure: it is optional, but it helps you identify each deployment. Note that you do not need to pass every parameter: those with a default value in the module can be left out when the default is fine.
Step 3: Read the module outputs
A module can return values to the main file through its output values. To read them, use the module's symbolic name followed by .outputs and the output name. Add this to the end of main.bicep:
output storageAccountName string = stg.outputs.storageNameThis way, the real name of the Storage Account created by the module is available to reuse in other resources or to check at the end of the deployment.
Step 4: Validate and deploy
Before deploying, build the file to confirm there are no syntax errors:
az bicep build --file main.bicepIf there are no errors, deploy to your Resource Group:
az deployment group create --resource-group rg-demo --template-file main.bicepVerify the result
When the command finishes, confirm the module ran correctly by checking the deployment outputs:
az deployment group show --resource-group rg-demo --name main --query properties.outputsYou should see the storageAccountName value with the name of the Storage Account. In the Azure portal, inside the Resource Group, you will also find a deployment named storageDeploy: that is your module's run recorded as a nested deployment.
Conclusion
With a module, a resource becomes a reusable block that you can call as many times as you need, changing only the parameters. The natural next step is to use a for loop over the module to create several environments (dev, test and production) from the same file. Which resource in your infrastructure would be the first to turn into a module?