Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 53

Dynamic Resource Creation with `count` and `for_each`

Recommended resources

Hello! Welcome to the fourth lesson in our module on Advanced Infrastructure as Code with Terraform.

In our last lesson, we established robust strategies for managing multiple environments using directory structures and Terraform workspaces. This allows us to maintain separate, isolated states for environments like dev and prod.

Today, we'll build on that foundation to make the configurations within those environments more dynamic. For instance, how do you specify that dev should have one server, while prod needs ten, without duplicating code? This lesson directly addresses the learning outcome: Use the count and for_each meta-arguments to create multiple resource instances dynamically.

We will start with count, Terraform's original looping construct. We'll see how it works and, more importantly, uncover a critical limitation that can be dangerous in production. Then, we will introduce for_each as the modern, more robust solution to this problem. By the end of this lesson, you'll be able to choose the right tool for the job and write cleaner, safer, and more flexible Terraform code.

1. Creating Multiple Resources with count

Imagine you need to create several identical AWS IAM users or EC2 instances. Your first instinct might be to copy and paste the resource block multiple times, but this violates the "Don't Repeat Yourself" (DRY) principle.

Terraform’s count meta-argument provides a simple way to create a specified number of copies of a resource from a single block.

Terraform 'count' Meta-Argument Illustration
This image shows the basic concept of using the `count` meta-argument to create multiple AWS resources. The `count.index` allows for unique configuration of each instance.

Let's see a practical example. We will define a list of usernames and use count to create an IAM user for each one.

Terraform for loop, for_each loop and count - Part 6

The video 'Terraform for loop, for_each loop and count' provides a clear, practical demonstration of using count to create multiple AWS IAM users. This is a perfect real-world scenario.

Watch from 00:42 to 07:59. Pay close attention to how: A list(string) variable is defined for the usernames. The count argument is set using the length() function on this variable. The count.index is used with array lookup syntax (var.user_names[count.index]) to assign a unique name to each user. The terraform plan and apply workflow results in the creation of three distinct users.

As you saw, the key elements are:

  • count = length(var.user_names): This tells Terraform to create as many resources as there are elements in our list.
  • name = var.user_names[count.index]: count.index provides the current iteration number (0, 1, 2, ...), which we use as an index to access elements in our list.

When you use count, Terraform creates a list of resource objects. You can access an individual user's ARN like this: aws_iam_user.example[0].arn.


2. The Hidden Danger of count

While count is straightforward, it has a significant drawback related to how Terraform identifies the resources it creates. Terraform identifies each resource instance by its index in the list (e.g., [0], [1], [2]).

This becomes a problem if you remove an element from the middle of your list.

Let's say you have this list: ["neo", "trinity", "morpheus"]. Terraform tracks them as:

  • aws_iam_user.example[0] -> "neo"
  • aws_iam_user.example[1] -> "trinity"
  • aws_iam_user.example[2] -> "morpheus"

Now, if you remove "trinity" from your list, it becomes ["neo", "morpheus"]. Terraform's plan will look like this:

  • aws_iam_user.example[0] -> "neo" (no change)
  • aws_iam_user.example[1] -> now "morpheus". Terraform sees this as a change from "trinity" to "morpheus". It will try to update the resource at index 1 in-place, or destroy and recreate it.
  • aws_iam_user.example[2] -> no longer exists. Terraform will destroy the resource that was at index 2 ("morpheus").

The result is unintended and potentially destructive changes to your infrastructure.

Choosing Between Count and For-Each

The video 'Choosing Between Count and For-Each' by Ned in the Cloud has a fantastic demonstration of this exact problem. It clearly shows the unexpected plan output.

Watch from 01:42 to 08:27. This segment first explains how count creates an ordered list of resources accessible by index. Then, it shows what happens when an item is added to the middle of the list, forcing Terraform to replace existing resources unnecessarily. The analogy to Kubernetes clusters makes the danger very clear.

This behavior makes count unsuitable for managing collections of resources where items might be added or removed non-sequentially. For this, we need a more stable way to identify each resource.


3. The Modern Solution: for_each

The for_each meta-argument was introduced in Terraform 0.12 to solve the problems with count. Instead of creating a list of resources identified by an index, for_each creates a map of resources identified by a stable string key.

for_each iterates over a map or a set of strings.

Using for_each with a Set

Let's refactor our IAM user example to use for_each. Since our original variable is a list, we'll use the toset() function to convert it.

variable "user_names" {
  description = "Create IAM users with these names"
  type        = list(string)
  default     = ["neo", "trinity", "morpheus"]
}

resource "aws_iam_user" "example" {



  # for_each iterates over a set or map. We convert our list to a set.
  for_each = toset(var.user_names)




  # each.key and each.value are the same for a set: the string value itself.
  name = each.value
}

Now, if you remove "trinity" from the list, Terraform knows the resource to destroy is the one uniquely identified by the key "trinity". The "morpheus" resource, identified by the key "morpheus", is left untouched.

Terraform tips & tricks: loops, if-statements, and gotchas - Gruntwork

The article 'Terraform tips & tricks' from Gruntwork provides an excellent textual explanation of this improvement.

Read the section 'Loops with for_each expressions'. Focus on the example of creating IAM users and the plan output shown when 'trinity' is removed. It contrasts perfectly with the dangerous plan you saw with count.

Using for_each with a Map

Using a map with for_each is even more powerful, as it lets you provide more complex configuration for each instance. The map's keys are used for resource identification, and the values (which can be objects) contain the configuration data.

variable "ec2_instances" {
  type = map(object({
    instance_type = string
    ami           = string
  }))
  default = {
    "web-server-prod" = {
      instance_type = "t2.micro"
      ami           = "ami-0c55b159cbfafe1f0"
    },
    "db-server-prod" = {
      instance_type = "t4g.small"
      ami           = "ami-0b5eea76982371e95"
    }
  }
}

resource "aws_instance" "server" {
  for_each = var.ec2_instances




  # each.key is the map key ("web-server-prod", "db-server-prod")
  # each.value is the object associated with that key
  
  instance_type = each.value.instance_type
  ami           = each.value.ami

  tags = {
    Name = each.key
  }
}

In this case, for_each creates two aws_instance resources. Their addresses in the state file will be aws_instance.server["web-server-prod"] and aws_instance.server["db-server-prod"].

Terraform for_each: A simple tutorial with Examples - KodeKloud

The KodeKloud article 'Terraform for_each: A simple tutorial with Examples' provides clear, practical code for this map-based approach.

Read the subsections 'Using for_each in resource blocks' and 'Expressions with for_each'. The first shows a great example of using a map of objects to create multiple EC2 instances with different instance types. The second explains how to use a for expression in a locals block to transform a list into a map suitable for for_each, a very powerful pattern.


4. count vs. for_each: Which to Use?

With the advantages of for_each, you might wonder if count is ever useful. It is, but for very specific scenarios.

General Rule: Always prefer for_each over count when iterating over a collection of distinct items.

However, count remains the best tool for two main use cases:

  1. Creating N identical, undifferentiated resources: If you just need three identical web servers in an auto-scaling group and don't care about their individual identity, count = 3 is simple and effective.

  2. Conditional Resource Creation: This is the most common and important use case for count today. You can use a ternary operator to set count to 1 (create the resource) or 0 (do not create the resource).

    variable "create_public_ip" {
      type    = bool
      default = true
    }
    
    resource "aws_eip" "ip" {
    
    
    
      # If var.create_public_ip is true, count is 1. If false, count is 0.
      count = var.create_public_ip ? 1 : 0
      
      instance = aws_instance.my_server.id
    }
    

    This pattern is clean, idiomatic, and much simpler than the equivalent logic with for_each.

Choosing Between Count and For-Each

Let's return to the 'Choosing Between Count and For-Each' video, which concludes by summarizing these exact use cases.

Watch from 13:43 to 16:51. This final segment clearly articulates when count is still the right choice, focusing on simple integer-based creation and, most importantly, conditional resource creation. The side-by-side comparison of conditional logic with count vs. for_each is particularly insightful.


5. Advanced Gotcha: count and for_each Limitations

There is one critical limitation you must be aware of for both count and for_each: The value you provide must be known before Terraform performs any actions.

You cannot use an attribute of a resource that has not yet been created. Terraform needs to know how many resources it's going to create during the plan phase to build the dependency graph. If the count depends on a value computed during the apply phase, Terraform cannot proceed.

Terraform tips & tricks: loops, if-statements, and gotchas - Gruntwork

The Gruntwork article you read earlier has a 'Terraform Gotchas' section that explains this limitation perfectly.

Read the small section titled 'count and for_each Have Limitations'. It shows an example of trying to use a random_integer resource to determine the count, which fails and produces a clear error message explaining the problem.

This is an important constraint to remember as you build more complex, interdependent configurations.


Conclusion

You have now mastered two of the most powerful features for writing dynamic and scalable Terraform code. Understanding the difference between count and for_each is a hallmark of an experienced Terraform user, as it demonstrates a deep understanding of how Terraform manages state and plans changes.

Key Takeaways:

  • count creates a list of resources identified by a numeric index. It is simple but can lead to destructive changes if elements are removed from the middle of a collection.
  • for_each creates a map of resources identified by a stable string key. It is the recommended, safer method for creating multiple resources from a collection (a set or map).
  • Referencing instances: count uses resource.name[index], while for_each uses resource.name["key"].
  • The primary use case for count today is for conditional logic: count = var.enabled ? 1 : 0.

In our next lesson, we will continue building dynamic configurations by learning to use data sources to fetch external information and dynamic blocks to generate repeatable nested configurations. This will allow us to, for example, dynamically create multiple ingress rules within a security group, a perfect complement to what we learned today.