adaptive.run TECH BLOG

Cloud can be tricky sometimes. Find out what scenarios we've ran into that are worth being mentioned and explained.

Writing Readable and Maintainable Azure Bicep Templates

Level: 200
Publishing date: 03-Jan-2025
Author: Catalin Popa

When working with Azure Bicep, clarity and structure are just as important as functionality. A well-organized template simplifies troubleshooting, enhances collaboration, and makes future modifications easier.

By following best practices, you can significantly improve the readability and maintainability of your Infrastructure-as-Code (IaC) templates. Here are some key recommendations:

Use a consistent naming convention to distinguish parameters, variables, resources, and outputs.
Structure the template logically and apply proper formatting to ensure clarity.
Avoid untyped objects or arrays by using User-Defined Types for validation and IntelliSense support.
Keep definitions simple and avoid embedding complex logic within resource declarations.
Leverage modules to break large templates into reusable components for better maintainability.

The Importance of a Naming Convention

Naming conventions play a vital role in making code readable. If you've worked with programming languages like JavaScript or Python, you’re probably familiar with patterns such as camelCase for variables or PascalCase for class names. A similar approach in Bicep templates ensures consistency and makes it easier to identify different components.

Unlike PowerShell or C#, Bicep does not yet have an industry-standard naming convention. However, establishing a well-defined naming pattern within your team can make a huge difference, especially when working with large-scale deployments.

Using Notation to Improve Clarity

In ARM templates written in JSON, parameters and variables were referenced with [parameter(...)] and [variable(...)], which functioned similarly to Hungarian Notation by indicating the type of value being used. A comparable method can be applied in Bicep by adding prefixes to parameters and variables, helping differentiate between them at a glance.

For instance, using prefixes such as param for parameters and var for variables can prevent confusion when reading a long template. This approach is particularly helpful when working on extensive IaC projects where clear separation between components saves time and reduces errors.
To illustrate, let’s look at an example. Below, you’ll see an Azure Virtual Network definition written without a structured naming approach. This makes it harder to distinguish whether a value comes from a parameter, variable, or a resource definition.

Bicep

param name string
param addressPrefix string

var location = 'westeurope'

resource virtualNetwork 'Microsoft.Network/virtualNetworks@2023-05-01' = {
name: name
location: location
properties: {
addressSpace: {
addressPrefixes: [addressPrefix]
    }
  }
}

output vnet object = virtualNetwork 

In this template, the purpose of name and addressPrefix isn't immediately clear. Without a naming pattern, it’s difficult to distinguish whether name refers to a resource name, parameter, or variable. In the next section, I’ll show how a structured naming approach improves clarity.

Applying a Clear Naming Convention

By using a structured naming approach, it becomes immediately clear where each value originates from. In the example below, prefixes help differentiate parameters, variables, resources, and outputs.

Bicep

param parVnetName string
param parAddressPrefix string

var varLocation = 'westeurope'

resource resVirtualNetwork 'Microsoft.Network/virtualNetworks@2023-05-01' = {
name: parVnetName
location: varLocation
properties: {
addressSpace: {
addressPrefixes: [parAddressPrefix]
    }
  }
}

output outVnet object = resVirtualNetwork 

Here, the prefixes make it obvious that:

      • parVnetName and parAddressPrefix are parameters,
      • varLocation is a variable,
      • resVirtualNetwork is a resource,
      • outVnet is an output referencing the deployed resource.

Recommended Prefixes for a Clear Naming Convention

If you find the structured naming approach helpful, you might consider using the following prefixes to differentiate Bicep components:

# Component Prefix
1 Module                 mod
2 Resource res
3 Variable var
4 Parameter par
5 Output out
6 Function func
7 Type type

This is just one approach to organizing Bicep templates. If you or your team already follow a different naming convention that ensures clarity and consistency, there’s no need to change it. The key is to maintain a structured format that makes templates easy to read and maintain over time.

Use Typed Objects and Arrays for Better Readability

There’s nothing inherently wrong with untyped objects or arrays—before User-Defined Types were introduced, they were the only option. However, now that we have the ability to define types, I strongly recommend using them in Bicep templates.

Typing an object or array provides more context about its contents, making the template easier to understand. Additionally, defining types enforces constraints on the expected structure, preventing accidental misconfigurations. A bonus is that IntelliSense can offer auto-completion and validation, reducing the likelihood of errors.

Below is an example where an Azure Virtual Network is defined with a parameterized subnet property. In this case, parSubnets is an untyped array, meaning it can hold any values. However, deploying subnets requires a specific structure, which is not enforced here:

Bicep

param parSubnets array
resource resVirtualNetwork 'Microsoft.Network/virtualNetworks@2024-01-01' = {
name: 'vnet-example'
location: 'westeurope'
properties: {
addressSpace: {
addressPrefixes: [
'10.0.0.0/16'
  ]
}
subnets: parSubnets
  }

Because parSubnets is defined as a generic array, there are no restrictions on its content. This makes the template harder to validate and troubleshoot, especially in large deployments. In the next section, we’ll explore how defining a proper type can improve both clarity and reliability.

By defining a User-Defined Type, we explicitly state what the array should contain. This ensures that every subnet object follows a consistent structure, improving both readability and validation. In the Bicep template below, parSubnets is now typed as subnetType, making it clear that each item in the array must include name and addressPrefix, while delegation remains optional (indicated by ?).

Bicep

param parSubnets subnetType

resource resVirtualNetwork 'Microsoft.Network/virtualNetworks@2024-01-01' = {
name: 'vnet-example'
location: 'westeurope'
properties: {
addressSpace: {
addressPrefixes: [
'10.0.0.0/16'
  ]
}
subnets: parSubnets
  }
}

type subnetType = {
name: string
properties: {
addressPrefix: string
delegation: string?
     }
}[] 

Benefits of Using a Typed Array

      • Improved Readability: Anyone reviewing the template can immediately understand what                  parSubnets is supposed to contain.
      • Stronger Validation: The deployment will fail if the provided values don’t match the expected          structure, reducing misconfigurations.
      • IntelliSense Support: Auto-completion suggests expected properties, making template                    authoring faster and reducing errors. 

Use Modules to Improve Reusability and Maintainability

As Bicep templates grow in complexity, maintaining them can become challenging. Instead of defining all resources in a single template, you can use modules to break down infrastructure components into reusable and manageable pieces.

Why Use Modules?

      • Improved Organization: Each module handles a specific component (e.g., networking,                    storage, compute), making the main template cleaner.
      • Reusability: Modules can be used across different environments or projects, reducing                       duplication.
      • Simplified Maintenance: Updates to a module can be applied consistently across multiple                deployments.

Example of a Virtual Network Module

We can move the virtual network definition into a separate module and call it from the main template:

Bicep

// vnetModule.bicep
param parVnetName string
param parAddressPrefixes array

resource resVirtualNetwork 'Microsoft.Network/virtualNetworks@2024-01-01' = {
name: parVnetName
location: resourceGroup().location
properties: {
addressSpace: {
addressPrefixes: parAddressPrefixes
    }
  }
}

output outVnetId string = resVirtualNetwork.id 

Now, in the main template, we can call the module instead of defining the virtual network directly:
Bicep

module modVnet './vnetModule.bicep' = {
name: 'deployVnet'
params: {
parVnetName: 'vnet-example'
parAddressPrefixes: ['10.0.0.0/16']
  }

Conclusion

Writing readable and maintainable Azure Bicep templates is essential for long-term efficiency in Infrastructure-as-Code. By following these best practices, you can create templates that are easier to understand, debug, and scale:

      • Use a consistent naming convention to distinguish parameters, variables, resources, and              outputs.
      • Structure your templates logically and apply proper formatting.
      • Avoid untyped objects and arrays by leveraging User-Defined Types for clarity and                        validation.
      • Break down large templates into modules to improve reusability and maintainability.

By implementing these strategies, your Bicep templates will be more structured, scalable, and easier to collaborate on within teams. As the Bicep ecosystem evolves, staying up to date with best practices will ensure that your Infrastructure-as-Code remains robust and future-proof.


Mobirise
adaptive.run

Transform your business.
Run adaptive.

Contact

Phone: +40 73 523 0005
Email: hello@adaptive.run

Mobirise Website Builder
Mobirise Website Builder

© Copyright  2019-2025 adaptive.run- All Rights Reserved