Skip to main content

Handling Ip Ranges in Azure Terraform

· 4 min read

When writing Terraform code for Azure resources, you'll quickly realise there are subtle differences in the API implementations for different resource types.

Generally this is not an issue and you can follow the docs to understand how each one is implemented. However, when it comes to setting up network rules/allow lists this can be a massive pain. Especially when you have to pass ip ranges in various different forms.

Let me show you some examples.

What am I on about?

Let's say for example, we have the following CIDR ranges in our Terraform code that we wish to grant access to our Azure resources.

locals {
my_ip_ranges = [
"17.0.0.0/8", # All of the Apples
"103.21.244.0/22", # Cloudflare
"104.16.0.0/13", # Cloudflare2
"1.1.1.1/32", # Cloudflare3
]
}

Firstly, I'd say don't allow these ip addresses access your Azure resources, but this is just an example. Below are a few ways we have to specify this in terraform code

Azure Kubernetes Cluster

When creating an AKS cluster, you can specify a list of public ip addresses access to the control plane, for things like kubectl. This is nice and simple as we can pass our list in without modifications

# abbreviated properties for example
resource "azurerm_kubernetes_cluster" "example" {
name = "my-aks-cluster"
location = "westeurope"
resource_group_name = "my-amazing-resource-group"

api_server_authorized_ip_ranges = local.my_ip_ranges
}

Simple!

Azure Cosmos DB

When creating a Cosmos DB a list of IP addresses must be specified that are comma separated with no spaces. No big deal, we can manipulate our locals list to conform to that.

# abbreviated properties for example
resource "azurerm_cosmosdb_account" "db" {
name = "my-amazing-cosmos-db"
location = "westeurope"
resource_group_name = "my-amazing-resource-group"

ip_range_filter = join(",", local.my_ip_ranges)
}

This will concatenate the list entries into a single string that is joined by a comma. Not too bad.

Storage Account

This one is a bit more tricky, in that it expects CIDR notation for ip addresses greater or equal to /30. This means our single ip addresses in /32 have to be specified as a single ip. For example 1.1.1.1/32 becomes just 1.1.1.1. This requires us to loop through each entry in our list and strip /32 from it.

# abbreviated properties for example
resource "azurerm_storage_account" "example" {
name = "my-storage-account"
resource_group_name = "my-amazing-resource-group"
location = "westeurope"

network_rules {
default_action = "Deny"
ip_rules = [
for ip in local.my_ip_ranges : replace(ip, "/32", "")
]
}

This is a terraform > 0.12 feature and allows us to loop through and run the replace function on each entry. Not optimal, but it meets the requirement for this resource type without having to duplicate our original list in various forms.

SQL Server

Now SQL server is special. It seems to ignore the fact that most/all other Azure APIs understand CIDR notation, but instead it requires you to specify the ip range. With the first ip and last ip you wish to allow access. This requires us to now have to calculate the first and last ip addresses to pass into the resource.

Why

Additionally, the SQL firewall rule is a separate resource for each entry in Terraform.

Fortunately, we have the available math functions in Terraform to be able to calculate what the start and end ip addresses are. Below is how we do this

# abbreviated properties for example
resource "azurerm_mssql_firewall_rule" "rules" {
for_each = local.my_ip_ranges

name = "ip-rule-${each.key}"

start_ip_address = cidrhost(each.value, 0)
end_ip_address = cidrhost(each.value, pow(2, 32 - split("/", each.value)[1]) - 2)
}

This works by doing the following

  • The cidrhost function is used to calculate the first ip address, with 0 being the first host number we wish to use in the CIDR range.
  • For the last ip we need to get the netmask which we use split on the / character to get the number.
  • Next we take that away from 32 (the smallest netmask) and get the remaining number.
  • Next we raise this remaining number to the power of 2 to calculate the number of ip addresses in the CIDR range.
  • Then we take 2 addresses away from this range as these are technically not usable by hosts (255 and 256).
  • We then use the cidrhost function again, but passing in the result of remaining ip addresses in the CIDR range to get the last ip.

There is a much better explanation of the math here

Summary

As you can see, there are many different ways we need to submit ip addresses for allowing access to Azure resources. Ideally we should avoid using Ip allow lists altogether! Alternatives like Private Link negate the need for allowing access to the public endpoint of Azure resource at all.