Day 12: Count and For_Each - Creating Multiple Resources

I'm a cloud-native enthusiast and tech blogger, sharing insights on Kubernetes, AWS, CI/CD, and Linux across my blog and Facebook page. Passionate about modern infrastructure and microservices, I aim to help others understand and leverage cloud-native technologies for scalable, efficient solutions.
Welcome to Day 12! Today we’ll master two powerful meta-arguments: count and for_each. These allow you to create multiple similar resources dynamically, making your infrastructure scalable and maintainable.
🎯 Today’s Goals
Understand count and for_each meta-arguments
Learn when to use count vs for_each
Master resource indexing and references
Handle dynamic resource creation
Avoid common pitfalls
🔢 The Count Meta-Argument
Count creates multiple instances of a resource based on a number.
Basic Count Syntax
resource "aws_instance" "web" {
count = 3
ami = "ami-12345"
instance_type = "t2.micro"
tags = {
Name = "web-server-${count.index}"
}
}
# Creates: web[0], web[1], web[2]
count.index
count.index is the current iteration number (0-based):
resource "aws_subnet" "public" {
count = 3
vpc_id = aws_vpc.main.id
cidr_block = "10.0.${count.index + 1}.0/24"
availability_zone = data.aws_availability_zones.available.names[count.index]
tags = {
Name = "public-subnet-${count.index + 1}"
Index = count.index
}
}
# Creates:
# - public[0]: 10.0.1.0/24 in us-east-1a
# - public[1]: 10.0.2.0/24 in us-east-1b
# - public[2]: 10.0.3.0/24 in us-east-1c
Dynamic Count with Variables
variable "instance_count" {
type = number
default = 3
}
resource "aws_instance" "app" {
count = var.instance_count
ami = data.aws_ami.amazon_linux.id
instance_type = "t2.micro"
}
Conditional Count (Create or Not)
variable "create_instance" {
type = bool
default = true
}
resource "aws_instance" "optional" {
count = var.create_instance ? 1 : 0
ami = "ami-12345"
instance_type = "t2.micro"
}
# If true: creates 1 instance
# If false: creates 0 instances (none)
Count with length()
variable "availability_zones" {
type = list(string)
default = ["us-east-1a", "us-east-1b", "us-east-1c"]
}
resource "aws_subnet" "public" {
count = length(var.availability_zones)
vpc_id = aws_vpc.main.id
cidr_block = "10.0.${count.index}.0/24"
availability_zone = var.availability_zones[count.index]
}
Referencing Count Resources
# Reference all instances
output "all_instance_ids" {
value = aws_instance.web[*].id
}
# Reference specific instance
output "first_instance_id" {
value = aws_instance.web[0].id
}
# Use in another resource
resource "aws_eip" "web" {
count = length(aws_instance.web)
instance = aws_instance.web[count.index].id
}
🔁 The For_Each Meta-Argument
For_each creates multiple resources based on a map or set.
Basic For_Each Syntax
resource "aws_instance" "servers" {
for_each = toset(["web", "api", "worker"])
ami = "ami-12345"
instance_type = "t2.micro"
tags = {
Name = "server-${each.key}"
}
}
# Creates: servers["web"], servers["api"], servers["worker"]
each.key and each.value
# With set: each.key == each.value
for_each = toset(["web", "api"])
# each.key = "web", each.value = "web"
# With map: each.key = key, each.value = value
for_each = {
web = "t2.micro"
api = "t2.small"
}
# each.key = "web", each.value = "t2.micro"
For_Each with Maps
variable "instances" {
type = map(object({
instance_type = string
ami = string
}))
default = {
web = {
instance_type = "t2.micro"
ami = "ami-12345"
}
api = {
instance_type = "t2.small"
ami = "ami-67890"
}
worker = {
instance_type = "t2.micro"
ami = "ami-12345"
}
}
}
resource "aws_instance" "servers" {
for_each = var.instances
ami = each.value.ami
instance_type = each.value.instance_type
tags = {
Name = each.key
Type = each.value.instance_type
}
}
For_Each with Sets
variable "subnet_names" {
type = set(string)
default = ["public-1", "public-2", "private-1"]
}
resource "aws_subnet" "subnets" {
for_each = var.subnet_names
vpc_id = aws_vpc.main.id
cidr_block = "10.0.${index(var.subnet_names, each.key)}.0/24"
tags = {
Name = each.key
}
}
Referencing For_Each Resources
# Reference all instances
output "all_instance_ids" {
value = values(aws_instance.servers)[*].id
}
# Reference specific instance
output "web_instance_id" {
value = aws_instance.servers["web"].id
}
# Create map of IDs
output "instance_id_map" {
value = {
for key, instance in aws_instance.servers :
key => instance.id
}
}
# Use in another resource
resource "aws_eip" "server_ips" {
for_each = aws_instance.servers
instance = each.value.id
tags = {
Name = "eip-${each.key}"
}
}
⚖️ Count vs For_Each: When to Use Which
Use Count When:
✅ Creating a specific number of identical resources
resource "aws_instance" "web" {
count = 5 # Create exactly 5 instances
}
✅ Conditional resource creation (0 or 1)
count = var.create_bastion ? 1 : 0
✅ Resources are truly identical and order doesn’t matter
resource "aws_subnet" "public" {
count = 3
}
Use For_Each When:
✅ Each resource has unique configuration
for_each = {
web = { type = "t2.micro", ami = "ami-123" }
api = { type = "t2.small", ami = "ami-456" }
}
✅ Resources are identified by name/key
for_each = toset(["production", "staging", "development"])
✅ You need to add/remove specific items
# Removing "staging" won't affect "production"
for_each = toset(["production", "development"])
The Key Difference
# COUNT: Resources identified by index
aws_instance.web[0]
aws_instance.web[1]
aws_instance.web[2]
# If you remove the middle item, indices shift!
# web[1] becomes what was web[2]
# FOR_EACH: Resources identified by key
aws_instance.servers["web"]
aws_instance.servers["api"]
aws_instance.servers["worker"]
# Removing "api" doesn't affect "web" or "worker"
⚙️ 1️⃣ Using count
Example:
resource "aws_instance" "web" {
count = 3
ami = "ami-123456"
instance_type = "t2.micro"
}
✅ What Terraform does:
Creates 3 resources, indexed by number (starting from 0):
aws_instance.web[0] aws_instance.web[1] aws_instance.web[2]
🧩 Access Example:
You can reference a specific one using its index:
aws_instance.web[1].id
⚠️ The Problem:
If you remove or reorder items (for example, if you reduce count to 2 or skip the middle one),
Terraform reindexes the remaining ones.
So if you had:
aws_instance.web[0] -> stays
aws_instance.web[1] -> deleted
aws_instance.web[2] -> becomes web[1]
Result: Terraform may destroy and recreate resources unexpectedly because the index numbers shift.
💡 Best for:
Simple, fixed-size lists—e.g., creating N identical servers.
⚙️ 2️⃣ Using for_each
Example:
resource "aws_instance" "servers" {
for_each = {
web = "t2.micro"
api = "t2.small"
worker = "t3.micro"
}
ami = "ami-123456"
instance_type = each.value
tags = {
Name = each.key
}
}
✅ What Terraform does:
Creates resources identified by key names, not numbers:
aws_instance.servers["web"]
aws_instance.servers["api"]
aws_instance.servers["worker"]
Each one is uniquely named and stable — based on its key.
🧩 Access Example:
You can reference a specific one by its key:
aws_instance.servers["web"].id
💪 The Advantage:
If you remove "api" from the map, Terraform only deletes that one:
aws_instance.servers["api"] -> destroyed
✅ "web" and "worker" stay exactly the same — no index shifting, no recreation.
🧠 The Key Difference (Summary)
| Feature | count | for_each |
| Identification | By index number ([0], [1], [2]) | By key name (["web"], ["api"]) |
| Input type | List or number | Map or set |
| Reference syntax | aws_instance.web[0] | aws_instance.web["web"] |
| When to use | Fixed-size or ordered lists | Named, key-based, or dynamic collections |
| If item removed | Indices shift → may recreate wrong instances | Only that key’s resource is destroyed |
| Stability | Fragile (depends on order) | Stable (depends on key) |
⚠️ Common Pitfalls and Solutions
Pitfall 1: Count Index Shift
# ❌ BAD: Using count with list
variable "servers" {
default = ["web", "api", "db"]
}
resource "aws_instance" "bad" {
count = length(var.servers)
# If you remove "api", "db" shifts from index 2 to 1
# Terraform will destroy and recreate it!
}
# ✅ GOOD: Use for_each
resource "aws_instance" "good" {
for_each = toset(var.servers)
# Removing "api" doesn't affect "web" or "db"
}
Pitfall 2: Can’t Use Both
# ❌ ERROR: Cannot use both
resource "aws_instance" "invalid" {
count = 3
for_each = toset(["a", "b"]) # ERROR!
}
Pitfall 3: For_Each Requires Map or Set
# ❌ ERROR: for_each needs map or set, not list
variable "azs" {
default = ["us-east-1a", "us-east-1b"]
}
resource "aws_subnet" "bad" {
for_each = var.azs # ERROR! List not supported
}
# ✅ GOOD: Convert to set
resource "aws_subnet" "good" {
for_each = toset(var.azs)
}
🧪 Hands-On Lab: Count vs For_Each
Let’s build infrastructure demonstrating both approaches!
Step 1: Create Project
mkdir terraform-count-foreach-lab
cd terraform-count-foreach-lab
Step 2: Create variables.tf
# variables.tf
variable "aws_region" {
type = string
default = "us-east-1"
}
variable "environment" {
type = string
default = "dev"
}
# For count example
variable "public_subnet_count" {
type = number
default = 3
}
variable "availability_zones" {
type = list(string)
default = ["us-east-1a", "us-east-1b", "us-east-1c"]
}
# For for_each example
variable "applications" {
type = map(object({
instance_type = string
desired_count = number
port = number
}))
default = {
web = {
instance_type = "t2.micro"
desired_count = 2
port = 80
}
api = {
instance_type = "t2.small"
desired_count = 2
port = 8080
}
worker = {
instance_type = "t2.micro"
desired_count = 3
port = 0
}
}
}
variable "create_bastion" {
type = bool
default = true
}
Step 3: Create main.tf
# main.tf
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}
provider "aws" {
region = var.aws_region
}
# VPC
resource "aws_vpc" "main" {
cidr_block = "10.0.0.0/16"
enable_dns_hostnames = true
tags = {
Name = "count-foreach-demo-vpc"
}
}
# Internet Gateway
resource "aws_internet_gateway" "main" {
vpc_id = aws_vpc.main.id
tags = {
Name = "main-igw"
}
}
# ============================================
# COUNT EXAMPLES
# ============================================
# Public Subnets using COUNT
resource "aws_subnet" "public" {
count = var.public_subnet_count
vpc_id = aws_vpc.main.id
cidr_block = "10.0.${count.index + 1}.0/24"
availability_zone = var.availability_zones[count.index % length(var.availability_zones)]
map_public_ip_on_launch = true
tags = {
Name = "public-subnet-${count.index + 1}"
Index = count.index
}
}
# Conditional Bastion using COUNT
resource "aws_instance" "bastion" {
count = var.create_bastion ? 1 : 0
ami = data.aws_ami.amazon_linux.id
instance_type = "t2.micro"
subnet_id = aws_subnet.public[0].id
tags = {
Name = "bastion-host"
}
}
# ============================================
# FOR_EACH EXAMPLES
# ============================================
# Application-specific Subnets using FOR_EACH
locals {
app_subnets = {
for app_name in keys(var.applications) :
app_name => {
cidr_block = "10.0.${10 + index(keys(var.applications), app_name)}.0/24"
az = var.availability_zones[0]
}
}
}
resource "aws_subnet" "app_subnets" {
for_each = local.app_subnets
vpc_id = aws_vpc.main.id
cidr_block = each.value.cidr_block
availability_zone = each.value.az
tags = {
Name = "${each.key}-subnet"
Application = each.key
}
}
# Security Groups using FOR_EACH
resource "aws_security_group" "app_sgs" {
for_each = var.applications
name = "${each.key}-sg"
description = "Security group for ${each.key}"
vpc_id = aws_vpc.main.id
dynamic "ingress" {
for_each = each.value.port > 0 ? [1] : []
content {
from_port = each.value.port
to_port = each.value.port
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = {
Name = "${each.key}-sg"
Application = each.key
}
}
# AMI Data Source
data "aws_ami" "amazon_linux" {
most_recent = true
owners = ["amazon"]
filter {
name = "name"
values = ["amzn2-ami-hvm-*-x86_64-gp2"]
}
}
# Application Instances using nested COUNT and FOR_EACH
locals {
# Flatten applications into individual instances
app_instances = flatten([
for app_name, app_config in var.applications : [
for i in range(app_config.desired_count) : {
key = "${app_name}-${i}"
app_name = app_name
instance_type = app_config.instance_type
index = i
}
]
])
# Convert to map for for_each
app_instances_map = {
for inst in local.app_instances :
inst.key => inst
}
}
resource "aws_instance" "app_instances" {
for_each = local.app_instances_map
ami = data.aws_ami.amazon_linux.id
instance_type = each.value.instance_type
subnet_id = aws_subnet.app_subnets[each.value.app_name].id
vpc_security_group_ids = [aws_security_group.app_sgs[each.value.app_name].id]
user_data = <<-EOF
#!/bin/bash
echo "${each.value.app_name} instance ${each.value.index}" > /tmp/info.txt
EOF
tags = {
Name = each.key
Application = each.value.app_name
Index = each.value.index
}
}
Step 4: Create outputs.tf
# outputs.tf
# COUNT outputs
output "public_subnet_ids_count" {
description = "Public subnet IDs (created with count)"
value = aws_subnet.public[*].id
}
output "bastion_instance_id" {
description = "Bastion instance ID (conditional with count)"
value = length(aws_instance.bastion) > 0 ? aws_instance.bastion[0].id : "Not created"
}
# FOR_EACH outputs
output "app_subnet_ids_foreach" {
description = "App subnet IDs (created with for_each)"
value = {
for key, subnet in aws_subnet.app_subnets :
key => subnet.id
}
}
output "security_group_ids_foreach" {
description = "Security group IDs (created with for_each)"
value = {
for key, sg in aws_security_group.app_sgs :
key => sg.id
}
}
output "app_instances_by_type" {
description = "Instances grouped by application"
value = {
for app_name in keys(var.applications) :
app_name => [
for key, instance in aws_instance.app_instances :
{
id = instance.id
private_ip = instance.private_ip
}
if local.app_instances_map[key].app_name == app_name
]
}
}
output "total_instance_count" {
description = "Total number of instances"
value = {
bastion = var.create_bastion ? 1 : 0
apps = sum([for app in var.applications : app.desired_count])
total = (var.create_bastion ? 1 : 0) + sum([for app in var.applications : app.desired_count])
}
}
Step 5: Test Count Behavior
# Initialize
terraform init
# Applyt
erraform apply -auto-approve
# View outputs
terraform output
# Now test count index shift issue
# Modify variables.tf: change public_subnet_count from 3 to 2
# Plan again
terraform plan
# Notice: Terraform wants to destroy public[2]
# If resources depend on indices, this can be problematic!
Step 6: Test For_Each Behavior
# Remove one application from variables.tf
# Change applications map to remove "api"
terraform plan
# Notice: Only "api" resources are destroyed
# "web" and "worker" are unchanged!
Step 7: Clean Up
terraform destroy -auto-approve
📝 Best Practices
✅ DO:
Prefer for_each for named resources
for_each = toset(["web", "api", "worker"])Use count for conditional creation
count = var.enabled ? 1 : 0Use count for simple multiples
count = 5 # When you just need 5 identical thingsConvert lists to sets for for_each
for_each = toset(var.list)
❌ DON’T:
Don’t use count with lists that might change
Don’t use both count and for_each together
Don’t make count/for_each depend on resource attributes
📝 Summary
Today you learned:
✅ Count meta-argument and count.index
✅ For_each meta-argument with maps and sets
✅ each.key and each.value
✅ When to use count vs for_each
✅ Common pitfalls and solutions
✅ Resource referencing patterns
🚀 Tomorrow’s Preview
Day 13: Conditional Expressions & Logic
Tomorrow we’ll:
Master conditional expressions
Learn ternary operators
Use logical operators
Implement complex conditions
Build conditional infrastructure
← Day 11: Lists, Maps & Sets | Day 13: Conditionals & Logic →
Remember: Choose for_each for flexibility, count for simplicity!



