count, for_each & dynamic blocks
Build many resources from data.
Real infrastructure is repetitive — three identical subnets, a security-group rule per port, one bucket per team. You do not copy-paste those; you generate them from data with the meta-arguments count and for_each, and the dynamic block. These turn a list or map into many resources, and choosing between count and for_each correctly is one of the highest-leverage Terraform skills.
# count: N copies, indexed 0..N-1resource "aws_instance" "web" {count = 3instance_type = "t3.micro"tags = { Name = "web-${count.index}" } # web-0, web-1, web-2}# for_each: one resource per map/set entry, keyed by identityresource "aws_iam_user" "team" {for_each = toset(["alice", "bob", "carol"])name = each.key # each.key / each.value}
Why for_each usually beats count
count is positional: resources are tracked by index (web[0], web[1], web[2]). Remove the middle item from the list and everything after it shifts index — Terraform sees web[1] and web[2] as changed and may destroy and recreate them, even though you only meant to delete one. for_each keys resources by a stable identity (the map key), so removing one entry touches only that one. Prefer for_each whenever the collection can change; reserve count for a fixed number of truly identical copies, or a simple on/off toggle.
# count pitfall: delete "bob" from the middle of a list of 3-/+ aws_instance.web[1] # was carol, now bob’s slot — destroyed & recreated-/+ aws_instance.web[2] # shifted — churned needlessly# for_each: delete "bob" from the map- aws_iam_user.team["bob"] # only bob is removed; alice & carol untouched
dynamic blocks
Some resources have repeatable nested blocks — many ingress rules inside one security group. A dynamic block generates those nested blocks from a collection, the same idea as for_each but for blocks within a resource rather than whole resources. Useful, but do not reach for it prematurely: a couple of static ingress blocks are clearer than a dynamic one, so use it when the list is genuinely data-driven or long.
resource "aws_security_group" "web" {dynamic "ingress" {for_each = var.allowed_ports # e.g. [80, 443]content {from_port = ingress.valueto_port = ingress.valueprotocol = "tcp"cidr_blocks = ["0.0.0.0/0"]}}}