What is the difference between count and for_each?
Quick Answer
Count creates multiple resource instances indexed by number (0, 1, 2...), while for_each creates instances indexed by string keys from a map or set. for_each is preferred because removing an item from the middle does not force recreation of subsequent resources, unlike count where index shifts cause unnecessary destroys.
Detailed Answer
Both count and for_each are Terraform meta-arguments for creating multiple instances of a resource from a single block, but they differ fundamentally in how they identify and track instances. Understanding this difference prevents one of the most common Terraform disasters in production.
Think of count like numbered seats in a movie theater — seat 0, seat 1, seat 2. If the person in seat 1 leaves and everyone shifts down, the person who was in seat 2 is now in seat 1, and Terraform thinks they are a different person. Think of for_each like named reserved seats — 'Alice's seat', 'Bob's seat', 'Carol's seat'. If Bob leaves, Alice and Carol keep their seats unchanged.
With count, resources are identified by their numeric index: aws_iam_user.payments_engineers[0], aws_iam_user.payments_engineers[1], etc. When you define count = length(var.engineer_names) and the list is ["alice", "bob", "carol"], index 0 is Alice, 1 is Bob, 2 is Carol. If you remove Bob from the middle of the list, the new list becomes ["alice", "carol"]. Now index 1 maps to Carol instead of Bob. Terraform sees that index 1 changed from Bob to Carol and index 2 disappeared, so it plans to update index 1 (changing Bob's user to Carol's) and destroy index 2 (Carol's original user). In production, this means Carol's IAM user gets destroyed and recreated, losing all her attached policies, access keys, and MFA configuration.
With for_each, resources are identified by string keys: aws_iam_user.payments_engineers["alice"], aws_iam_user.payments_engineers["bob"], aws_iam_user.payments_engineers["carol"]. When you remove Bob, Terraform only destroys aws_iam_user.payments_engineers["bob"] — Alice and Carol are untouched because their keys have not changed. This is dramatically safer and more predictable.
for_each accepts either a map or a set of strings. When you pass a map, each.key gives you the map key and each.value gives you the map value, allowing you to associate configuration data with each instance. When you pass a set, each.key and each.value are both the set element. You cannot pass a list directly to for_each — you must convert it to a set with toset() because for_each requires unique keys.
There are still valid use cases for count. The most common is the conditional resource pattern: count = var.enable_monitoring ? 1 : 0 creates the resource only when the variable is true. This is clean, readable, and does not suffer from the index-shift problem because there is only one instance. count is also appropriate when creating a fixed number of identical resources where order does not matter and the count will never change — for example, three NAT gateways, one per availability zone, created in a predictable order.
A subtle gotcha with for_each is that the keys must be known at plan time. You cannot use for_each with a map whose keys come from a resource that has not been created yet — Terraform needs to know all the instance keys to build the dependency graph. If you hit this limitation, you may need to restructure your code or use a targeted apply to create the dependency first.
In modern Terraform, the best practice is to default to for_each for collections and reserve count for boolean conditionals. This prevents the index-shift problem and makes your code more self-documenting since each instance has a meaningful name instead of a number.