Introduction: Designing for Reuse and Scalability
You’ve just written your fifth Bicep template to deploy a storage account. Different environments, same logic, slightly tweaked each time. It’s working—but it’s starting to feel messy.
That’s the creeping cost of repetition.
As your infrastructure grows, you’ll often find yourself duplicating the same blocks of code across templates: deploying a virtual network here, a storage account there, maybe wiring up logging or monitoring the same way over and over again. Every time something changes, you’re chasing it down in multiple places.
This isn’t just tedious—it fragments your codebase. It breaks consistency. It slows down your ability to scale, collaborate, or make confident changes.
That’s the problem Bicep modules are designed to solve.
Modules give you a way to define a piece of infrastructure logic once—then reuse it across environments, projects, and teams with confidence.
A module might encapsulate a full virtual network setup, a secure storage account with diagnostics, or even a complete service deployment. You plug it into any template that needs it—without reinventing the wheel.
In this guide, we’ll break down the power of modules and walk through how to use them effectively, including:
- What Bicep modules are and how they differ from regular Bicep files
- How to design and reference modules in your templates
- Best practices for creating clean, focused, and reusable modules
- Real-world examples that demonstrate how modules fit into larger architecture
By the end, you’ll not only know how to use modules—you’ll understand why they matter, how they align with solid design principles, and how they can transform the way you structure and scale your infrastructure.
What Is a Module, Really?
At first glance, a Bicep module might seem like a new or separate concept—but technically, it’s nothing special.
A Bicep module is just another Bicep file. Same syntax. Same structure. No new language features inside the file itself.
What makes it a module isn’t the file—it’s how it’s used.
When you reference one Bicep file from another—typically using the module
keyword, passing parameters into it, and optionally collecting outputs—you’re using that file as a module.
This small shift has a big impact.
Instead of writing massive, monolithic templates, you start thinking in terms of reusable building blocks—each focused on a single purpose. Sometimes that building block is small, like a storage account or virtual network. Other times it encapsulates a full application stack: a web app, database, networking, and monitoring—all packaged together.
And your main.bicep
file? That becomes something bigger.
It becomes the high-level plan that brings together modular building blocks—each one encapsulating a specific part of your infrastructure. Instead of repeating details, you assemble reusable components into a coherent whole.
Here’s what this interaction looks like:
In this diagram:
- The
main.bicep
file is the calling template - It references a
storageAccount.bicep
module - It passes in parameters like the name, location, and SKU
- The module defines the resource using those inputs
- It optionally returns an output—such as the resource ID—back to the main template
💡 A file only becomes a module when it is referenced from another template using the
module
keyword. Copying and pasting the same resource definition into different files is not modularisation—it’s duplication.
This is how we shift from “just writing templates” to designing infrastructure as clean, reusable, maintainable components.
And the beauty is: there’s no special folder structure, no naming convention, and no added complexity. A module is simply a Bicep file that’s written with encapsulation and reuse in mind—and then used from another file that calls it.
In the next section, we’ll explore why this model is so valuable—and how to decide when it’s the right time to introduce modules into your templates.
When and Why to Use Modules
By now, you know what a Bicep module is. But just because you can write a module doesn’t mean you always should.
The real value of modules shows up when your infrastructure starts to grow—across teams, environments, or projects—and the design pressure increases. You’re not just deploying a few resources anymore. You’re building a system.
This is the moment you stop thinking in individual resources and start thinking in infrastructure patterns.
So—when should you reach for a module?
There’s no strict rule, but there are some clear signs that modularisation is the right move.
1. You’re Repeating the Same Resource Logic Across Templates
Maybe it’s a storage account that always needs diagnostics enabled, or a virtual network with the same subnet layout. If you’ve copied and pasted the same block of code more than once, that’s your signal.
Instead of duplicating, turn that resource into a module. Then reference it from anywhere.
2. You Want to Standardise Patterns Across Teams
Modules are a powerful way to enforce consistency. Let’s say your platform team wants every web app to be deployed with logging, monitoring, a naming convention, and app settings configured.
Rather than asking every team to copy best practices into their templates, you give them a module. One input, one output. Consistent behaviour, minimal effort.
3. You Need to Make the Same Change in Multiple Places
Without modules, updating infrastructure logic means chasing down the same resource across templates. With modules, you update it once—at the source—and every template that references it gets the improvement.
This becomes even more valuable when you version your modules and use them across environments, business units, or even repositories.
4. Your Template Is Getting Hard to Understand
If your main.bicep
file has 800+ lines and is trying to define an entire environment—networking, compute, monitoring, security—it’s doing too much.
That’s not just hard to read. It’s hard to trust.
Modules let you break it down into logical units:
networking.bicep
storage.bicep
compute.bicep
diagnostics.bicep
Now your main file reads like a plan, not a wall of YAML-like noise.
5. You’re Designing for Reuse or Scale
Sometimes the goal is simply reuse—across dev, test, and prod. Other times, it’s scale: you want the same application pattern deployed across dozens of regions or customers.
Either way, modules let you define something once and apply it in multiple contexts, without rewriting or reworking the core logic.
The Shift: From Implementation to Composition
The more your infrastructure evolves, the more your mindset needs to shift—from “how do I define this resource?” to “how do I compose this system from clean, reliable components?”
That’s what modularisation enables.
💡 When writing templates starts to feel like wiring up systems,
you’re not just a Bicep user anymore—
you’re thinking like an infrastructure architect.
When Should You Use a Module?
- When you’re repeating logic or effort across templates
- When you want to enforce best practices across teams
- When your template is becoming too long to manage confidently
- When you’re designing for reuse across projects, tenants, or environments
Let’s Build a Module (Step-by-Step)
Now that we’ve seen why modules matter and when to use them, let’s walk through how to build one—step by step.
We’ll start with something small but meaningful: a storage account. This is a common building block in Azure, and a great example of how to turn a standard resource into a clean, reusable module.
You’ll see how to:
- Define inputs and outputs in a module
- Use the
module
keyword to plug it into another template - Pass values into the module, and retrieve values back
Let’s get started.
Step 1: Define the Module (storageAccount.bicep
)
Create a new file called storageAccount.bicep
. This file will contain all the logic needed to deploy a storage account—but written in a way that makes it reusable and parameterised.
|
|
Let’s break this down:
- We define three parameters:
storageAccountName
– the name of the storage accountlocation
– defaults to the resource group’s locationsku
– defaults to'Standard_LRS'
These parameters allow the calling template to customise how this storage account is created.
- The resource block provisions the actual storage account.
- The output exposes the
resourceId
of the created storage account, which can then be used by the parent template.
🧠 This makes your module more than just a black box. It can pass useful values back to the caller—like IDs, hostnames, or connection strings. It’s how modules communicate.
Nothing here is new syntax. If you’ve written Bicep before, you’ve seen all of this. The difference now is intent: you’re writing this with reuse in mind—designing for input and output, instead of hardcoding everything.
Step 2: Plug the Module into Your Main Template (main.bicep
)
Now, let’s create (or open) a main.bicep
file. This will act as the parent template—it will call the module, pass values into it, and receive values back.
|
|
Let’s walk through what’s happening:
-
module storageModule './storageAccount.bicep' = { ... }
This is how you reference another Bicep file as a module. You give it a symbolic name (storageModule
) and point to the file path. -
name: 'myStorageAccountDeployment'
This is a unique deployment name for this module instance. It’s used in the Azure deployment history to help track what was deployed. -
params: { ... }
This block passes values into the module’s parameters. Here’s what we’re doing:storageAccountName
is set to'mystorage${uniqueString(resourceGroup().id)}'
.
This usesuniqueString()
to avoid naming collisions—especially important since storage account names must be globally unique.location
andsku
are passed through directly.
-
output storageAccountId = storageModule.outputs.storageAccountId
This captures the output from the module—in this case, the storage account ID we’re expecting it to send back to us.
🧠 Think of it like calling a function:
You pass inputs to the module, it does its job, and gives you a result back.
A Quick Recap
You just built and used your first module. 🎉
✅ You defined a Bicep file with parameterised inputs and an output
✅ You deployed it from a parent file using the module
keyword
✅ You passed values in and received a result back
✅ You structured your logic into a reusable, standalone unit
This isn’t just about syntax. You’re now thinking about infrastructure in terms of composable components—not just one long file with everything jammed inside.
Remember the diagram from earlier?
You’ve now brought that structure to life—turning your main.bicep
file into a coordinator, and your storageAccount.bicep
module into a focused building block.
But this is just the beginning.
Now that you’ve got the basics down, let’s take things further. In the next section, we’ll explore best practices for designing modules that are clean, focused, and production-ready.
Best Practices for Designing and Organising Modules
You’ve built a working module. That’s a good start. But as any experienced engineer knows, working doesn’t always mean good.
A messy module might technically deploy resources—but it’s hard to reuse, hard to trust, and painful to maintain. Over time, that mess compounds across projects and teams.
Great modules, on the other hand, are a pleasure to use. They feel like tools: focused, reliable, and predictable.
Here are 9 practical, field-tested principles to help you move from “working” to “well-designed.”
1. Keep Each Module Focused on One Thing
A good module should do one job well. It might define:
- A virtual network
- A storage account with diagnostic settings
- An application stack (which can itself be broken into smaller modules)
🧠 This improves readability, testability, and reuse—and makes naming easier too.
If your module starts branching into “…and maybe it could also deploy a Key Vault,” it’s time to split it up.
2. Use Clear, Descriptive Names
Name your modules based on what they actually do:
- ✅
virtualNetwork.bicep
- ✅
webAppWithDatabase.bicep
- ❌
module1.bicep
,infraStuff.bicep
🔍 Descriptive names help both you and your team know what a module is at a glance—no digging required.
The same rule applies to parameters and outputs: if you need a comment to explain what something means, it’s probably time for a better name.
3. Parameterise What Should Be Flexible
Use parameters to allow variation across environments or contexts:
- Resource names
- SKUs and pricing tiers
- Feature flags like
enableDiagnostics
🛠 This lets you reuse the same module across dev, test, and prod—without editing the file.
But don’t overdo it. Not everything needs to be configurable. Some things are best left hardcoded.
4. Provide Defaults Where It Makes Sense
Help users by setting reasonable defaults:
|
|
🔧 Defaults reduce friction for callers while still allowing overrides when needed.
Consistency improves when users don’t have to decide on every little detail.
5. Expose Only What’s Needed via Outputs
Use output
statements to return values like resource IDs, hostnames, or connection strings.
🎯 Treat outputs as your module’s public interface—keep it clean and minimal.
Avoid exposing internals unless they’re genuinely useful to the calling template.
6. Organise Modules in a Logical Folder Structure
As your module library grows, discoverability becomes a real issue. Group related modules together:
|
|
📁 Without structure, even small module libraries become a maze—and people will stop reusing what’s hard to find.
7. Version Your Modules
If a module is shared across teams or environments, versioning is essential. It protects you from unintended breakage when making changes.
Use Git tags, semantic versioning, or folder versions like v1.0.0/
.
🔐 Versioning gives you a safe upgrade path—so your modules can evolve without causing chaos.
8. Document Complex Logic
Don’t assume future-you (or your teammates) will remember why something exists.
Use comments to explain:
- Why a parameter exists
- What a conditional means
- Why a particular API version or resource setting is used
✍️ Good code explains how. Great code also explains why.
9. Be Opinionated Where It Adds Value
Don’t aim for infinite flexibility. Aim for predictable, usable structure.
Encode best practices like:
- Secure defaults
- Consistent naming conventions
- Enforced tags or diagnostics
🧱 A good module doesn’t just let you configure things—it guides the user toward doing it the right way.
Reuse is good. Predictability is better.
Bringing It All Together
A clean module is:
- Easy to understand
- Easy to use
- Easy to change without fear
These design principles aren’t just about aesthetics—they’re about trust.
Trust in the module. Trust in the system. Trust in your ability to evolve infrastructure safely over time.
💬 Great modules don’t just scale infrastructure.
They scale confidence.
In the next section, we’ll take these principles off the whiteboard and put them into practice—walking through real-world module patterns that combine networking, storage, and compute into clean, composable systems.
Let’s build something real.
Real-World Examples and Use Cases
So far, we’ve explored how modules work, when to use them, and how to design them well. Now it’s time to see it all come together in action.
We’ll walk through three patterns you’re likely to encounter in your real work:
- A foundational module that’s reused across environments
- A full application stack composed from smaller modules
- A shared-module model used across teams and projects
Each example highlights how modular thinking improves clarity, reusability, and maintainability as your infrastructure grows.
Let’s start with the most common: a reusable virtual network.
Example 1: A Reusable Virtual Network Module
Let’s say you need to deploy a virtual network with two subnets: one for frontend resources, one for backend. This is a common pattern—and a perfect candidate for a reusable module.
Module: vnet.bicep
|
|
Parent Template: main.bicep
|
|
This pattern scales easily.
Want to deploy to another environment? Change the parameter.
Want a third subnet? Extend the module—or make subnets dynamic with an array.
Either way, you’re designing with reuse and clarity—not copy-paste.
Example 2: Modularising a Full Application Stack
So far, we’ve focused on a single resource module. But what happens when your infrastructure gets more complex?
Let’s say you’re deploying an entire application stack—not just one component. This is where modular design really shines.
Project Structure
|
|
webApp.bicep
|
|
sqlDatabase.bicep
|
|
storageAccount.bicep
|
|
main.bicep
|
|
📦 Even with multiple layers, this pattern stays clean and manageable—because each module is focused and encapsulated.
Example 3: Sharing Modules Across Teams and Projects
In larger organisations, platform teams often create shared infrastructure modules—which are consumed by application teams to standardise deployments.
These shared modules live in a central module repository (GitHub, Azure DevOps, etc.) and are versioned for safe reuse.
Central Repo Structure
|
|
Referencing Published Modules from a Registry
In a downstream project:
|
|
🔐 Here we’re using the
br:
syntax to reference a module stored in a Bicep registry—allowing for versioned reuse at scale.
You can even take advantage of Azure Verified Modules from Microsoft and the community—handcrafted, secure-by-default, and production-ready modules published for shared use.
Shared modules encode best practices: naming conventions, security policies, diagnostics, and more—all in one place.
App teams get standardised infrastructure, while platform teams retain control over patterns and enforcement.
The Takeaway
Whether you’re deploying a simple VNet or a full-stack application, modules scale with you. They help you move from:
- Copy-pasting code → to referencing clean, purpose-built components
- Managing resource files → to composing structured environments
- Writing infrastructure → to designing reusable systems
This is the power of modularisation in Bicep.
You’re not just deploying resources anymore.
You’re building infrastructure that’s clear, composable, and ready to evolve.
Now it’s your turn: take what you’ve seen here and start breaking down your own infrastructure into clean, modular components. Start with a resource you use often. Turn it into a module. Test it. Then reuse it across projects. You’ll be surprised how quickly it pays off.
Conclusion: From Templates to Systems
What started as just a way to reuse code has now become something bigger—
a blueprint for how to think about infrastructure: cleaner, clearer, and built to scale.
You’ve now seen what Azure Bicep modules can do—from basic reuse to full-scale composition.
But more importantly, you’ve experienced a shift in mindset: from managing resources to designing systems.
This wasn’t just about breaking files into smaller pieces.
It was about scaling with intention, simplifying complexity, and building trust into every deployment.
Let’s recap what you’ve accomplished:
✅ You understood what a module is—and how it fits into the bigger picture
✅ You identified when to use modules—and why they matter
✅ You built your first module—from definition to deployment
✅ You explored best practices for clean, scalable design
✅ You saw real-world patterns that turn theory into usable infrastructure
This is how good infrastructure grows: not from more code, but from better structure.
What’s Next: Advanced Patterns and Automation
You’ve laid the groundwork. Now you’re ready to go deeper.
Here are a few directions to explore in the next phase of your journey:
- Conditional deployments – Let modules decide whether to deploy certain resources based on parameters
- Loops and arrays – Dynamically create multiple VMs, subnets, or other resources with
for
expressions - Cross-module dependencies – Connect outputs from one module directly into another
- Bicep registries – Publish and version reusable modules across teams
📘 Many of these topics are covered in the next article in this series:
“Advanced Bicep Patterns: Conditional Logic, Loops, and Dependencies.”
🎯 Until then, take the time to modularise something real.
Not everything—just one part of your infrastructure that feels too tightly coupled or too often repeated.
That’s how it starts.
Thanks for following along—and well done.
You’re not just writing infrastructure anymore.
You’re designing it—with purpose, with clarity, and with confidence.