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.