Dropping a quick reference here on some specific use cases for Terraform syntax.
Scenario #1
I want to build an Azure Route Table. It is going to contain 1 or more routes, but those are dependent upon the implementation; one may have 1 or 2, another may have more or even zero.
A normal route table in Terraform would look like this:
resource "azurerm_route_table" "test-routetable" { name = "testroutes" location = var.location resource_group_name = var.resourcegroupname disable_bgp_route_propagation = false route { name = "clientsubnet_to_nva" address_prefix = var.networkipaddress["clientsubnet"] # This is a map variable next_hop_type = "VirtualAppliance" next_hop_in_ip_address = local.nva-ge3_ip # a local that populates the ip of my network virtual appliance } } |
The intention here is that anything matching a specific subnet gets routed through a network virtual appliance to do stuff with (scan, forward, etc).
With Terraform, you can put the routes inside the route table resource, or create them as independent resources and link them to the route table resource.
To enable the ‘variable’ nature of my routes, I created a new variable:
variable "clientnetworks" { type = list(map(string)) default = [] } |
You can see this is a list of maps containing string values. This lets me supply input that looks like this:
clientnetworks = [ # Values must follow CIDR notation, so /32 or /27 or /24 or something { name = "Clientsubnet1" # Name will be the route name, no spaces value = "10.1.1.0/24" } , { name = "Clientsubnet2" value = "10.1.2.0/24" } ] |
Now I need to make my ‘route’ block dynamic within the route table. For this, I use dynamic blocks with a foreach expression:
resource "azurerm_route_table" "test-routetable" { name = "testroutes" location = var.location resource_group_name = var.resourcegroupname disable_bgp_route_propagation = false # For each item in the list of this variable map, we create a route dynamic "route" { for_each = var.clientnetworks content { name = route.value["name"] address_prefix = route.value["value"] next_hop_type = "VirtualAppliance" next_hop_in_ip_address = local.nva-ge3_ip # a local that populates the ip of my network virtual appliance } } } |
This is saying, “for each item in the clientnetworks variable, create a “route” block within my route table resource, and set it’s contents based upon the values found within the instance of the map variable element.
This achieves my goal, where I can populate the input variable with different contents for each implementation, and yet my resource declaration can stay consistent.
Scenario #2
I need to create network security group rules for the list of map values referenced in Scenario #1. This may be a single value, or multiple items, and I want them all contained within a single NSG rule.
I learned about Terraform Console today which really helped in testing and understanding the correct syntax to use here.
If my input looks like this:
clientnetworks = [ # Values must follow CIDR notation, so /32 or /27 or /24 or something { name = "Clientsubnet1" # Name will be the route name, no spaces value = "10.1.1.0/24" } , { name = "Clientsubnet2" value = "10.1.2.0/24" } ] |
Then what I want to achieve is a list containing each “value” from each map in my variable list. In effect, “[10.1.1.0/24,10.1.2.0/24]”.
This is done using a “splat” expression, as identified in the Terraform docs.
I can use the following syntax from my variable:
var.clientnetworks[*].value
Putting this into an NSG rule resource, remembering to set plurality on “destination_address_prefixes”:
resource "azurerm_network_security_rule" "any_clientnetwork_any_mgmtnsg" { resource_group_name = var.resourcegroupname name = "any_clientnetwork_any" priority = 1300 direction = "Outbound" access = "Allow" protocol = "*" source_port_range = "*" destination_port_range = "*" source_address_prefix = "*" destination_address_prefixes = var.clientnetworks[*].value network_security_group_name = azurerm_network_security_group.mgmt-nsg.name description = "This allows outbound to client networks" } |
Last Note
I have tested these scenarios when the input variable exists but is empty, and not good things happen. With the route, if it exists and then I empty the variable, terraform won’t remove the route. But if it is already empty, then the dynamic block evaluates as empty and doesn’t create a route.
For the NSG, Terraform happily passes a validate and a plan, but when applying Azure comes back with an error because it cannot create the resource when the destination prefix is empty.
I could create some conditional logic within that property line to check for when the variable is empty, however I’m already using conditional logic from a different variable for the resource as a whole:
count = var.clientUsingVPN == true ? 1 : 0 # If client is using a VPN, we need this rule