Insight Tech APAC Blog Logo

Address Repetition using Shared Variable File Patterns

stephentulp
December 8, 2023

7 minutes to read

Bicep Advent Calendar

Introduction

Now that we have an understanding of the Bicep structure, syntax, Bicep modules and parameters, we can start to look at some patterns that can be used to simplify our Bicep code. In this post, we’ll look at how we can use shared variable files to reduce the repetition of common values across multiple Bicep files. We can load these values from a shared JSON or YML file within the Bicep file. When building out the Bicep templates and code, there will be common variables that you generally reuse across a set of Bicep files. Duplicating these variables introduces the chances for errors and makes it harder to maintain the code when you need to make changes.

Shared Variable Files

There are quite a few Bicep functions that we can use to load files into our Bicep code. These include, the loadTextContent() function to load a text file, the loadJsonContent() function to load a JSON file and the loadYamlContent() function to load a YAML file.

We will use the following for the examples in this post:

  • loadYamlContent() function to load a YAML file to address resource naming and location prefixes
  • loadJsonContent() function to load a JSON file for shared Network Security Group rules

Resource Naming

Within the main.bicep that we have been using, there are multiple Azure resources that we will deploy as part of the landing zone, this will enable us to have a consistent naming experience and also take away the complexities of needing to define all naming segments within each Bicep file. We will use the loadYamlContent() function to load a YAML file that contains the naming segments that we will use for the resources.

  • We define a shared .yml that outlines all the naming prefixes that we will use for the resources.
"resourceGroup": "arg",
"networkSecurityGroup": "nsg",
"virtualNetwork": "vnt",
"routeTable": "udr"

In the main.bicep file we will use the loadYamlContent() function to load and import the above shared file.

var namePrefixes = loadYamlContent('../../configuration/shared/namePrefixes.yml')
var locationPrefixes = loadYamlContent('../../configuration/shared/locationPrefixes.yml')

We then create a variable that will use the toLower() function to convert the naming segments to lowercase and then use the concat() function to concatenate the various naming segments together, this is the shared variable name and also some other parameters that we will use for the naming of the resources.

var locPrefix = toLower('${locationPrefixes.australiaeast}')
var argPrefix = toLower('${namePrefixes.resourceGroup}-${locPrefix}-${lzPrefix}-${envPrefix}')
var vntPrefix = toLower('${namePrefixes.virtualNetwork}-${locPrefix}-${lzPrefix}-${envPrefix}')

Network Security Group rules

Suppose you have multiple Bicep files that define their own network security groups (NSG). You have a common set of security rules that must be applied to each NSG, and then you have application-specific rules that must be added.

  • Define two (2) JSON files that includes common security rules that we will apply across the landing zone, spliting in and outbound security rules makes it easier to manage and update each rule set as required. We will have shared rules for ICMP and a default deny rule.
{
  "networkSecurityGroupSecurityRulesInbound": [
    {
      "name": "INBOUND-FROM-virtualNetwork-TO-virtualNetwork-PORT-any-PROT-Icmp-ALLOW",
      "properties": {
        "protocol": "Icmp",
        "sourcePortRange": "*",
        "sourcePortRanges": [],
        "destinationPortRange": "*",
        "destinationPortRanges": [],
        "sourceAddressPrefix": "VirtualNetwork",
        "sourceAddressPrefixes": [],
        "sourceApplicationSecurityGroupIds": [],
        "destinationAddressPrefix": "VirtualNetwork",
        "destinationAddressPrefixes": [],
        "destinationApplicationSecurityGroupIds": [],
        "access": "Allow",
        "priority": 1000,
        "direction": "Inbound",
        "description": "Shared - Allow Outbound ICMP traffic (Port *) from the subnet."
      }
    },
    {
      "name": "INBOUND-FROM-any-TO-any-PORT-any-PROT-any-DENY",
      "properties": {
        "protocol": "*",
        "sourcePortRange": "*",
        "sourcePortRanges": [],
        "destinationPortRange": "*",
        "destinationPortRanges": [],
        "sourceAddressPrefix": "*",
        "sourceAddressPrefixes": [],
        "sourceApplicationSecurityGroupIds": [],
        "destinationAddressPrefix": "*",
        "destinationAddressPrefixes": [],
        "destinationApplicationSecurityGroupIds": [],
        "access": "Deny",
        "priority": 4096,
        "direction": "Inbound",
        "description": "Shared - Deny Inbound traffic (Port *) from the subnet."
      }
    }
  ]
}
{
  "networkSecurityGroupSecurityRulesOutbound": [
    {
      "name": "OUTBOUND-FROM-virtualNetwork-TO-virtualNetwork-PORT-any-PROT-Icmp-ALLOW",
      "properties": {
        "protocol": "Icmp",
        "sourcePortRange": "*",
        "sourcePortRanges": [],
        "destinationPortRange": "*",
        "destinationPortRanges": [],
        "sourceAddressPrefix": "VirtualNetwork",
        "sourceAddressPrefixes": [],
        "sourceApplicationSecurityGroupIds": [],
        "destinationAddressPrefix": "VirtualNetwork",
        "destinationAddressPrefixes": [],
        "destinationApplicationSecurityGroupIds": [],
        "access": "Allow",
        "priority": 1000,
        "direction": "Outbound",
        "description": "Shared - Allow Outbound ICMP traffic (Port *) from the subnet."
      }
    },
    {
      "name": "OUTBOUND-FROM-any-TO-any-PORT-any-PROT-any-DENY",
      "properties": {
        "protocol": "*",
        "sourcePortRange": "*",
        "sourcePortRanges": [],
        "destinationPortRange": "*",
        "destinationPortRanges": [],
        "sourceAddressPrefix": "*",
        "sourceAddressPrefixes": [],
        "sourceApplicationSecurityGroupIds": [],
        "destinationAddressPrefix": "*",
        "destinationAddressPrefixes": [],
        "destinationApplicationSecurityGroupIds": [],
        "access": "Deny",
        "priority": 4096,
        "direction": "Outbound",
        "description": "Shared - Deny Outbound traffic (Port *) from the subnet."
      }
    }
  ]
}
  • In the Bicep file, we will declare variables that imports both the inbound and outbound shared security rules:
var sharedNSGrulesInbound = json(loadTextContent('../../configuration/shared/nsgRulesInbound.json')).networkSecurityGroupSecurityRulesInbound
var sharedNSGrulesOutbound = json(loadTextContent('../../configuration/shared/nsgRulesOutbound.json')).networkSecurityGroupSecurityRulesOutbound
  • When we define the NSG resource, the concat() function is used to combine the various arrays together and set the securityRules property. The subnet.securiutyRules allows us to have custom rules for each subnet.
// Module: Network Security Group
module networkSecurityGroup '../CARML/network/network-security-group/main.bicep' = [for (subnet, i) in subnets: {
  name: 'nsg-${i}'
  scope: resourceGroup()
  params: {
    name: subnet.networkSecurityGroupName
    location: location
    securityRules: concat(sharedNSGrulesInbound, subnet.securityRules, sharedNSGrulesOutbound)
    tags: tags
  }
}]

Considerations

There are a few things to consider when using shared variable files:

  • The shared JSON content will be included inside the ARM template generated by Bicep and will count towards the 4MB size limit for ARM.
  • Ensure shared variables don’t conflict with values specified in the Bicep file. This can happen when you have shared NSG rules and then use Azure Policy or an external parameter file to deploy the same rule.
  • Use separate shared configuration files for different purposes. For example, you might have the following shared configuration files:
    • networkConfig.json - configuration used common network values.
    • storageConfig.json - configuration used common storage values.

Conclusion

Using shared variable files is a great way to reduce the repetition of common values across multiple Bicep files and enables us to control these values from a central location. They provide a lot of value for resource naming, shared NSG rules, shared routes and also virtual machine configurations.

Further Reading

Some further reading on the topics covered in this post: