Anti-Pattern 3: Making giant spaghetti messes of Code or splitting Code illogically
Trent Steenholdt
December 3, 2024
8 minutes to read
Welcome back to our series on Azure Bicep anti-patterns. In this third installment, we tackle the issues that arise from creating overly messy, and illogically structured code—or as some might call it, “spaghetti code”. These kinds of practices can turn your Infrastructure as Code (IaC) into a maintenance nightmare and you’ll be left on your own to troubleshoot it, without any support or collaboration efforts. Let’s explore how to design clear and logical code that balances complexity with readability.
Revisiting Modularisation
Before diving in, it’s important to revisit the concept of modularisation we discussed in the previous post. Modularisation helps in organising your code into reusable, manageable pieces, enhancing readability and maintainability. Ignoring this principle often leads to the creation of convoluted code structures and is the reason “spaghetti code” starts to reappear time and time again.
The problems with Spaghetti Code
Out-of-Order Code and Overreliance on dependsOn
One common issue is placing resources and modules in a disorganised manner within your Bicep files. E.g. You have built feature A, but now are tacking on features B and C without much consideration to their order in the file itself. Developers sometimes then rely heavily on the “legacy” dependsOn
property or resource references to manage the deployment order, neglecting the logical flow and readibility of the code.
Why is this problem:
- Humans are incredibly lazy creatures: If your bicep code becomes difficult for team members to read and understand the code, they wont collaborate or support you moving forward.
- Maintenance: Troubleshooting and updating the code becomes a complex task, making one change breaks another, then another etc.
- Error-Prone: Increases the likelihood of introducing bugs due to overlooked dependencies.
Example of Disorganized Code:
// Start of the file
resource vm 'Microsoft.Compute/virtualMachines@2021-04-01' = {
name: 'myVM'
scope: resourceGroup(resourceGroups.outputs.name)
location: location
properties: {
// VM properties
}
dependsOn: [
networkInterface
]
}
// Somewhere in the middle of the file
module resourceGroups '../modules/resourceGroups.bicep' = {
name: 'resourceGroupsModule'
params: {
location: location
tags: tags
}
}
// End of the files
resource networkInterface 'Microsoft.Network/networkInterfaces@2021-05-01' = {
name: 'myNIC'
scope: resourceGroup(resourceGroups.outputs.name)
location: location
properties: {
// NIC properties for myVM
}
}
In this example, the virtual machine resource is defined at the start of the file yet it would be deployed after other components and modules in the file, making the code much harder to follow.
Solution:
- Logical Ordering: Place resources in a logical sequence that reflects their dependencies.
- Minimise
dependsOn
: Let Bicep handle implicit dependencies when possible.
Refactored Code:
// Create your resource groups first
module resourceGroups '../modules/resourceGroups.bicep' = {
name: 'resourceGroupsModule'
params: {
location: location
tags: tags
}
}
// Define the network interface next
resource networkInterface 'Microsoft.Network/networkInterfaces@2021-05-01' = {
name: 'myNIC'
scope: resourceGroup(resourceGroups.outputs.name)
location: location
properties: {
// NIC properties
}
}
// Then define the virtual machine
resource vm 'Microsoft.Compute/virtualMachines@2021-04-01' = {
name: 'myVM'
scope: resourceGroup(resourceGroups.outputs.name)
location: location
properties: {
// VM properties
networkProfile: {
networkInterfaces: [
{
id: networkInterface.id
}
]
}
}
}
By organizing the code logically, you improve readability and reduce the need for explicit dependsOn
declarations.
Naming resources and modules clearly
Naming deployments
As your infrastructure evolves, you might add new features that necessitate multiple instances of similar resources. Using generic names like storageAccount
can lead to confusion when you have more than one.
Example of Vague Naming:
resource storageAccount 'Microsoft.Storage/storageAccounts@2021-04-01' = {
name: 'mystorageaccount'
// properties
}
If you later add another storage account, it becomes unclear which is which.
Solution:
- Descriptive Names: Rename resources to reflect their purpose.
- Consistent Naming Conventions: Adopt a naming standard across your codebase.
Refactored Example:
resource storageAccountForBlob 'Microsoft.Storage/storageAccounts@2021-04-01' = {
name: 'blobstorageaccount'
// properties
}
resource storageAccountForDataLake 'Microsoft.Storage/storageAccounts@2021-04-01' = {
name: 'datalakestorageaccount'
// properties
}
By using descriptive names, you make the code more understandable and easier to maintain.
Proper usage of Parameters over Variables
Parameters vs. Variables
Another big anti-pattern I continue to see in Bicep is misuse of var
when param
should be used. Understanding when to use each is crucial for creating flexible and maintainable templates.
- Parameters (
param
): Used for values that can change between deployments. E.g. ‘australiaeast’ vs ‘australiasoutheast’ - Variables (
var
): Used for values derived from parameters or constants within the template.
Common Misuse:
var location = 'eastus'
This hardcodes the location, making it difficult to deploy to different regions.
Proper Use of Parameters:
@description('The location for all resources.')
param location string = resourceGroup().location
Proper usage of Data Types
Using the correct data types (string
, array
, object
) enhances code clarity and reduces errors.
Example with Types:
@description('List of subnet names.')
param subnetNames array = [
'subnet1'
'subnet2'
]
@description('Configuration settings for the application.')
param appConfig object = {
setting1: 'value1'
setting2: 'value2'
}
Benefits:
- Validation: Bicep can perform type checking, catching errors early (testing of IaC is important).
- IntelliSense Support: Improved code completion and documentation in editors.
- Clarity: Other developers can quickly understand what kind of data is expected.
Advanced users of Bicep, consider using user-defined data types to enrich the IntelliSense Support of your code.
Best Practices for Code Structure
-
Logical Resource Ordering: Place resources in the order they are deployed and depend on each other.
-
Descriptive Naming: Use clear and descriptive names for resources, modules, and variables.
- Appropriate Use of Parameters and Variables:
- Use
param
for inputs that may change between deployments. - Use
var
for computed values within the template.
- Use
- Consistent Coding Standards:
- Adopt a style guide for naming conventions and code formatting.
- Use comments and descriptions to explain complex logic.
- Limit the Use of
dependsOn
:- Let Bicep infer dependencies whenever possible.
- Explicitly define
dependsOn
only when necessary.
Conclusion
Avoiding messy and illogical code structures is crucial for the success of your IaC projects. By organising your code logically, using descriptive names, and appropriately leveraging parameters and variables, you make your Bicep templates more readable and maintainable.
Remember, your code should be understandable not just by machines, but by the humans who will read and maintain it in the future.
What’s Next?
In our next post, we’ll explore the importance of using outputs for dependencies between modules and resources. Ignoring outputs can create unnecessary bottlenecks and complexity in your infrastructure deployments. Stay tuned as we continue to uncover common anti-patterns and how to avoid them.