Insight Tech APAC Blog Logo

Anti-Pattern 3: Making giant spaghetti messes of Code or splitting Code illogically

trentsteenholdt
December 3, 2024

8 minutes to read

Azure Bicep Anti-Patterns

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

  1. Logical Resource Ordering: Place resources in the order they are deployed and depend on each other.

  2. Descriptive Naming: Use clear and descriptive names for resources, modules, and variables.

  3. Appropriate Use of Parameters and Variables:
    • Use param for inputs that may change between deployments.
    • Use var for computed values within the template.
  4. Consistent Coding Standards:
    • Adopt a style guide for naming conventions and code formatting.
    • Use comments and descriptions to explain complex logic.
  5. 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.