Putting Terraform `for_each` Into Practice

Example of deploying complex Azure infrastructure using Terraform for_each

Posted by Adam Mazouz on Wednesday, May 24, 2023
Reading Time: 4 minutes

Photo by mkjr_ on Unsplash

Introduction

Last week, during my time looking into how to rewrite a Terraform example that is written in one monolith main.tf into separate modules. I found myself relearning Terraform and testing all the new features and functionalities HashiCorp is continuously adding. But before I get into how I managed to create those reusable modules, I stumbled upon an issue that I am sure many of you who written Terraform or any infrastructure-as-code (IaC) tools have faced:

  • How to deploy multiple resource of the same type? Ex: EC2 instances, Azure VNet subnets, Monitoring Alerts …etc.
  • How to reduce the number of copy/paste lines in your IaC code?
  • How to reuse the same code for multiple projects? Ex: Prod, Test, Dev .. etc.

Of course, if you are familiar with writing scripts in PowerShell or Python, the answer would be as simple as for loop. So what is the alternative when working with Terraform HCL.

count: Replicating Identical Resources

First thing I have looked into is using count, let me give you an example:

resource "aws_instance" "k8s_workers" {
  count         = 3
  ami           = data.aws_ami.amazon_linux.id
  instance_type = var.instance_type
  #...
}

count is used to manage similar resources or ideally, identical once. As you can see from the example above, if you would like to have multiple Azure VMs or EC2 Instance, Terraform will replicate the given resource or module a specific number of times with an incrementing counter. I would use count to deploy a fleet of VMs that has the same workload or server the same service or workload, ex: K8s worker nodes or Dev/Test SQL VMs.

What if each resource is identical and have different parameters? In this case count won’t be doable, and HCL has another way for looping through variables in data structure rather than using an integer to replicate resources – for_each.

for_each: Looping Through maps and sets

Similar to count, for_each iterates over each item declared in data structure variable and creates one copy of the resource.

Something I learned while testing this. You cannot use both count and for_each in the same block.

Lets go through the example I worked on – Azure VNet Subnets. What I have achived is, I dont need to duplicate the number of resource blocks required in all the three files main.tf, variables.tf, and output.tf. I just modify terrafrom.tfvars with a data structure formate variables. I used map in with VNet Subnets. But you can use set as well, a quick example for set of strings to create AWS IAM User:

resource "aws_iam_user" "the-accounts" {
  for_each = toset( ["Adam", "Kyle", "Nelson", "Cody"] )
  name     = each.key
}

Example: Provisioning Azure Subnets

Below are the to the Azure VNet Subnet example, I am currently using this example in one of the Terraform Modules to deploy Pure Cloud Block Store in Azure with all the required prerequisites. Link to repo.

variables.tf

variable "subnets" {
  type = map(any)
  default = {
   cbs_subnet_mgmt = {
      name             = "cbs_subnet_mgmt"
      address_prefixes = ["10.10.1.0/24"]
    }
    cbs_subnet_iscsi = {
      name             = "cbs_subnet_iscsi"
      address_prefixes = ["10.10.2.0/24"]
    }
    cbs_subnet_repl = {
      name             = "cbs_subnet_repl"
      address_prefixes = ["10.10.3.0/24"]
    }
    cbs_subnet_sys = {
      name             = "cbs_subnet_sys"
      address_prefixes = ["10.10.4.0/24"]
    }
  }
}

In the main.tf I was able to pass the name of each subnet by using each.value["name"]. Also, I have used a conditional argument since I only wanted to configure the cbs_system_subnet with service endpoints.

 = each.value["map.key"] == "map.value" ? ["If yes, do something"] : ["If not, do nothing"]

main.tf

resource "azurerm_subnet" "subnet" {
  for_each             = var.subnets    
  resource_group_name  = var.resource_group_name
  virtual_network_name = azurerm_virtual_network.cbs_virtual_network.name
  name                 = format("%s%s%s", var.resource_group_name, var.resource_group_location, each.value["name"])
  address_prefixes     = each.value["address_prefixes"]
  service_endpoints    = each.value["name"] == "cbs_subnet_sys" ? ["Microsoft.AzureCosmosDB", "Microsoft.KeyVault"] : []
}

output.tf

output "azure_subnet_id" {
    value = {
        for id in keys(var.subnets) : id => azurerm_subnet.subnet[id].id
    }
    description = "Lists the ID's of the subnets"
}

output "azure_subnet_name" {
    value = {
        for id in keys(var.subnets) : id => azurerm_subnet.subnet[id].name
    }
    description = "Lists the Names's of the subnets"
}

Sum’ It up

Terraform, is no doubt a powerful infrastructure as code tool, provides a multitude of features to simplify the provisioning and management of cloud resources. Among these features, the for_each. By Leveraging it I was able to reduce code duplication and improve code maintainability. Hope you would find this useful and it will influence you to script cleaner code and build reusable modules.


comments powered by Disqus