Anti-Pattern 2: Skipping Modularisation and not using templates for reuse
Trent Steenholdt
December 2, 2024
6 minutes to read
In our ongoing series on Azure Bicep anti-patterns, we turn our attention to a common pitfall: skipping modularisation and not using templates for reuse. While Azure Bicep simplifies the process of deploying Azure resources, neglecting modularisation can lead to code that’s so hard to read, maintain, and scale, that people are instantly turned away from it. In this second installment, we’ll explore why modularisation matters and how to do it effectively.
I have actually covered this topic before, but for the completeness on this series, I want to talk about why it’s a big anti-pattern to not do it!
Why Modularisation Matters
Modularisation is the practice of breaking down your code into smaller, reusable components or modules. This approach offers several benefits:
- Simplicity: Smaller modules are easier to understand and manage.
- Reusability: Modules can be reused across different deployments, saving time.
- Scalability: Modular code scales better as your infrastructure grows.
- Maintainability: Updates and bug fixes can be made in one place and propagate throughout.
- Collaboration: Teams can work on different modules simultaneously without conflicts.
The actual Anti-Pattern thats the biggest prolem: Overcomplicating Modularisation
A common anti-pattern is when developers attempt to modularise their code but end up creating unnecessary layers of abstraction. This often results in a “layered inception” effect—much like the movie Inception—where modules call other modules in a convoluted manner, making the code harder to follow.
Example of Over-Nested Modules
Consider the following structure:
main.bicep
calls one file,shared.bicep
shared.bicep
does all the work and calls other modules
This raises the question: Why not have the logic directly in main.bicep
or properly distribute it into meaningful modules?
Overcomplicated Example
// main.bicep
import * as shared from '../../configuration/shared/shared.conf.bicep'
targetScope = 'subscription'
// Common Parameters
@description('Optional. Location for all resources.')
param location string = deployment().location
@description('Optional. An object of tag key & value pairs to be appended to the Azure Subscription and Resource Group.')
param tags shared.tagsType
// ... (Additional parameters and variables)
// Module: Shared Services
module sharedServices '../modules/shared/shared.bicep' = {
name: take('sharedServices-${guid(deployment().name)}', 64)
scope: resourceGroup(resourceGroups.shared)
dependsOn: [
resourcecGroupForShared
networkingServices
]
params: {
acrName: resourceNames.conatinerRegistry
acrPullEntraGroupId: acrPullEntraGroupId
adminUsername: vmLocalUserName
adminPassword: vmLocalUserPassword
encryptionAtHost: false
keyVaultName: resourceNames.keyVaultShared
location: location
deployACR: deployACR
deployServiceBus: deployServiceBus
deployStorageTelemetry: deployStorageTelemetry
deployVM: deployVM
peSubnetName: resourceNames.privateEndpointSubnet
serviceBusName: resourceNames.serviceBus
servicePrincipalId: servicePrincipalId
sharedSubnetName: resourceNames.sharedSubnet
storageAccountDataName: resourceNames.storageAccountData
storageAccountTelemetryName: resourceNames.storageAccountTelemetry
tags: tags
virtualNetworkResourceId: deployVnet ? networkingServices.outputs.virtualNetworkResourceId : existingVnetResourceId
vmName: resourceNames.vmName
virtualMachineConfig: virtualMachineConfig
}
}
In this example, main.bicep
imports a shared configuration and delegates most of the work to shared.bicep
, which might further call other modules. This can make the codebase:
- Extremely Hard to Read: It’s not immediately clear where the logic resides and how params get passed to the nested bicep file.
- Difficult to Maintain: Changes require navigating through multiple layers. Want to add one
param
? Now you have to add it in two bicep files. - Less Reusable: Deeply nested modules are harder to reuse in other contexts.
Simplify This Example
To avoid this anti-pattern, aim for a flatter structure where each module serves a clear purpose and is used appropriately.
Simplified Example
// main.bicep
import * as shared from '../../configuration/shared/shared.conf.bicep'
targetScope = 'subscription'
// Parameters
param location string = deployment().location
param tags object = {}
// Resource Groups Module
module resourceGroups '../modules/resourceGroups.bicep' = {
name: 'resourceGroupsModule'
params: {
location: location
tags: tags
}
}
// Networking Module
module networking '../modules/networking.bicep' = {
name: 'networkingModule'
params: {
location: location
tags: tags
// ... other networking parameters
}
}
// Compute Module
module compute '../modules/compute.bicep' = {
name: 'computeModule'
params: {
location: location
tags: tags
// ... other compute parameters
}
}
// Storage Module
module storage '../modules/storage.bicep' = {
name: 'storageModule'
params: {
location: location
tags: tags
// ... other storage parameters
}
}
Each module—resourceGroups.bicep
, networking.bicep
, compute.bicep
, and storage.bicep
—handles a specific area of your infrastructure and is directly referenced by main.bicep
. This structure is:
- Easier to Read: The deployment flow is clear and straightforward.
- Maintainable: Updates to a module don’t require diving through multiple layers.
- Reusable: Modules can be used independently in other deployments.
Another Pro Tip: Use Azure Verified Modules
If you haven’t used Azure Verified Modules, then it’s a must go to right now! In 2024, there is no need to reinvent the wheel. Use Azure Verified Modules to benefit from community best practices.
Website: Azure Verified Modules
Example Usage:
module logAnalytics 'br/public:monitoring/log-analytics-workspaces:2.0.2' = {
name: 'logAnalyticsWorkspace'
params: {
name: 'myLogAnalyticsWorkspace'
location: location
retentionInDays: 30
tags: tags
}
}
Conclusion
Modularisation is essential for creating scalable, maintainable, and reusable Azure Bicep code. Avoid the anti-pattern of overcomplicating your modules by keeping your structure simple and logical. By following best practices and leveraging existing modules, you can significantly improve your Infrastructure as Code projects.
What’s Next?
In the next post of this series, we’ll examine the dangers of creating “giant spaghetti messes” of code or splitting code illogically. We’ll provide tips on how to design clear and logical code that balances complexity with readability.