Address Repetition using Shared Variable File Patterns
Stephen Tulp
December 8, 2023
7 minutes to read
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 prefixesloadJsonContent()
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 thesecurityRules
property. Thesubnet.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: