Terraform For Each Examples - How to use for_each | Devops Junction

In this post, we’ll try to cover Terraform looping constructs.

We’ll cover the looping constructs that specifically work at the resource level. They handle resource creation itself.

There are two Looping constructs.

  1. count: This is often brought up when talking about looping with Terraform.
  2. for_each: This technique is similar to the count method but has some advantages. It should be generally used overcount.

The for_each argument will iterate over a data structure to configure resources or modules with each item in turn.

It works best when the duplicate resources need to be configured differently but share the same lifecycle.

It is more like any other for_each in any given programming language.

Terraform for each Simple List Example

Let's look at below simple example of Terraform for each. Here we have used terraform null_resource

Create main.tf file with below contents

locals {
  avengers = ["ironman", "captain america", "thor","doctor strange","spider man","hulk","black panther","black widow"]
}
resource "null_resource" "avengers" {
  for_each = toset(local.avengers)
  triggers = {
    name = each.value
  }
}
output "avengers" {
  value = null_resource.avengers
}
  • The null_resource resource implements the standard resource lifecycle but takes no further action. The triggers argument allows specifying a random set of values.  These values when changed will cause the resource to be replaced.
  • With for_each, we must convert the List type to a Set with toset(local.avengers). Note, we could have also used a variable with type = set(string) instead of using the toset function.
  • There’s a special each object that is assigned by Terraform.  The object has 2 attributes: each.key and .each.value

In the previous example, we pointed out the conversion of the List to a Set with toset(local.avengers). If we did not convert it and used this code instead:

resource "null_resource" "avengers" {
  for_each = local.avengers # instead of toset(local.avengers)
  triggers = {
    name = each.value
  }
}

Terraform would produce an error like this

Error: Invalid for_each argument

  on main.tf line 28, in resource "null_resource" "avengers":
  28:   for_each = local.avengers

The given "for_each" argument value is unsuitable: the "for_each" argument
must be a map, or set of strings, and you have provided a value of type tuple.

As per the above error snippet, it is confirmed that for_each can be assigned  to the following items

  1. a Map
  2. Set of Strings

To get more clarity on this topic, let’s take a look at the difference between a Terraform List and a Set.

set = toset(["w", "x", "x"]) # => ["w", "x"] all elements are unique
list = ["w", "x", "x"] # => ["w", "x", "x"] the elements don't have to be unique

So the difference between a List and a Set is that Set values are all guaranteed to be unique. Also, Sets do not have any particular ordering.

And map structure looks like this:

map = {w = 1, x = 2}

With a map, the key naturally provides uniqueness already.

So Terraform’s for_each type requirement stems from uniqueness.

Each element in the iteration needs to have a unique key. Terraform did this by design. It allows us to reference resources by a unique identifier easily.

Let's apply changes with terraform apply command

You should see output similar to below

Outputs:

avengers = {
  "balck panther" = {
    "id" = "2802565293094280916"
    "triggers" = tomap({
      "name" = "balck panther"
    })
  }
  "black widow" = {
    "id" = "6013354124200750553"
    "triggers" = tomap({
      "name" = "black widow"
    })
  }
  "captain america" = {
    "id" = "5480470924120220280"
    "triggers" = tomap({
      "name" = "captain america"
    })
  }
  "doctor strange" = {
    "id" = "4072771293727751674"
    "triggers" = tomap({
      "name" = "doctor strange"
    })
  }
  "hulk" = {
    "id" = "7390401988508483769"
    "triggers" = tomap({
      "name" = "hulk"
    })
  }
  "ironman" = {
    "id" = "3593363658702079486"
    "triggers" = tomap({
      "name" = "ironman"
    })
  }
  "spider man" = {
    "id" = "3684530120944521232"
    "triggers" = tomap({
      "name" = "spider man"
    })
  }
  "thor" = {
    "id" = "9185261633914444058"
    "triggers" = tomap({
      "name" = "thor"
    })
  }
}

By looking at Terraform output, we can confirm that resulting resources created with the for_each is a Map.

The resulting object is a Map with unique keys that ties it back to the for_each assignment. This is why for_each can only be assigned a Map or a Set of Strings: uniqueness

 

Terraform For Each with Map Example

The recommended way to use a for_each loop is with a Map value. It’s a natural fit since we don’t have to do any toset conversion.

Look at the below snippet.

locals {
  strengths = {
    "Ironman"         = "Artificial Intelligence"
    "Captain America" = "Sheild"
    "Hulk"            = "Muscle Power"
    "Black widow"     = "Martial Arts"
    "Thor"            = "Hammer"
    "Spider Man"      = "Web"
    "Black Panther"   = "Vibranium suit"
  }
}
resource "null_resource" "strengths" {
  for_each = local.strengths
  triggers = {
    name  = each.key
    power = each.value
  }
}
output "strengths" {
  value = null_resource.strengths
}

Here we are listing the name and power of marvel's avengers in map format.

This would result in the below output format

strengths = {
  "Black Panther" = {
    "id" = "6089652037089638193"
    "triggers" = tomap({
      "name" = "Black Panther"
      "power" = "Vibranium suit"
    })
  }
  "Black widow" = {
    "id" = "3853301065318607159"
    "triggers" = tomap({
      "name" = "Black widow"
      "power" = "Martial Arts"
    })
  }
  "Captain America" = {
    "id" = "182468351001110290"
    "triggers" = tomap({
      "name" = "Captain America"
      "power" = "Sheild"
    })
  }
  "Hulk" = {
    "id" = "7283220352459556352"
    "triggers" = tomap({
      "name" = "Hulk"
      "power" = "Muscle Power"
    })
  }
  "Ironman" = {
    "id" = "4223635774718233881"
    "triggers" = tomap({
      "name" = "Ironman"
      "power" = "Artificial Intelligence"
    })
  }
  "Spider Man" = {
    "id" = "3262424659295311455"
    "triggers" = tomap({
      "name" = "Spider Man"
      "power" = "Web"
    })
  }
  "Thor" = {
    "id" = "4876955704767038788"
    "triggers" = tomap({
      "name" = "Thor"
      "power" = "Hammer"
    })
  }
}

 

Terraform For Each Dynamic block example

Here, In this section, We are going to discuss terraform looping construct, the dynamic nested block. The dynamic nested block provides a way to build repeated nested configuration blocks. This construct works at the attribute level.

Take a look at below example

resource "aws_security_group" "foreachusecase" {
  name        = "demo_for_foreach_usecase"
  description = "demo_for_foreach_usecase"

  ingress {
    description = "rule for http"
    from_port   = 80
    to_port     = 80
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }
  ingress {
    description = "rule for ssh"
    from_port   = 22
    to_port     = 22
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }
}

Dynamic nested blocks can be used to assign multiple attributes. Now, the above code can be re-written as below

locals {
  ports = [80, 22]
}
resource "aws_security_group" "foreachusecase" {
  name        = "demo_for_foreach_usecase"
  description = "demo_for_foreach_usecase"

  dynamic "ingress" {
    for_each = local.ports
    content {
      description = "description ${ingress.key}"
      from_port   = ingress.value
      to_port     = ingress.value
      protocol    = "tcp"
      cidr_blocks = ["0.0.0.0/0"]
    }
  }
}

In the above code,

  • The dynamic argument is the original attribute we declared with a configuration block: “ingress”
  • for_each assignment is used.
  • The content block contains the original “ingress” block.
  • Terraform implicitly provides an ingress object. The object name matches the dynamic argument “ingress”.
  • The ingress object is a wrapper iterator object that contains info for each element that was assigned with for_each = local.ports

Dynamic block with map attributes

locals {
  map = {
    "http rule" = {
      port = 80
      cidr_blocks = ["0.0.0.0/0"],
    }
    "ssh rule" = {
      port = 22
      cidr_blocks = ["0.0.0.0/0"],
    }
  }
}
resource "aws_security_group" "foreachusecase" {
  name        = "demo_for_foreach_usecase"
  description = "demo_for_foreach_usecase"

  dynamic "ingress" {
    for_each = local.map
    content {
      description = ingress.key # IE: "demo_for_foreach_usecase"
      from_port   = ingress.value.port
      to_port     = ingress.value.port
      protocol    = "tcp"
      cidr_blocks = ingress.value.cidr_blocks
    }
  }
}
output "map" {
  value = aws_security_group.foreachusecase
}

Take a look at the below table, where each iteration is defined with proper values

  • the ingress object is a wrapper object
  • the ingress.value unravels the wrapper object and contains each element of the map
  • the ingress.key is used because it contains the description (http rule and ssh rule)
Iteration Values
1 ingress.key = "http rule" and ingress.value = {port = 80, cidr_blocks = ["0.0.0.0/0"]
2 ingress.key = "ssh rule" and ingress.value = {port = 22, cidr_blocks = ["0.0.0.0/0"]

 

Terraform For Each General AWS example

In this example, we shall see how we can create an auto-scaling group in AWS using terraform for_each capability.

Let's take a look at below example

terraform{
  required_version = ">=0.12"
}

provider "aws"{
  region = "us-east-1"
}

data "aws_availability_zones" "all"{}

resource "aws_autoscaling_group" "asg" {
  launch_configuration = aws_launch_configuration.alc.id
  availability_zones   = data.aws_availability_zones.all.names

  min_size = 2
  max_size = 2

  # Use for_each to loop over var.custom_tags
  dynamic "tag" {
    for_each = var.custom_tags
    content {
      key                 = tag.key
      value               = tag.value
      propagate_at_launch = true
    }
  }
}

resource "aws_launch_configuration" "alc" {
  image_id        = "ami-07ebfd5b3428b6f4d"
  instance_type   = "t2.nano"

  lifecycle {
    create_before_destroy = true
  }
}

If you look at the resource aws_autoscaling_group  block, we have a dynamic block defined with 'for_each' functionality.

variable 'custom_tag' is assigned to this for_each functionality.

Now, take a look at the below variables.tf file contents

variable "custom_tags" {
  description = "Custom tags to set on the Instances in the ASG"
  type        = map(string)
  default = {
    "AAA" = "King"
    "ZZZ" = "Lion"
  }
}

we have a default map with two keys namely AAA and BBB with values King and Lion respectively.

Now, when we execute main.tf with terraform apply command, terraform will create ASG (Auto Scaling Group) in our AWS account with two tags namely

"AAA" = "King"
"ZZZ" = "Lion"

Let's run below terraform commands to see working in real action

terraform init
terraform validate
terraform plan
terraform apply – auto-approve

you should see output similar to below

Plan: 2 to add, 0 to change, 0 to destroy.
aws_launch_configuration.alc: Creating...
aws_launch_configuration.alc: Creation complete after 5s [id=terraform-20220206141921193000000001]
aws_autoscaling_group.asg: Creating...
aws_autoscaling_group.asg: Still creating... [10s elapsed]
aws_autoscaling_group.asg: Still creating... [20s elapsed]
aws_autoscaling_group.asg: Still creating... [30s elapsed]
aws_autoscaling_group.asg: Still creating... [40s elapsed]
aws_autoscaling_group.asg: Still creating... [50s elapsed]
aws_autoscaling_group.asg: Still creating... [1m0s elapsed]
aws_autoscaling_group.asg: Still creating... [1m10s elapsed]
aws_autoscaling_group.asg: Creation complete after 1m13s [id=terraform-20220206141926079900000002]

Now we can confirm the same by logging into the AWS console. Please see the below screenshot for reference

 

 

 

 

 

 

Terraform count and for_each together.

Sometimes you cannot just use for_each you might need a help of count too.

But count and for_each are mutually exclusive and you cannot use them together but there is a way.

we discussed the same in another article

Terraform Create Multiple EC2 with different Configs – for_each and count together

 

Cheers
Tapan Hegde

Follow me on Linkedin My Profile
Follow DevopsJunction onFacebook orTwitter
For more practical videos and tutorials. Subscribe to our channel

Buy Me a Coffee at ko-fi.com

Signup for Exclusive "Subscriber-only" Content

Loading