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.
- count: This is often brought up when talking about looping with Terraform.
- 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. Thetriggers
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 withtoset(local.avengers)
. Note, we could have also used a variable withtype = set(string)
instead of using thetoset
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
- a Map
- 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”
- A
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 withfor_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
Signup for Exclusive "Subscriber-only" Content