Hey again! 👋
Today, we're diving into one of Bicep’s most powerful features: modules.
If your main Bicep file is starting to look like a wall of never-ending code, don’t worry — there’s a better way. 🎯
Modules help you break down, reuse, and organize your templates into clean, focused units. Think of them like Lego bricks — build them once, snap them together whenever you need them.
Let’s explore how to create and use Bicep modules with style.
🤔 Why Use Modules?
You've probably already felt the pain of:
- Massive monolithic templates 🧱
- Repeating the same code across projects 🔁
- Difficult-to-navigate resource definitions 😵
Bicep modules fix all that:
✅ Reusability
Create once, reuse across multiple templates or projects.
✅ Encapsulation
Group related resources (like an App Service and its plan) together. Hide the implementation, expose only the essentials.
✅ Composability
Chain modules and pass outputs from one into another. It’s like building infrastructure pipelines from modular blocks.
🛠️ Creating a Module
A module is just another Bicep file. You treat it like any other template — but instead of deploying it directly, you import it into a main file using the module
keyword.
Here’s an example:
module appModule 'modules/app.bicep' = {
name: 'myApp'
params: {
location: location
appServiceAppName: appServiceAppName
environmentType: environmentType
}
}
🧾 Breakdown:
-
appModule
→ symbolic name, used internally (not visible in Azure). -
'modules/app.bicep'
→ path to the Bicep file. -
params
→ pass in all required parameters for the module. -
name
→ the deployment name (used in Azure’s deployment logs).
🧰 When to Use a Module
Use a module when:
- You’re grouping related resources (like a full Function App setup).
- You want to reuse infrastructure patterns (like a common network config).
- You’re refactoring a massive Bicep file and want better readability.
💡 Don’t go overboard. You don’t need a module for every single resource — group resources that logically belong together.
🔄 Chain Modules Together
Let’s say you want to deploy a VM on a custom VNet. You can:
- Create a virtual network module.
- Create a VM module.
- Output the subnet resource ID from the VNet module and pass it into the VM module.
module virtualNetwork 'modules/vnet.bicep' = {
name: 'virtual-network'
}
module virtualMachine 'modules/vm.bicep' = {
name: 'virtual-machine'
params: {
adminUsername: adminUsername
adminPassword: adminPassword
subnetResourceId: virtualNetwork.outputs.subnetResourceId
}
}
✅ Bicep knows there’s a dependency and will wait for the VNet to finish before deploying the VM. Neat!
🔧 Add Parameters & Outputs
Each module acts like a little black box. You feed it inputs (parameters), and it can give you outputs in return.
📥 Parameters
You can define them just like in any Bicep file:
@description('The name of the storage account.')
param storageAccountName string
📌 Best practice: Don’t duplicate business rules (like production-only SKUs) inside modules unless it makes sense. Keep your modules generic — let the parent template decide.
📤 Outputs
Useful if the parent needs something back — like a name, ID, or connection string (non-secret!).
@description('The storage account name.')
output storageName string = storageAccount.name
💡 Never output secrets. If you need to handle those, consider Azure Key Vault integration instead.
🔄 Nest Modules
Yes, modules can call other modules. That’s called nesting. But keep it manageable — too many layers, and it becomes spaghetti Bicep 🍝
If your setup gets really complex, it might be better to use deployment pipelines (like in Azure DevOps or GitHub Actions) instead of nesting everything in Bicep.
🔎 How Modules Work Behind the Scenes
- Every module triggers its own deployment (
Microsoft.Resources/deployments
). - Azure tracks each one separately in deployment history.
- If you give two deployments the same name, the later one overwrites the first. Use unique names if you want to preserve history.
Bicep transpiles everything into a single JSON ARM template, regardless of how many modules you use. So under the hood, Azure only sees one mega-template.
🧪 Bonus: Add Conditions to Modules
Want to make modules even more dynamic? Combine with if
to only deploy under certain conditions:
module logs 'modules/diagnostics.bicep' = if (sendDiagnostics) {
name: 'diagnostics'
params: {
logAnalyticsWorkspaceId: logAnalyticsWorkspaceId
}
}
🧠 TL;DR – Bicep Modules Recap
- Group related resources into clean, reusable files.
- Use parameters/outputs to control and connect your modules.
- Chain modules by passing outputs from one into another.
- Use conditions and defaults to make them flexible.
- Don’t over-nest. Keep it simple and maintainable!
Bicep modules = cleaner templates, reusable patterns, and fewer headaches 😌
Next time your Bicep file starts feeling bloated — split it up. Your future self will thank you 🙏
If this was helpful or if you’ve got Bicep ideas to share, come say hey on LinkedIn. Let’s learn together! 🚀
Top comments (0)