Dynamic Terraform Configurations with try and for_each Functions

The try function combined with for_each in Terraform offers a great approach to handling multiple variations in data structures within Terraform. In this blog post, we will look at using both these features to develop more resilient and adaptable Terraform configurations and will also include an example of this usage

Quick overview of try and for_each

  • try Function: Allows you to attempt to evaluate a list of expressions and returns the first expression that does not produce an error. It’s particularly useful in handling optional attributes that may or may not be present in your data structures – read further here
  • for_each Argument: Used to iterate over usually a map or set of strings, for example: creating one instance of a Terraform resource for each time – read further here

Combining both.. what are the benefits?

  1. Increasing configuration flexibility – Certainly a huge benefit! Combining both will make your terraform configurations more dynamic and even more adaptable to various scenarios that you will use Terraform for. As I mentioned initially – even more multiple variations within your data structures
  2. Simplification of terraform code – Your Terraform configurations will be more adaptable and somewhat dynamic to multiple scenarios without requiring additional changes to your terraform code
  3. Better error handling – Preventing your Terraform from failing due to missing optional attributes or variations in data structures

An example of using both

Consider the scenario where you want to deploy a Network Security Group (NSG) ruleset in Terraform, these rules will certainly vary from specific IP address, multiple IP destinations, ports or port ranges – so many variations are possible! Ofcourse in each rule you cannot define all as your Terraform configuration will fail – a number of these arguments are optional and this is where using the try function within for_each in Terraform becomes so useful!

azurerm_network_security_rule can be found here that shows the number of optional values available

Terraform locals

The locals block is where you define the network security rules. Each rule is a map with key-value pairs representing the rule’s properties. Mandatory arguments include name, priority, direction, access, protocol and the rest are optional properties such as (only a few of the available options, check the linked Terraform doc for more properties) :

  • source_port_ranges – (Optional) List of source ports or port ranges. This is required if source_port_range is not specified.
  • destination_port_range – (Optional) Destination Port or Range. Integer or range between 0 and 65535 or * to match any. This is required if destination_port_ranges is not specified.

  • destination_port_ranges
     – (Optional) List of destination ports or port ranges. This is required if destination_port_range is not specified.
  • source_address_prefix – (Optional) CIDR or source IP range or * to match any IP. Tags such as VirtualNetworkAzureLoadBalancer and Internet can also be used. This is required if source_address_prefixes is not specified.
  • source_address_prefixes – (Optional) List of source address prefixes. Tags may not be used. This is required if source_address_prefix is not specified.
locals {
  nsgrules = {
    rdp = {
      name                       = "rdp"
      priority                   = 100
      direction                  = "Inbound"
      access                     = "Allow"
      protocol                   = "Tcp"
      source_port_range          = "*"
      destination_port_range     = "3389"
      source_address_prefix      = "VirtualNetwork"
      destination_address_prefix = "*"
    },
    AD = {
      name                         = "AD"
      priority                     = 110
      direction                    = "Inbound"
      access                       = "Allow"
      protocol                     = "Tcp"
      source_port_range            = "*"
      destination_port_ranges      = [53,389,636]
      source_address_prefixes      = ["10.100.52.39/32", "10.100.52.49/32"]
      destination_address_prefixes = ["10.100.52.10/32", "10.100.62.10/32"]
    }
  }
}

Create Network Security Group (NSG) and Resource Group

Prior to deploying any NSG rules, we need a NSG and Azure resource group to be created:

resource "azurerm_resource_group" "tamopsrg" {
  name     = "tamopsrg"
  location = "uksouth"
}

resource "azurerm_network_security_group" "tamopsnsg" {
  name                = "tamopsnsg"
  location            = azurerm_resource_group.example.location
  resource_group_name = azurerm_resource_group.example.name
}

NSG rule creation using Terraform

The azurerm_network_security_rule resource is where the for_each loop and try function are used. The for_each loop iterates over each entry in the locals.nsgrules map, creating a security rule for each

The try function is used for optional attributes like source_address_prefix(es) and destination_address_prefix(es). If these attributes are not present in the rule definition, try returns null, effectively omitting them from the resource definition.

resource "azurerm_network_security_rule" "testrules" {
  for_each = local.nsgrules

  name                   = each.value.name
  priority               = each.value.priority
  direction              = each.value.direction
  access                 = each.value.access
  protocol               = each.value.protocol
  source_port_range      = each.value.source_port_range
  destination_port_range = each.value.destination_port_range

  source_address_prefixes      = try(each.value.source_address_prefixes, null)
  source_address_prefix        = try(each.value.source_address_prefix, null)
  destination_address_prefixes = try(each.value.destination_address_prefixes, null)
  destination_address_prefix   = try(each.value.destination_address_prefix, null)

  resource_group_name         = azurerm_resource_group.tamopsrg.name
  network_security_group_name = azurerm_network_security_group.tamopsnsg.name
}

Please note: Azure NSG rules are processed in order of priority. Ensure that your rules have unique priorities and are ordered

Some additional thoughts to finish

  • Use try Sparingly: While try is powerful, overuse can mask errors that should be addressed directly. Use it when you genuinely expect and can safely handle missing data or errors
  • Clear Fallback Values: Ensure that the fallback values provided to try do not inadvertently introduce incorrect configurations. In many cases, null is appropriate, but this may vary depending on the context
  • Document Your Use of try: When using try, especially in complex configurations, document its purpose and the scenarios you’re addressing. This will help maintain clarity for yourself and others.

The combination of both try and each is truly awesome! 🙂 and offers a great solution to deal with variability and potential errors in your Terraform configurations.

GitHub repository containing examples used above

Leave a Reply

Discover more from Thomas Thornton Blog

Subscribe now to keep reading and get access to the full archive.

Continue reading

Discover more from Thomas Thornton Blog

Subscribe now to keep reading and get access to the full archive.

Continue reading