Insight Tech APAC Blog Logo

Anti-Pattern 2: Skipping Modularisation and not using templates for reuse

trentsteenholdt
December 2, 2024

6 minutes to read

Azure Bicep Anti-Patterns

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.