Using Terraform Data Sources Effectively
Learn how to use Terraform data sources to query existing resources, look up AMIs, reference remote state, and build dynamic configurations. Complete.
Cloud Computing
Learn how to use Terraform count and for_each to create multiple resources. Side-by-side comparison, practical examples, conditional creation
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.
| 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 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.
# Single instance
output "first_ip" {
value = aws_instance.web[0].public_ip
}
# All instances
output "all_ips" {
value = aws_instance.web[*].public_ip
}variable "instance_count" {
type = number
default = 3
}
resource "aws_instance" "web" {
count = var.instance_count
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t3.micro"
}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
}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 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"].
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
}
}# 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 }
}variable "users" {
type = set(string)
default = ["alice", "bob", "charlie"]
}
resource "aws_iam_user" "team" {
for_each = var.users
name = each.value
}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.
count = 3 replicas)count = var.enabled ? 1 : 0)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)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
}
}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
}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
}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)The "count" value depends on resource attributes that cannot be determined
until apply.Fix: Use values known at plan time (variables, locals, data sources).
Learn by doing with interactive courses on CopyPasteLearn:
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.
Learn how to use Terraform data sources to query existing resources, look up AMIs, reference remote state, and build dynamic configurations. Complete.
Master Terraform version constraints for Terraform core and providers. Covers operators, lock files, required_version, required_providers, and upgrade...
Complete guide to Terraform lifecycle rules. Learn prevent_destroy, create_before_destroy, ignore_changes
Master multi-account AWS management with Terraform. Learn provider aliases, cross-account IAM roles, AWS Organizations integration, and production-ready.