Note: Premium video content requires a subscription.

In this post, we’ll cover Terraform looping constructs. Terraform is declarative, so it’s looping structure may seem weird to those used to procedural programming loops. There are a few ways to performing looping with Terraform. We’ll cover the looping constructs that specifically work at the resource level. They handle resource creation itself. There are two of them:

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 over count. We’ll explain why in this post.

## Count Technique: Simple Example

The count technique for looping is commonly brought up due to its simplicity. Here’s an example:

``````resource "null_resource" "simple" {
count = 2
}
output "simple" {
value = null_resource.simple
}
``````

Results in:

``````\$ terraform apply -auto-approve
Outputs:

simple = [
{
"id" = "3420691179710313007"
},
{
"id" = "3938693299857881001"
},
]
``````

It’s pretty easy to grasp. Terraform creates as many of those types of resources as the count value.

Also, note we’re using the null_resource. It’s useful for testing.

## Count Technique: List Example

Here’s an example how to achieve a loop that performs some logic with the count technique:

``````locals {
names = ["bob", "kevin", "stewart"]
}
resource "null_resource" "names" {
count = length(local.names)
triggers = {
name = local.names[count.index]
}
}
output "names" {
value = null_resource.names
}
``````

This results in:

``````\$ terraform apply -auto-approve
Outputs:

names = [
{
"id" = "5944721793170602916"
"triggers" = {
"name" = "bob"
}
},
{
"id" = "2105600370698729010"
"triggers" = {
"name" = "kevin"
}
},
{
"id" = "4326674858695036914"
"triggers" = {
"name" = "stewart"
}
},
]
``````

Let’s point out some things:

1. We’re using the Terraform length function to set the count value.
2. We’re using a special `count.index` variable that Terraform provides. Terrafrom provides this special variable when we use the count technique. It’s magic. We then use this index value to get the name’s actual value from `local.names` List itself.

Overall, this code creates 3 null_resources with the attributes: bob, kevin, and stewart.

## For Each: Simple List Example

Now, let’s redo the last List `count` example, except with `for_each` this time.

``````locals {
minions = ["bob", "kevin", "stewart"]
}
resource "null_resource" "minions" {
for_each = toset(local.minions)
triggers = {
name = each.value
}
}
output "minions" {
value = null_resource.minions
}
``````

Produces:

``````\$ terraform apply
Outputs:

minions = {
"bob" = {
"id" = "5967124853326357661"
"triggers" = {
"name" = "bob"
}
}
"kevin" = {
"id" = "7583543865560916708"
"triggers" = {
"name" = "kevin"
}
}
"stewart" = {
"id" = "2631710640638349221"
"triggers" = {
"name" = "stewart"
}
}
}
``````

We’ve achieved the same looping results with both `count` and `for_each`. Some observations:

• With `for_each`, we must convert the List type to a Set with `toset(local.minions)`. Note, we could have also used a variable with `type = set(string)` instead of using the `toset` function. We’ll discuss the reasons for needing to use a Set type soon.
• There’s a special `each` object that is assigned by Terraform. Again, it’s magic. The object has 2 attributes: `each.key` and `.each.value`
• In the case of a Set type, key and value are the same. Generally, recommend sticking to `each.value` as it feels more natural to understand.

The nice thing about the `for_each` over the `count` approach is that we directly access the List values. We no longer have to look it up. IE: `local.names[count.index]` vs `each.value`.

## For Each Argument Requirements

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

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

Terraform produces an error like this:

``````Error: Invalid for_each argument

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

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.
``````

Terraform tells us that `for_each` must be assigned only 1) a map or 2) set of strings.

To help understand why this is the case, let’s take a look at the difference between a Terraform List and a Set.

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

So the difference between a List and 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 = {a = 1, b = 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 look closely at the `minions` output, which shows all the `null_resource` items created.

``````\$ terraform apply
Outputs:

minions = {
"bob" = {
"id" = "5967124853326357661"
"triggers" = {
"name" = "bob"
}
}
"kevin" = {
"id" = "7583543865560916708"
"triggers" = {
"name" = "kevin"
}
}
"stewart" = {
"id" = "2631710640638349221"
"triggers" = {
"name" = "stewart"
}
}
}
``````

The resulting resources created with the `for_each` is a Map. We can tell by the surrounding `{...}` curly brackets vs the `[...]` square brackets. This is big difference from when we used `count`. The `count` technique resulted in a List. The `for_each` technique results in 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.

## For Each: 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. It also nicely reduces mental overhead.

``````locals {
heights = {
bob     = "short"
kevin   = "tall"
stewart = "medium"
}
}
resource "null_resource" "heights" {
for_each = local.heights
triggers = {
name   = each.key
height = each.value
}
}
output "heights" {
value = null_resource.heights
}
``````

Results in:

``````\$ terraform apply
Outputs:

heights = {
"bob" = {
"id" = "3741889141135951736"
"triggers" = {
"height" = "short"
"name" = "bob"
}
}
"kevin" = {
"id" = "4411119930138579807"
"triggers" = {
"height" = "tall"
"name" = "kevin"
}
}
"stewart" = {
"id" = "2041734924508225405"
"triggers" = {
"height" = "medium"
"name" = "stewart"
}
}
}
``````

The usage of both `each.key` and `each.value` is natural and easier to understand.

At the beginning, we mentioned that `for_each` should generally be used over `count` because it provides an advantage. That advantage has to do with what happens when we update the code.

### The count Problem

Let’s say we have a `count` loop that creates 2 resources: bob and stewart.

``````locals {
names = ["bob", "stewart"]
}
resource "null_resource" "names" {
count = length(local.names)
triggers = {
name = local.names[count.index]
}
}
``````

First, we apply and create the resources. Next, we add kevin to the list of names.

``````locals {
names = ["bob", "kevin", "stewart"]
}
``````

Then, when we apply again, we expect that only kevin gets added and nothing else is affected. However, something else unexpected happens:

``````\$ terraform plan
Terraform will perform the following actions:

# null_resource.names[1] must be replaced
-/+ resource "null_resource" "names" {
~ id       = "756389024251013718" -> (known after apply)
~ triggers = { # forces replacement
~ "name" = "stewart" -> "kevin"
}
}

# null_resource.names[2] will be created
+ resource "null_resource" "names" {
+ id       = (known after apply)
+ triggers = {
+ "name" = "stewart"
}
}

Plan: 2 to add, 0 to change, 1 to destroy.
``````

Instead of just adding kevin and leaving the current resources untouched, Terraform wants to replace stewart with kevin and then recreate stewart again. This is because the index for stewart has changed. The key learning point is that stewart’s unique identifier is associated with the List index.

``````names = ["bob", "stewart"]           # stewart index was 1
names = ["bob", "kevin", "stewart"]  # stewart index is now 2
``````

Luckily, this is only a `null_resource` for testing, so there’s no harm done here. If this was a database or an EC2 instance, then the resource would be deleted and recreated. Putting it kindly, it’s undesirable behavior.

### The for_each Solution

The `for_each` technique avoids this deletion behavior since the resource unique identifier remains the same. For example if we start with:

``````locals {
heights = {
bob     = "short"
stewart = "medium"
}
}
resource "null_resource" "heights" {
for_each = local.heights
triggers = {
name   = each.key
height = each.value
}
}
output "heights" {
value = null_resource.heights
}
``````

We apply and create the resources. Then we add kevin to the heights Map.

``````locals {
heights = {
bob     = "short"
kevin   = "tall"
stewart = "medium"
}
}
``````

The plan looks like this:

``````\$ terraform plan
Terraform will perform the following actions:

# null_resource.heights["kevin"] will be created
+ resource "null_resource" "heights" {
+ id       = (known after apply)
+ triggers = {
+ "height" = "tall"
+ "name"   = "kevin"
}
}

Plan: 1 to add, 0 to change, 0 to destroy.
``````

This is a significant improvement. The kevin null_resource will be added, and everything else left is untouched.

## Summary

In this post, we covered 2 Terraform looping constructs: `count` and `for_each`. We provided some examples and explained why generally, you should prefer the `for_each` technique over the `count` approach. In these introductory examples, we assign only one attribute value to the resource for each iteration of the loop. Next, we’ll cover how to assign multiple attributes per iteration. We’ll also cover the dynamic nested block looping construct.

The source code for the examples is available at: terraform-hcl-tutorials/4-loops-count-for-each

Want It to be Easier to Work with Terraform?

Check out Terraspace: The Terraform Framework.