Context - Azure Firewall Policy
Let’s say you want to manage Azure Firewall Policy rules via Terraform, and let’s say the person maintaining (adding, editing, and deleting) those rules is not the Terraform expert.
You can use csv files and Terraform’s dynamic blocks. The goal is to have the operator edit rows in the csv files, and once the deployment pipeline picks up those edits, the dynamic blocs in Terraform dynamic blocs will generate the firewall rules in to apply on your Azure Firewall or Az Fw Policy.
Azure Firewall or Az Fw Policies can have 3 types of rules Network rules, NAT rules and Application rules.
CSVdecode
I’ve used the csvdecode function in the terraform file to go over the 3 csv files. These locals will be used afterwards.
locals {
net_collection_rules = csvdecode(file("${path.module}/network_collection_rules.csv"))
nat_collection_rules = csvdecode(file("${path.module}/nat_collection_rules.csv"))
app_collection_rules = csvdecode(file("${path.module}/application_collection_rules.csv"))
}
Network rules
network_collection_rules.csv
name,source_addresses,destination_ports,destination_addresses,protocols
DNSOne,10.0.0.0/8,53,1.1.1.1/32,Any
DNSTwo,10.0.0.0/8,53,1.0.0.1/32,Any
resource "azurerm_firewall_policy_rule_collection_group" "azfwpolicyrcg" {
name = "policy-rcg"
firewall_policy_id = azurerm_firewall_policy.azfwpolicy.id
priority = 500
network_rule_collection {
name = "network_rule_collection1"
priority = 400
action = "Deny"
dynamic "rule" {
for_each = { for netrule in local.net_collection_rules : netrule.name => netrule }
content {
name = rule.value.name
source_addresses = [rule.value.source_addresses]
destination_ports = [rule.value.destination_ports]
destination_addresses = [rule.value.destination_addresses]
protocols = [rule.value.protocols]
}
}
}
}
This will pickup all the rows from the network rules csv file and create as many Terraform blocks.
NAT rules
Same for NAT rules, but let’s say you have multiple values for source addresses or multiple protocols from the same source addresses. You could have multiple rows for each source address and protocol, but DRY (don’t repeat yourself). Here is how you can include multiple values included in the same cell.
nat_collection_rules.csv
name,source_addresses,destination_ports,destination_address,protocols,translated_address,translated_port
nat_rule_collection1_rule1,"10.0.0.3,10.0.0.2",80,192.168.1.1,"TCP,UDP",192.168.0.1,8080
nat_rule_collection {
name = "nat_rule_collection1"
priority = 300
action = "Dnat"
dynamic "rule" {
for_each = { for natrule in local.nat_collection_rules : natrule.name => natrule }
content {
name = rule.value.name
protocols = split(",", rule.value.protocols)
source_addresses = split(",", rule.value.source_addresses)
destination_address = rule.value.destination_address
destination_ports = [rule.value.destination_ports]
translated_address = rule.value.translated_address
translated_port = tonumber(rule.value.translated_port)
}
}
}
Application rules
For Application rules you can use the same split() terraform function to include multiple values cells. However, it gets tricky with the protocols field, as it has two sub-fields: type and port.
We can use nested terraform dynamic blocks to loop over the rows of the application rules csv files to create many app rules and loop over the protocols cell on each app rule iteration. In the nested block I have used the “:” to split between the type and the port of the protocol. This is how it would look like.
application_collection_rules.csv
name,source_addresses,destination_fqdns,protocols
app_rule_collection1_rule1,"10.0.0.1","*.microsoft.com","Http:80,Https:443"
application_rule_collection {
name = "app_rule_collection1"
priority = 500
action = "Deny"
dynamic "rule" {
for_each = { for apprule in local.app_collection_rules : apprule.name => apprule }
content {
name = rule.value.name
source_addresses = split(",", rule.value.source_addresses)
destination_fqdns = split(",", rule.value.destination_fqdns)
dynamic "protocols" {
for_each = [ for protocols in split(",", rule.value.protocols) : protocols ]
content {
type = tostring(split(":", protocols.value)[0])
port = tostring(split(":", protocols.value)[1])
}
}
}
}
}
Summary
The operator only has to handle the csv files with his editor of choice, probably Excel. without digging in the Terraform configuration files.