Use for_each over count for flexibility

Learn when and how to use for_each instead of count in Terraform for more flexible and maintainable infrastructure. This lesson explains the core concept, provides a hands-on walkthrough, and compares options to help you choose the right approach.

Focus: use for_each over count for flexibility

Sponsored

Stuck managing a list of similar resources with count? You've probably hit the wall: change the order of your list and Terraform wants to destroy and recreate everything. Or you need to remove one item from the middle and watch your state file get torn apart. This is the classic count pain point. In this lesson, you'll learn why using for_each is the more flexible and maintainable choice for most scenarios, and you'll see exactly how to make the switch.

The problem this lesson solves

When you use count to create multiple resources, Terraform treats them as an ordered list. This seems simple at first, but it creates a hidden dependency on the order of your input. Let's say you have a variable that's a list of subnets:

variable "subnets" {
  type    = list(string)
  default = ["10.0.1.0/24", "10.0.2.0/24", "10.0.3.0/24"]
}

resource "aws_subnet" "main" {
  count = length(var.subnets)
  cidr_block = var.subnets[count.index]
  vpc_id = aws_vpc.main.id
}

Now imagine you need to reorder that list — maybe you want "10.0.2.0/24" first for some reason. Because count uses index numbers, Terraform now thinks the subnet at index 0 (which was 10.0.1.0/24) is the same resource as the one now at index 0 (10.0.2.0/24). The subnet's CIDR block would be updated in place, but if anything else depends on those subnet IDs, you could get unexpected updates or even destruction. Worse, if you remove the second item, the subnet at index 0 and index 1 get renumbered, causing Terraform to plan a destroy and recreate of resources that didn't actually change.

This problem only gets worse as your infrastructure grows. Managing a handful of resources with count is fine, but for anything beyond a static, ordered list, count becomes a maintenance nightmare.

Core concept / mental model

Think of count as numbered parking spots — each spot is identified by its position in the row. If you move a car (resource) to a different spot, it's a different spot, and the old spot might get reused. In contrast, for_each is like named parking spots — each spot has a unique name, like "Spot-A" or "Spot-B" — and moving things around doesn't change which spot is which.

In Terraform terms:

  • count creates resources indexed by an integer: [0, 1, 2, ...]
  • for_each creates resources keyed by a unique string or set: { "key1" = ..., "key2" = ... }

These keys are the identity of each resource. With for_each, Terraform tracks each resource by its key, so even if you reorder or remove items from your input map or set, Terraform only touches the affected resources.

This mental model is crucial: the key is the identity. When you use for_each, you are telling Terraform, "This resource is named 'subnet-a' and should be managed as such, regardless of its position in a list."

How it works step by step

Here's how you transition from count to for_each:

  1. Change your input from a list to a map or set. for_each accepts a map or a set of strings. If you have a list, you can convert it to a set with toset() or to a map with { for idx, val in list : val => val } — but prefer a map for clarity.
  2. Replace count = length(...) with for_each = var.subnets.
  3. Replace count.index with each.key (and each.value). In a map, each.key is the map key, and each.value is the value. In a set, each.key and each.value are the same.
  4. Reference resources using the key, not the index. Instead of aws_subnet.main[0].id, you use aws_subnet.main["10.0.1.0/24"].id or values(aws_subnet.main)[*].id.
  5. Update any references in your configuration. This includes outputs, other resources, and module calls.

Let's see this in action.

Hands-on walkthrough

Let's rewrite the subnet example with for_each. We'll use a map to give each subnet a meaningful name.

variable "subnets" {
  type = map(string)
  default = {
    "subnet-a" = "10.0.1.0/24"
    "subnet-b" = "10.0.2.0/24"
    "subnet-c" = "10.0.3.0/24"
  }
}

resource "aws_subnet" "main" {
  for_each = var.subnets
  cidr_block = each.value
  vpc_id = aws_vpc.main.id
  tags = {
    Name = each.key
  }
}

Notice how we use each.key for the tag — that's the flexible part. If you later change the map's keys, Terraform will rename the tags appropriately, but it won't destroy and recreate subnets unless the value (CIDR block) changes.

Now, let's see how to reference these resources in outputs:

output "subnet_ids" {
  value = { for k, v in aws_subnet.main : k => v.id }
}

output "all_subnet_ids" {
  value = values(aws_subnet.main)[*].id
}

The first output gives you a map of key-to-ID, which is super handy for passing to other modules or for use in loops. The second gives you a list of all IDs, regardless of key.

Let's also show how to conditionally create resources with for_each using a set — a very common pattern:

variable "enable_metrics" {
  type    = bool
  default = true
}

resource "aws_cloudwatch_metric_alarm" "high_cpu" {
  for_each = var.enable_metrics ? toset(["cpu"]) : toset([])
  alarm_name          = "high-cpu"
  comparison_operator = "GreaterThanThreshold"
  evaluation_periods  = "2"
  metric_name         = "CPUUtilization"
  namespace           = "AWS/EC2"
  period              = "120"
  statistic           = "Average"
  threshold           = "80"
  alarm_description   = "This metric monitors ec2 cpu utilization"
}

Here, if enable_metrics is true, the for_each evaluates to a set with one key "cpu", creating the alarm. If false, it's an empty set, creating nothing. This pattern is much cleaner than the count = var.enable ? 1 : 0 trick and avoids the [0] indexing.

Pro tip: When you use for_each with a set, remember that each.key and each.value are the same. This can be confusing initially, so keep that in mind.

Compare options / when to choose what

Scenario count for_each
Static, ordered list of identical resources (e.g., three identical VMs) ✅ Clean and idiomatic ✅ Also works but adds key complexity
Multiple resources with distinct attributes (e.g., subnets with different CIDRs) ❌ Fragile — order matters ✅ Natural — key is the identity
Removing or reordering items ❌ Destroys/recreates ✅ Only touches affected resources
Conditional creation based on a bool count = var.enable ? 1 : 0 but needs [0] toset() trick works cleanly
Passing arguments to modules (module for_each) ✅ Works, but module instances are indexed ✅ Better — instances keyed by name

When to choose count:

  • You have a simple list of identical resources and you don't expect to reorder or remove items.
  • You need to reference resources by index (e.g., [0] for the first) and that's semantically meaningful.
  • You're dealing with a list that is genuinely ordered and positional (like nodes in a cluster, where index 0 is the primary).

When to choose for_each:

  • You have a map or set of resources that are identified by a name or key.
  • You want to reorder or remove items without causing churn.
  • You need conditional creation in a clean way.
  • You need to pass per-resource configuration easily.

In general, for_each is the more flexible choice and is often recommended by the Terraform community for anything beyond simple count scenarios.

Troubleshooting & edge cases

Here are common pitfalls and how to fix them:

  1. Error: "Invalid for_each argument" — The for_each value must be a map or a set of strings. If you pass a list, wrap it with toset(), or convert it to a map with { for idx, val in list : val => val }.
  2. Error: "Missing resource instance key" — You're referencing a resource with for_each without a key, like aws_subnet.main.id. You need to use aws_subnet.main["subnet-a"].id or values(aws_subnet.main)[*].id.
  3. Sensitive values in for_each — If your map keys are sensitive, Terraform may warn about the key being exposed in state. Consider using a non-sensitive key (like subnet-a) and keep the sensitive value in the value.
  4. For_each with resources that have name or tags that must be unique — If you change a key, Terraform might create a new resource with a new physical name, and the old one may remain. Make sure to plan carefully.
  5. Complex values in setsfor_each with a set of complex objects is not allowed; you must use a set of strings. Convert your object list to a set of IDs first.
  6. Empty for_each — If your map or set is empty, Terraform creates zero resources. That's usually what you want, but check for unintended consequences in references that expect at least one instance.

What you learned & what's next

In this lesson, you learned how to use for_each over count for flexibility in Terraform. You now understand the core concept: for_each uses keys to identify resources, making your infrastructure more resilient to changes. You completed a hands-on exercise that transformed a count-based resource into a for_each-based one, and you saw how to reference those resources cleanly.

With this skill, you can confidently manage sets of related resources — subnets, instances, IAM policies, and more — without fearing reordering or deletion. This is a critical step in writing clean, maintainable Terraform.

Your next lesson in the Terraform foundations path will likely dive into modules and how to pass complex inputs — a perfect companion to for_each, since you can use for_each on module blocks too. Keep this lesson in mind; it'll pay off big time when you start building reusable infrastructure components.

Happy Terraforming!

Practice recap

To solidify this lesson, take a Terraform configuration you've written with count and convert it to for_each. Start by changing the variable from a list to a map, then update the resource block and any references. Run terraform plan and observe how the output changes — you should see that reordering the map keys no longer triggers destructive changes. This hands-on practice will make the concept stick.

Common mistakes

  • Using count with a list and then reordering the list causes Terraform to destroy and recreate resources, even if nothing changed — switch to for_each to avoid this.
  • Forgetting to wrap a list in toset() when using for_eachfor_each requires a map or a set of strings, not a list.
  • Referencing a for_each resource without a key, like aws_subnet.main.id — you need aws_subnet.main["subnet-a"].id or values(aws_subnet.main)[*].id.
  • Using count for conditional creation and then referencing [0] — this breaks if the condition is false; prefer the toset([]) trick.

Variations

  1. Use for_each directly on module blocks to create multiple module instances, each with its own key.
  2. Use dynamic "block" with for_each inside a resource to create nested blocks (e.g., multiple ingress rules in a security group).
  3. Combine for_each with flatten() to create resources from a nested structure, like multiple environments and services.

Real-world use cases

  • Managing a set of subnets in a VPC where each subnet has a unique CIDR and tag, and administrators may add or remove subnets without affecting others.
  • Creating IAM policies for multiple services from a map of service name to JSON policy document, allowing independent updates.
  • Rolling out EC2 instances for different environments (dev, staging, prod) using a map keyed by environment, enabling per-environment configuration.

Key takeaways

  • for_each uses keys to identify resources, providing flexibility and stability compared to count's index-based approach.
  • Use for_each when you have a map or set of resources that are semantically named or need to be reordered/removed independently.
  • Always reference for_each resources with their key (resource.name[key]) or use values() to get all instances.
  • Favor for_each for conditional creation using the toset() trick over count with [0] indexing.
  • Choose count only for static, positional lists of identical resources where index-based access is meaningful.

Sponsored

Sponsored

Discussion

Questions, corrections, and tips help everyone reading this page.

0 comments

Add a comment

Shown publicly with your comment.

Be constructive · max 4,000 characters

No comments yet — start the thread.

Related tutorials, quizzes, and articles for this topic.