Introduction
When you need to create multiple similar resources in Terraform — three subnets, five EC2 instances, or a security group per environment — you use count or for_each. Both create multiple resource instances, but they work differently and have different strengths.
Quick Comparison
| Feature | count | for_each |
|---|---|---|
| Input | Number | Map or set of strings |
| Reference | count.index (0, 1, 2…) | each.key, each.value |
| Resource address | aws_instance.web[0] | aws_instance.web["app"] |
| Removing middle item | Shifts all subsequent indices | Only removes that item |
| Best for | Identical resources | Resources with unique config |
count: Create N Identical Resources
count takes a number and creates that many instances:
resource "aws_instance" "web" {
count = 3
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t3.micro"
tags = {
Name = "web-${count.index}"
}
}
This creates web-0, web-1, web-2.
Access count resources
# Single instance
output "first_ip" {
value = aws_instance.web[0].public_ip
}
# All instances
output "all_ips" {
value = aws_instance.web[*].public_ip
}
count with variables
variable "instance_count" {
type = number
default = 3
}
resource "aws_instance" "web" {
count = var.instance_count
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t3.micro"
}
Conditional creation with count
Use count = 0 or count = 1 to conditionally create resources:
variable "create_monitoring" {
type = bool
default = true
}
resource "aws_cloudwatch_metric_alarm" "cpu" {
count = var.create_monitoring ? 1 : 0
alarm_name = "high-cpu"
comparison_operator = "GreaterThanThreshold"
evaluation_periods = 2
metric_name = "CPUUtilization"
namespace = "AWS/EC2"
period = 120
statistic = "Average"
threshold = 80
}
# Reference conditional resource (may not exist)
output "alarm_arn" {
value = var.create_monitoring ? aws_cloudwatch_metric_alarm.cpu[0].arn : null
}
count with lists
Use count.index to index into a list:
variable "subnet_cidrs" {
default = ["10.0.1.0/24", "10.0.2.0/24", "10.0.3.0/24"]
}
variable "availability_zones" {
default = ["us-east-1a", "us-east-1b", "us-east-1c"]
}
resource "aws_subnet" "public" {
count = length(var.subnet_cidrs)
vpc_id = aws_vpc.main.id
cidr_block = var.subnet_cidrs[count.index]
availability_zone = var.availability_zones[count.index]
tags = {
Name = "public-${count.index}"
}
}
for_each: Create Resources From a Map or Set
for_each creates one instance per item in a map or set:
resource "aws_instance" "web" {
for_each = toset(["app", "api", "worker"])
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t3.micro"
tags = {
Name = each.value
}
}
This creates aws_instance.web["app"], aws_instance.web["api"], aws_instance.web["worker"].
for_each with a map
variable "instances" {
default = {
app = {
instance_type = "t3.medium"
disk_size = 50
}
api = {
instance_type = "t3.small"
disk_size = 30
}
worker = {
instance_type = "t3.large"
disk_size = 100
}
}
}
resource "aws_instance" "web" {
for_each = var.instances
ami = "ami-0c55b159cbfafe1f0"
instance_type = each.value.instance_type
root_block_device {
volume_size = each.value.disk_size
}
tags = {
Name = each.key
Role = each.key
}
}
Access for_each resources
# Single instance
output "app_ip" {
value = aws_instance.web["app"].public_ip
}
# All IPs as a map
output "all_ips" {
value = { for k, v in aws_instance.web : k => v.public_ip }
}
for_each with a set of strings
variable "users" {
type = set(string)
default = ["alice", "bob", "charlie"]
}
resource "aws_iam_user" "team" {
for_each = var.users
name = each.value
}
The Index Problem: Why for_each Is Safer
This is the biggest difference. With count, resources are identified by index. If you remove an item from the middle, everything shifts:
# Original: count = 3, creates [0], [1], [2]
# Remove the middle server:
# [0] stays, [1] becomes the old [2], old [2] is destroyed
# Terraform destroys and recreates resources!
With for_each, resources are identified by key. Removing an item only affects that item:
# Original: for_each with "app", "api", "worker"
# Remove "api":
# "app" stays, "worker" stays, only "api" is destroyed
# No unnecessary changes!
Rule of thumb: If resources might be added or removed independently, use for_each.
When to Use count vs for_each
Use count when:
- Creating N identical copies (e.g.,
count = 3replicas) - Conditional creation (
count = var.enabled ? 1 : 0) - Resources are truly interchangeable (any can be destroyed/recreated)
Use for_each when:
- Each resource has unique configuration
- Resources are identified by name/key (subnets, users, environments)
- Resources may be added or removed independently
- You need stable resource addresses
Practical Decision Guide
Need N identical copies? → count
Conditional resource? → count (0 or 1)
Each resource has a unique name? → for_each
Resources from a list of names? → for_each (with toset())
Resources from a map of configs? → for_each
Might remove items from the list? → for_each (avoids index shifting)
Real-World Examples
Multi-Environment Setup
variable "environments" {
default = {
dev = {
instance_type = "t3.micro"
min_size = 1
max_size = 2
}
staging = {
instance_type = "t3.small"
min_size = 2
max_size = 4
}
production = {
instance_type = "t3.large"
min_size = 3
max_size = 10
}
}
}
resource "aws_autoscaling_group" "app" {
for_each = var.environments
name = "asg-${each.key}"
min_size = each.value.min_size
max_size = each.value.max_size
desired_capacity = each.value.min_size
tag {
key = "Environment"
value = each.key
propagate_at_launch = true
}
}
Security Group Rules
variable "ingress_rules" {
default = {
http = {
port = 80
cidr_blocks = ["0.0.0.0/0"]
}
https = {
port = 443
cidr_blocks = ["0.0.0.0/0"]
}
ssh = {
port = 22
cidr_blocks = ["10.0.0.0/8"]
}
}
}
resource "aws_security_group_rule" "ingress" {
for_each = var.ingress_rules
type = "ingress"
from_port = each.value.port
to_port = each.value.port
protocol = "tcp"
cidr_blocks = each.value.cidr_blocks
security_group_id = aws_security_group.web.id
description = each.key
}
Combining count and for_each
You can’t use both on the same resource, but you can use them in nested modules:
# Module uses count for replicas
module "service" {
source = "./modules/service"
for_each = var.services
name = each.key
instance_type = each.value.instance_type
replicas = each.value.replicas # count inside module
}
Common Errors
“Invalid for_each argument”
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 list.
Fix: Convert list to set:
for_each = toset(var.my_list)
“Invalid count argument”
The "count" value depends on resource attributes that cannot be determined
until apply.
Fix: Use values known at plan time (variables, locals, data sources).
Hands-On Courses
Learn by doing with interactive courses on CopyPasteLearn:
Conclusion
Use count for simple numeric repetition and conditional creation. Use for_each when resources have unique identities and need stable addresses. When in doubt, prefer for_each — it’s safer when resources change over time because it avoids the index shifting problem that count has.




