Terraform Data Sources Explained: Read External Information
Learn Terraform data sources to read existing AWS resources, look up AMIs, query remote state, and reference external information in your configurations.
DevOps
Terraform for_each vs count explained with practical examples. Learn when to use each, how to migrate from count to for_each
Both count and for_each create multiple instances of a resource. The difference: count uses numeric indexes, for_each uses string keys. This matters more than you'd think — count can destroy your resources when you remove an item from the middle of a list.
variable "subnet_cidrs" {
default = ["10.0.1.0/24", "10.0.2.0/24", "10.0.3.0/24"]
}
resource "aws_subnet" "main" {
count = length(var.subnet_cidrs)
vpc_id = aws_vpc.main.id
cidr_block = var.subnet_cidrs[count.index]
}State addresses:
aws_subnet.main[0] → 10.0.1.0/24
aws_subnet.main[1] → 10.0.2.0/24
aws_subnet.main[2] → 10.0.3.0/24Now remove the middle subnet (10.0.2.0/24):
variable "subnet_cidrs" {
default = ["10.0.1.0/24", "10.0.3.0/24"] # Removed 10.0.2.0/24
}terraform plan:
~ aws_subnet.main[1] 10.0.2.0/24 → 10.0.3.0/24 (update in-place — FAILS, can't change CIDR)
- aws_subnet.main[2] 10.0.3.0/24 (destroy!)Count shifted all indexes down. Subnet [1] tries to change from 10.0.2.0 to 10.0.3.0 (which fails), and subnet [2] gets destroyed. This is production-breaking.
variable "subnets" {
default = {
"us-east-1a" = "10.0.1.0/24"
"us-east-1b" = "10.0.2.0/24"
"us-east-1c" = "10.0.3.0/24"
}
}
resource "aws_subnet" "main" {
for_each = var.subnets
vpc_id = aws_vpc.main.id
cidr_block = each.value
availability_zone = each.key
}State addresses:
aws_subnet.main["us-east-1a"] → 10.0.1.0/24
aws_subnet.main["us-east-1b"] → 10.0.2.0/24
aws_subnet.main["us-east-1c"] → 10.0.3.0/24Remove the middle subnet:
variable "subnets" {
default = {
"us-east-1a" = "10.0.1.0/24"
"us-east-1c" = "10.0.3.0/24" # Only "us-east-1b" removed
}
}terraform plan:
- aws_subnet.main["us-east-1b"] (destroy — only this one!)Only the removed subnet is destroyed. The others are untouched.
| Feature | count | for_each |
|---|---|---|
| Key type | Numeric index [0], [1], [2] | String key ["name"], ["az"] |
| Remove middle item | ❌ Shifts all indexes (destructive) | ✅ Only removes that key |
| Reordering | ❌ Triggers destroy/create | ✅ Keys are stable |
| Input type | Number | Map or set of strings |
| Conditional | ✅ count = var.enabled ? 1 : 0 | ⚠️ More verbose |
| Reference | aws_subnet.main[0] | aws_subnet.main["us-east-1a"] |
| Best for | On/off toggles | Collections of distinct resources |
variable "create_bastion" {
type = bool
default = false
}
resource "aws_instance" "bastion" {
count = var.create_bastion ? 1 : 0
ami = data.aws_ami.al2023.id
instance_type = "t3.micro"
tags = { Name = "bastion" }
}
# Reference:
output "bastion_ip" {
value = var.create_bastion ? aws_instance.bastion[0].public_ip : null
}variable "worker_count" {
default = 3
}
resource "aws_instance" "worker" {
count = var.worker_count
ami = data.aws_ami.al2023.id
instance_type = "t3.medium"
tags = { Name = "worker-${count.index}" }
}This is safe because the instances are interchangeable — it doesn't matter if indexes shift.
variable "buckets" {
default = {
logs = {
versioning = true
lifecycle_days = 90
}
artifacts = {
versioning = true
lifecycle_days = 30
}
backups = {
versioning = false
lifecycle_days = 365
}
}
}
resource "aws_s3_bucket" "this" {
for_each = var.buckets
bucket = "${var.project}-${each.key}"
}
resource "aws_s3_bucket_versioning" "this" {
for_each = { for k, v in var.buckets : k => v if v.versioning }
bucket = aws_s3_bucket.this[each.key].id
versioning_configuration {
status = "Enabled"
}
}variable "ingress_rules" {
default = {
http = { port = 80, cidr = "0.0.0.0/0" }
https = { port = 443, cidr = "0.0.0.0/0" }
ssh = { port = 22, cidr = "10.0.0.0/8" }
}
}
resource "aws_security_group_rule" "ingress" {
for_each = var.ingress_rules
type = "ingress"
security_group_id = aws_security_group.web.id
from_port = each.value.port
to_port = each.value.port
protocol = "tcp"
cidr_blocks = [each.value.cidr]
description = each.key
}variable "developers" {
type = set(string)
default = ["alice", "bob", "carol"]
}
resource "aws_iam_user" "dev" {
for_each = var.developers
name = each.key
}
# Remove "bob" → only bob's user is deleted
# alice and carol are untouchedUse moved blocks to avoid destroying resources:
# Old: count-based
# resource "aws_subnet" "public" {
# count = 3
# ...
# }
# New: for_each-based
moved {
from = aws_subnet.public[0]
to = aws_subnet.public["us-east-1a"]
}
moved {
from = aws_subnet.public[1]
to = aws_subnet.public["us-east-1b"]
}
moved {
from = aws_subnet.public[2]
to = aws_subnet.public["us-east-1c"]
}
resource "aws_subnet" "public" {
for_each = {
"us-east-1a" = "10.0.0.0/24"
"us-east-1b" = "10.0.1.0/24"
"us-east-1c" = "10.0.2.0/24"
}
vpc_id = aws_vpc.main.id
cidr_block = each.value
availability_zone = each.key
}variable "names" {
default = ["web", "api", "worker"]
}
resource "aws_ecs_service" "this" {
# Convert list to set
for_each = toset(var.names)
name = each.key
}variable "instances" {
default = {
web = { type = "t3.micro", enabled = true }
api = { type = "t3.small", enabled = true }
legacy = { type = "t3.micro", enabled = false }
}
}
resource "aws_instance" "this" {
# Only create enabled instances
for_each = { for k, v in var.instances : k => v if v.enabled }
instance_type = each.value.type
ami = data.aws_ami.al2023.id
tags = { Name = each.key }
}Need to toggle on/off? → count (0 or 1)
N identical, interchangeable? → count (but for_each is still safer)
Named, distinct resources? → for_each (always)
Resources you might remove one? → for_each (always)Use count for simple on/off toggles (count = var.enabled ? 1 : 0). Use for_each for everything else — collections of resources, named items, anything where you might add or remove items. The index reordering problem with count has caused more production incidents than any other Terraform gotcha. When in doubt, use for_each.
Learn Terraform data sources to read existing AWS resources, look up AMIs, query remote state, and reference external information in your configurations.
Learn when to use Terraform depends_on for explicit resource dependencies. Understand implicit vs explicit dependencies, common use cases
Learn Terraform locals to reduce duplication, compute values, and organize complex configurations. Practical examples with naming conventions, tag maps
Understand Terraform variables (inputs) and outputs. Learn types, validation, defaults, sensitive values