Skip to main content

Command Palette

Search for a command to run...

Day 12: Count and For_Each - Creating Multiple Resources

Updated
11 min read
Day 12: Count and For_Each - Creating Multiple Resources
S

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)

Featurecountfor_each
IdentificationBy index number ([0], [1], [2])By key name (["web"], ["api"])
Input typeList or numberMap or set
Reference syntaxaws_instance.web[0]aws_instance.web["web"]
When to useFixed-size or ordered listsNamed, key-based, or dynamic collections
If item removedIndices shift → may recreate wrong instancesOnly that key’s resource is destroyed
StabilityFragile (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:

  1. Prefer for_each for named resources

     for_each = toset(["web", "api", "worker"])
    
  2. Use count for conditional creation

     count = var.enabled ? 1 : 0
    
  3. Use count for simple multiples

     count = 5  # When you just need 5 identical things
    
  4. Convert lists to sets for for_each

     for_each = toset(var.list)
    

DON’T:

  1. Don’t use count with lists that might change

  2. Don’t use both count and for_each together

  3. 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!

T

Thank you!

More from this blog

S

StackOps - Diary

33 posts

Welcome to the StackOps - Diary. We’re dedicated to empowering the tech community. We delve into cloud-native and microservices technologies, sharing knowledge to build modern, scalable solutions.