# terraform / iteration / meta-arguments
Scale Infrastructure with Code: count, for_each & Dynamic Blocks
When you need to provision 5 identical worker nodes, 10 IAM users, or 15 security group firewall rules, you don't duplicate code blocks. Terraform provides count, for_each, and dynamic blocks to iterate cleanly across lists, sets, and maps.
Choosing the Right Iteration Method
| Method | Iterates Over | Resource Address Example | Best Used For |
|---|---|---|---|
count |
Whole Number (integer) | aws_instance.web[0] |
Identical clones or conditional toggle (0 or 1) |
for_each |
Set of strings or Map of objects | aws_instance.web["api"] |
Distinct resources with unique configurations (IAM, Subnets) |
dynamic |
List/Map inside a resource | ingress { ... } |
Repeating nested blocks (Security group rules, tags, routes) |
1. The count Meta-Argument & Conditionals
# Launch 3 web servers with index numbering resource "aws_instance" "web" { count = 3 ami = "ami-0123456789abcdef0" instance_type = "t3.micro" tags = { Name = "web-server-${count.index + 1}" } }
# If enable_bastion is true -> count is 1 (created) # If enable_bastion is false -> count is 0 (skipped) resource "aws_instance" "bastion" { count = var.enable_bastion ? 1 : 0 ami = "ami-0123456789abcdef0" instance_type = "t3.nano" }
The Index Shift Flaw: Why count Breaks on Lists
Suppose you use count = length(var.users) with ["alice", "bob", "charlie"]. If "alice" leaves the company and is removed from index 0, "bob" becomes index 0 and "charlie" becomes index 1. Terraform will destroy and recreate every single user after the deleted item!
Unintended Destruction
On stateful resources (like databases or storage), index shifting can destroy and replace live production data. Always use for_each when iterating over distinct named items!
What Terraform Does with count
users[0] = "alice"DELETED from code
users[0] becomes "bob"Terraform updates index 0 from alice to bob
users[2] destroyedTotal count shrank from 3 to 2
2. The for_each Meta-Argument (The Safe Pattern)
variable "usernames" { type = list(string) default = ["alice", "bob", "charlie"] } resource "aws_iam_user" "team" { for_each = toset(var.usernames) name = each.value }
locals { subnets = { pub-1a = { cidr = "10.0.1.0/24", az = "ap-south-1a" } pub-1b = { cidr = "10.0.2.0/24", az = "ap-south-1b" } } } resource "aws_subnet" "main" { for_each = local.subnets vpc_id = aws_vpc.prod.id cidr_block = each.value.cidr availability_zone = each.value.az }
3. for Comprehensions & Splat Expressions
# 1. Transform list of user names to uppercase output "upper_users" { value = [for u in var.usernames : upper(u)] } # 2. Extract ARN map from for_each resource output "user_arns" { value = { for k, user in aws_iam_user.team : k => user.arn } }
# Splat [*] extracts an attribute across all count items output "all_server_ips" { value = aws_instance.web[*].public_ip }
4. Dynamic Blocks for Nested Resource Configurations
locals { ingress_ports = [80, 443, 8080, 22] } resource "aws_security_group" "web_sg" { name = "web-traffic-sg" description = "Allow HTTP, HTTPS, WebApp, and SSH" dynamic "ingress" { for_each = local.ingress_ports content { from_port = ingress.value to_port = ingress.value protocol = "tcp" cidr_blocks = ["0.0.0.0/0"] } } }
Eliminate 40 Lines of Copy-Paste
Instead of writing out 4 separate ingress { ... } blocks with identical protocol and CIDR definitions, a dynamic "ingress" block generates them cleanly from a list variable or local value.