Terraform “for_each” loops

Terraform is a great tool for writing Infrastructure as Code. However, there are times that it can be frustrating, especially when using loops to generate multiple resources, and then generating something like an output resource. Add in maps and custom objects and you have the makings of this:

In this article I am going to take you through the process of provisioning multiple resources using a complex map object and then generating an output that loops through all items and generates a custom map.

As always, this is just one way of doing things and I encourage you to do what works for you.

Scenario: Provision an Azure Load Balancer with multiple rules.

In this scenario we will provision an Azure Load Balancer as a resource. We will then provision 2 rules for the Load Balancer.

Step 1. Directory structure

Create the following file structure:

azure_lb
├── input.tfvars
├── load-balancer.tf
├── meta.tf
├── outputs.tf
└── variables.tf

Step 2. The meta.tf file

This file is simply metadata that I like to configure.

terraform {
  required_version = ">=0.12.29"
}

provider "azurerm" {
  features {}
  version = "=2.20.0"
}

data "azurerm_subnet" "this" {
  name                 = var.subnet_name
  resource_group_name  = var.resource_group_name
  virtual_network_name = var.vnet_name
}

The data block is used to get the Subnet ID which will be used later in the code.

Step 3. The variables.tf file

Add the following variables:

variable "resource_group_name" {
  description = "The name of the resource group."
  type        = string
}

variable "subnet_name" {
  description = "The subnet name."
  type        = string
}

variable "vnet_name" {
  description = "The Vnet name."
  type        = string
}

variable "lb_rules" {
  description = "List of Load Balancer rules."
  type        = map(object({
    name          = string
    frontend_port = number
    backend_port  = number
    protocol      = string
  }))
}

Let’s look at the lb_rules variable. So this is a map of objects. Here is an example on how we can create multiple lb_rules objects:

lb_rules = {
  "rule1" = {
    name          = "rule_1"
    frontend_port = 80
    backend_port  = 80
    protocol      = "Tcp"
  },
  "rule2" = {
    name          = "rule_2"
    frontend_port = 443
    backend_port  = 443
    protocol      = "Tcp"
  }
}

In the above example, rule_1 and rule_2 are two list items of type map.

Step 4. The load-balancer.tf file

Add the following code to this file:

resource "azurerm_lb" "this" {
  location            = "Australia East"
  name                = "lb-test"
  resource_group_name = var.resource_group_name

  frontend_ip_configuration {
    name      = "lb-test-ip-config"
    subnet_id = data.azurerm_subnet.this.id
    private_ip_address_allocation = "Dynamic"
  }
}

resource "azurerm_lb_rule" "this" {
  for_each = var.lb_rules

  backend_port        = each.value.backend_port
  frontend_port       = each.value.frontend_port
  name                = each.value.name
  protocol            = each.value.protocol
  loadbalancer_id     = azurerm_lb.this.id
  resource_group_name = var.resource_group_name
  frontend_ip_configuration_name = azurerm_lb.this.frontend_ip_configuration[0].name
}

The azurerm_lb block is fairly straightforward. It creates the Load Balancer resource, gives it a Front-end IP Configuration and provisions it in the variable defined Resource Group.

The azurerm_lb_rule block is where we are going to use the for_each statement to create the Load Balancer rules.

The for_each loop will iterate over “rule_1” and “rule_2” items. We then reference the map values using each.value.<key>.

Step 5. The outputs.tf file

I want to show how we can only output specific items from a block. Copy the following bit of code into the file:

output "lb_rules_map" {
  value = [for lb in azurerm_lb_rule.this : map("rule_name", lb.name, "rule_id", lb.id)]
}

output "lb_rules_all" {
  value = azurerm_lb_rule.this
}

The lb_rules_all output will display all information for each Load Balancer rule, which may not be always ideal.

In order to obtain specific information, we can use a for loop to iterate over the multiple azurerm_lb_rule resources and create a custom map as your output values. In this case we have created the following lb_rules_map output:

lb_rules_map = [
  {
    "rule_id" = "/subscriptions/blah-blah-blah/resourceGroups/azure-test/providers/Microsoft.Network/loadBalancers/lb-test/loadBalancingRules/rule_1"
    "rule_name" = "rule_1"
  },
  {
    "rule_id" = "/subscriptions/blah-blah-blah/resourceGroups/azure-test/providers/Microsoft.Network/loadBalancers/lb-test/loadBalancingRules/rule_2"
    "rule_name" = "rule_2"
  },
]

Step 6. The input.tfvars file

Update the parameters as per your requirements. I am assuming that Resource Group, Subnet and Vnets have already been provisioned.

resource_group_name = "azure-test"
subnet_name         = "subnet-1"
vnet_name           = "azure-test-vnet"

lb_rules = {
  "rule1" = {
    name          = "rule_1"
    frontend_port = 80
    backend_port  = 80
    protocol      = "Tcp"
  },
  "rule2" = {
    name          = "rule_2"
    frontend_port = 443
    backend_port  = 443
    protocol      = "Tcp"
  }
}

And as always, happy Terraforming.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.