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.
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, with0
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
and256
). - 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.