Subnetting in Azure Bicep

In this blog, I am writing about Bicep CIDR functions and will show a practical example on how to use this. With these CIDR functions subnetting in Bicep has been made easier. We can now create subnets programmatically.

What is CIDR?

CIDR stands for Classless Inter-Domain Routing and it is a method of allocating IP addresses and routing internet protocol packets. CIDR uses prefix a notation to represent IP address blocks. In this notation, an IP address is followed by a forward slash and a number that indicates the length of the network prefix. For example, 192.168.1.0/24 represents a block of IP addresses where the first 24 bits are the network prefix, and the remaining 8 bits are available for host addresses.

Bicep CIDR functions

There are three CIDR functions available:

parseCidr()

Let’s take a look at the first function: parseCidr(). This function parses an IP address range to get various properties of the specified address range. This function expects one parameter, namely: parseCidr(network). The network parameter is the IP address range to parse.

For example, to get information about the following IP address range 192.168.1.0/24 call the parseCidr function:

output outParseCidrInformation object = parseCidr('192.168.1.0/24')
view raw parseCidr.bicep hosted with ❤ by GitHub

Outputs:

"outParseCidrInformation": {
"type": "Object",
"value": {
"broadcast": "192.168.1.255",
"cidr": 24,
"firstUsable": "192.168.1.1",
"lastUsable": "192.168.1.254",
"netmask": "255.255.255.0",
"network": "192.168.1.0"
}
}

cidrSubnet()

The cidrSubnet() function splits the specified IP address range into subnets with a new CIDR value and returns the IP address range of the subnet with the specified index. This function expects three parameters: cidrSubnet(network, cidr, subnetIndex).

The first parameter is the IP address space to split, the second parameter is the CIDR of the subnet and the last parameter is used to output the desired subnet.

output outCidrSubnet string = cidrSubnet('192.168.1.0/24', 25, 0)

Outputs:

"outCidrSubnet": {
"type": "String",
"value": "192.168.1.0/25"
}

The output is the first subnet with CIDR /25 created from the address space. If we changed the subnetIndex from 0 to 1 it would have outputted the second subnet 192.168.1.128/25.

cidrHost()

The cidrHost() function calculates the usable IP addresses of the given address space. of the host with the specified index on the specified IP address range in CIDR notation. This function expects two parameters: cidrHost(network, hostIndex).

The first parameter is the IP address space and the second parameter is to output the desired host. For example, if cidrHost is called with host index 0 using address space 192.168.1.0/24 the output will be 192.168.1.1. If host index 1 is used the output will be 192.168.1.2 and so on.

output outCidrHost array = [for i in range(0, 10): cidrHost('192.168.1.0/24', i)]
view raw cidrHost.bicep hosted with ❤ by GitHub

Outputs

"outCidrHost": {
"type": "Array",
"value": [
"192.168.1.1",
"192.168.1.2",
"192.168.1.3",
"192.168.1.4",
"192.168.1.5",
"192.168.1.6",
"192.168.1.7",
"192.168.1.8",
"192.168.1.9",
"192.168.1.10"
]
}

This function can be valuable, for example, allowing the creation of programmatic whitelists for specific IP address ranges within a network security group or firewall. Notably, it excludes reserved IP addresses from its results.

Subnetting a virtual network with cidrSubnet()

Let’s put the function cidrSubnet() to the test. We aim to create a virtual network with address space 192.168.1.0/24. As we have observed in the parseCidr range output, the /24 prefix can accommodate 256 IP addresses.

To begin, we need to determine the number of subnets required. In this scenario, we’ll be working with 2 subnets, each capable of accommodating 128 addresses, denoted by a CIDR notation of /25. You can use parseCidr to calculate the amount hosts for the subnet, or you can calculate it manually using the following equation: 2^(32-25) = 2^7, which equals 128. There are various methods to calculate subnets, but this is the approach I find most straightforward.

Now let’s create the Bicep file to deploy the virtual network and two subnets with the cidrSubnet() function.

@description('The preferred IP address space for the virtual network. Add the CIDR. Default: 192.168.1.0/24')
param parAddressSpace string = '192.168.1.0/24'
@description('The preferred CIDR for the subnet. Default: 25')
param parSubnetCidr int = 25
@description('The amount of subnets to create. Default: 2')
param parAmountOfSubnets int = 2
var varSubnetCalculations = map(range(0, parAmountOfSubnets), i => {
name: 'subnet-${i}'
properties: {
addressPrefix: cidrSubnet(parAddressSpace, parSubnetCidr, i)
}
})
resource resVirtualNetwork 'microsoft.network/virtualNetworks@2019-11-01' = {
name: 'my-vnet2'
location: 'westeurope'
properties: {
addressSpace: {
addressPrefixes: [
parAddressSpace
]
}
subnets: varSubnetCalculations
}
}

Let’s walk through the provided Bicep code. First, I’ve defined several parameters: parAddressSpace, parSubnetCidr, and parAmountOfSubnets. These parameters serve as inputs that can be customised when deploying the virtual network. For instance, parAddressSpace allows you to specify the preferred IP address space in CIDR notation, while parSubnetCidr lets you define the subnet CIDR prefix length, and parAmountOfSubnets sets the number of subnets to be created.

Following the parameter definitions, I’ve created a variable called varSubnetCalculations. This variable utilises the map function to generate an array of objects, each representing the properties of a subnet expected by the virtual network resource. This approach enhances code readability and simplifies the configuration of multiple subnets within the virtual network. It iterates through the specified number of subnets and computes the corresponding address prefixes using the cidrSubnet function. These subnet property objects are then used in the subnets property of the resVirtualNetwork resource, facilitating the creation of the virtual network with the desired subnets.

For clarity, this is the output of varSubnetCalculations:

[
{
"name": "subnet-0",
"properties": {
"addressPrefix": "192.168.1.0/25"
}
},
{
"name": "subnet-1",
"properties": {
"addressPrefix": "192.168.1.128/25"
}
}
]

If you want to know more about the map() function or other lambda’s check out my “Azure Bicep Lambda Expressions” blog post.

When we deploy the Bicep code to Azure it creates the virtual network including the subnets:

Virtual network my-vnet with the specified address space
Virtual network my-vnet with the created subnets

Conclusion

This is how to leverage the Bicep CIDR functions to create subnets for your Azure networks. In this blog, I used IPv4 addresses, but the CIDR functions also work with IPv6. Subnetting has been made easier with these functions, but you will still need to think about the CIDR for the subnets.

Leave a comment