How can Bicep modules be structured to create reusable and standardized Azure infrastructure across multiple environments?
Question
Answers
Bicep modules are one of the most effective ways to move from writing individual Azure Resource Manager (ARM) templates to building a modular Infrastructure as Code (IaC) architecture. Instead of maintaining separate infrastructure definitions for development, testing, staging, and production, teams can create reusable modules and supply environment-specific parameters.
A good Bicep architecture separates what the infrastructure is from how it is configured for a particular environment.
A simplified structure could be:
Infrastructure/ │ ├── main.bicep ├── parameters/ │ ├── dev.bicepparam │ ├── test.bicepparam │ └── prod.bicepparam │ └── modules/ ├── networking.bicep ├── storage.bicep ├── keyvault.bicep ├── appservice.bicep └── monitoring.bicep
The main.bicep file acts as the composition layer, while individual modules represent reusable infrastructure components.
For example:
module network './modules/networking.bicep' = { name: 'networkDeployment' params: { environmentName: environmentName location: location } }
The networking module can then be reused for different environments without duplicating the implementation.
Separate reusable logic from environment configuration
One of the most important principles is to avoid putting environment-specific values directly inside reusable modules.
For example:
Module ↓ Defines architecture Environment parameters ↓ Defines values Deployment ↓ Combines both
Development might use smaller SKUs and fewer resources, while production may require higher availability, stronger security controls, and different scaling configurations.
The module itself should ideally remain unchanged.
Use naming conventions consistently
Enterprise Bicep deployments benefit from standardized naming.
For example:
<organization>-<application>-<environment>-<resource>
A production deployment could produce names such as:
sc-orders-prod-vnet sc-orders-prod-kv sc-orders-prod-app
The exact naming convention should be defined centrally rather than reinvented inside every module.
Use outputs to connect modules
Modules should expose only the information that other modules actually need.
For example, a networking module could output a subnet resource ID:
output appSubnetId string = appSubnet.id
Another module can consume that output.
This creates a dependency chain:
Networking ↓ Subnet ID ↓ Application
Use Azure Verified Modules where appropriate
Microsoft's Azure Verified Modules (AVM) ecosystem provides reusable, standardized Bicep modules for many Azure resources. Teams should evaluate whether an existing verified module meets their requirements before creating and maintaining their own implementation.
Production architecture
A mature implementation might look like:
main.bicep │ ┌────────────┼────────────┐ ↓ ↓ ↓ Networking Security Monitoring │ │ │ └────────────┼────────────┘ ↓ Application ↓ Database
Then:
dev.bicepparam test.bicepparam prod.bicepparam
supply environment-specific configuration.
Common mistakes
Beginners often:
- Duplicate entire Bicep files for every environment
- Hard-code resource names
- Mix secrets with infrastructure code
- Create highly coupled modules
-
Put too much logic inside
main.bicep - Create modules that are so generic they become difficult to understand
- Ignore dependency management
The objective isn't to create the maximum number of modules.
The objective is to create logical, reusable boundaries.
A practical learning project
A useful portfolio project is:
Build a three-environment Azure platform using Bicep
Create:
Dev → Test → Production
and provision:
- Resource Group
- Virtual Network
- Subnets
- Key Vault
- Storage
- Application service
- Monitoring
Use the same modules across all three environments while changing only the required parameters.
That demonstrates considerably more practical IaC capability than simply writing a single Bicep template.