Terraform Count and For_Each – T5

In the last blog, we learned about Terraform resources and data sources.

Today we’re going to learn something really cool – how to create multiple things with just a few lines of code.

Imagine you need to create 5 servers. Without what we’re learning today, you’d have to write the same server code 5 times. That’s like writing “I will not copy code” 100 times on a blackboard – boring and painful!

But with count and for_each, you can create 5 servers (or 50!) with almost the same amount of code as creating 1. Pretty magical, right?

The Problem: Repeating Yourself

Let’s say you want to create 3 files. The old way would be:

resource "local_file" "file1" {
  content  = "This is file 1"
  filename = "file1.txt"
}

resource "local_file" "file2" {
  content  = "This is file 2"
  filename = "file2.txt"
}

resource "local_file" "file3" {
  content  = "This is file 3"
  filename = "file3.txt"
}

That’s a lot of typing! And what if you need 10 files? Or 100? You’d be there all day!

Enter Count: Your Copy Machine

Count is like having a copy machine that can make exactly the number of copies you want.

Here’s the magic version:

resource "local_file" "files" {
  count    = 3
  content  = "This is file ${count.index + 1}"
  filename = "file${count.index + 1}.txt"
}

That’s it! This creates 3 files with just one resource block.

What’s happening here:

  • count = 3 tells Terraform “make 3 of these”
  • count.index is a special number that starts at 0, then 1, then 2
  • count.index + 1 gives us 1, 2, 3 (more human-friendly)

Understanding Count.Index

Count.index is like a counter that Terraform uses:

  • First file: count.index = 0 (so count.index + 1 = 1)
  • Second file: count.index = 1 (so count.index + 1 = 2)
  • Third file: count.index = 2 (so count.index + 1 = 3)

Let’s see this in action:

resource "local_file" "greeting" {
  count    = 4
  content  = "Hello! I am file number ${count.index + 1}"
  filename = "greeting-${count.index + 1}.txt"
}

This creates:

  • greeting-1.txt with content “Hello! I am file number 1”
  • greeting-2.txt with content “Hello! I am file number 2”
  • greeting-3.txt with content “Hello! I am file number 3”
  • greeting-4.txt with content “Hello! I am file number 4”

Try it! It’s pretty cool to see Terraform create multiple files instantly.

Count with Variables

You can use variables to control how many things you create:

variable "how_many_servers" {
  type        = number
  description = "How many servers do you want?"
  default     = 2
}

resource "local_file" "server_config" {
  count    = var.how_many_servers
  content  = "Server ${count.index + 1} configuration"
  filename = "server-${count.index + 1}-config.txt"
}

Now you can change the number without editing your code:

terraform apply -var="how_many_servers=5"

Boom! 5 server config files created.

Count with Lists

This is where it gets really useful. You can use count with lists:

variable "server_names" {
  type    = list(string)
  default = ["web", "database", "cache"]
}

resource "local_file" "servers" {
  count    = length(var.server_names)
  content  = "Configuration for ${var.server_names[count.index]} server"
  filename = "${var.server_names[count.index]}-config.txt"
}

What’s new:

  • length(var.server_names) counts how many items in the list (3 in this case)
  • var.server_names[count.index] gets the item at that position

This creates:

  • web-config.txt
  • database-config.txt
  • cache-config.txt

Conditional Creation with Count

Want to create something only sometimes? Count can do that too:

variable "create_backup" {
  type    = bool
  default = false
}

resource "local_file" "backup_config" {
  count    = var.create_backup ? 1 : 0
  content  = "Backup configuration enabled"
  filename = "backup-config.txt"
}

How this works:

  • If create_backup is true: count = 1 (creates 1 file)
  • If create_backup is false: count = 0 (creates 0 files)

Try it both ways:

terraform apply -var="create_backup=true"
terraform apply -var="create_backup=false"

Enter For_Each: The Smart Choice

While count is great, for_each is often smarter. Think of for_each as count’s intelligent cousin.

Here’s the same example with for_each:

variable "servers" {
  type = map(string)
  default = {
    web   = "Web Server Config"
    db    = "Database Server Config"  
    cache = "Cache Server Config"
  }
}

resource "local_file" "server_configs" {
  for_each = var.servers
  content  = each.value
  filename = "${each.key}-config.txt"
}

What’s different:

  • for_each = var.servers loops through the map
  • each.key is the left side (“web”, “db”, “cache”)
  • each.value is the right side (“Web Server Config”, etc.)

This creates the same files but with a smarter approach.

Why For_Each is Often Better

Let me show you why for_each is usually the better choice:

With Count: If you have servers [“web”, “db”, “cache”] and you remove “db”, Terraform gets confused:

  • Old: web(0), db(1), cache(2)
  • New: web(0), cache(1)
  • Terraform thinks cache moved and recreates it!

With For_Each:

  • Old: web(“web”), db(“db”), cache(“cache”)
  • New: web(“web”), cache(“cache”)
  • Terraform just removes db, leaves cache alone

Smart, right?

For_Each with Sets

You can use for_each with lists too, but you need to convert them to sets:

variable "backup_folders" {
  type    = list(string)
  default = ["documents", "photos", "music"]
}

resource "local_file" "backup_info" {
  for_each = toset(var.backup_folders)
  content  = "Backup information for ${each.value} folder"
  filename = "${each.value}-backup-info.txt"
}

What’s toset()? It converts a list to a set. For_each needs either a map or a set, not a list.

With sets, each.key and each.value are the same thing.

Real World Example: Multiple Environments

Let’s build something useful – configurations for different environments:

variable "environments" {
  type = map(object({
    server_size = string
    backup_days = number
    ssl_enabled = bool
  }))
  default = {
    dev = {
      server_size = "small"
      backup_days = 7
      ssl_enabled = false
    }
    staging = {
      server_size = "medium"
      backup_days = 14
      ssl_enabled = true
    }
    prod = {
      server_size = "large"
      backup_days = 30
      ssl_enabled = true
    }
  }
}

resource "local_file" "env_config" {
  for_each = var.environments
  
  filename = "${each.key}-environment.json"
  content = jsonencode({
    environment = each.key
    server_size = each.value.server_size
    backup_days = each.value.backup_days
    ssl_enabled = each.value.ssl_enabled
    created_by  = "Terraform"
  })
}

output "environments_created" {
  value = {
    for env_name, config in local_file.env_config : 
    env_name => config.filename
  }
}

What’s new here:

  • map(object({...})) – a map where each value is an object with specific properties
  • jsonencode() – converts the data to JSON format
  • The output uses a for expression to show all created files

This creates three JSON files with environment-specific configurations!

When to Use Count vs For_Each

Use Count when:

  • You need a specific number of identical things
  • You’re working with simple lists
  • The order matters and won’t change
# Good use of count - creating 5 identical workers
resource "local_file" "workers" {
  count    = 5
  content  = "Worker ${count.index + 1} ready for tasks"
  filename = "worker-${count.index + 1}.txt"
}

Use For_Each when:

  • You need to create things with different configurations
  • You might add/remove items later
  • You want to reference specific instances by name
# Good use of for_each - different server types
variable "server_types" {
  default = {
    web = "t2.micro"
    db  = "t2.small"  
    api = "t2.medium"
  }
}

resource "local_file" "server_specs" {
  for_each = var.server_types
  content  = "Server ${each.key} uses ${each.value} instance type"
  filename = "${each.key}-server-spec.txt"
}

Practical Example: Website Components

Let’s create a complete website setup:

variable "website_components" {
  type = map(object({
    port        = number
    health_path = string
    replicas    = number
  }))
  default = {
    frontend = {
      port        = 3000
      health_path = "/health"
      replicas    = 2
    }
    backend = {
      port        = 8080
      health_path = "/api/health"
      replicas    = 3
    }
    database = {
      port        = 5432
      health_path = "/db-health"
      replicas    = 1
    }
  }
}

# Create configuration for each component
resource "local_file" "component_config" {
  for_each = var.website_components
  
  filename = "${each.key}-service.yaml"
  content = <<-EOF
    # ${each.key} Service Configuration
    apiVersion: v1
    kind: Service
    metadata:
      name: ${each.key}-service
    spec:
      port: ${each.value.port}
      replicas: ${each.value.replicas}
      healthCheck:
        path: ${each.value.health_path}
        port: ${each.value.port}
    
    # Generated by Terraform
  EOF
}

# Create health check scripts
resource "local_file" "health_checks" {
  for_each = var.website_components
  
  filename = "check-${each.key}.sh"
  content = <<-EOF
    #!/bin/bash
    echo "Checking ${each.key} service health..."
    curl -f http://localhost:${each.value.port}${each.value.health_path}
    if [ $? -eq 0 ]; then
      echo "${each.key} is healthy!"
    else
      echo "${each.key} is not responding!"
    fi
  EOF
}

# Output summary
output "website_summary" {
  value = {
    components_created = length(var.website_components)
    service_configs    = [for k, v in local_file.component_config : v.filename]
    health_scripts     = [for k, v in local_file.health_checks : v.filename]
    total_replicas     = sum([for comp in var.website_components : comp.replicas])
  }
}

This creates:

  • Service configuration files for each component
  • Health check scripts for each component
  • A nice summary of everything created

Combining Count and For_Each

Sometimes you need both! Here’s how:

variable "server_groups" {
  type = map(object({
    count = number
    type  = string
  }))
  default = {
    web = {
      count = 3
      type  = "frontend"
    }
    api = {
      count = 2  
      type  = "backend"
    }
  }
}

# First, create groups with for_each
resource "local_file" "server_groups" {
  for_each = var.server_groups
  
  filename = "${each.key}-group.txt"
  content  = "Group: ${each.key}, Type: ${each.value.type}, Count: ${each.value.count}"
}

# Then, create individual servers with count
resource "local_file" "servers" {
  for_each = var.server_groups
  count    = each.value.count
  
  filename = "${each.key}-server-${count.index + 1}.txt"
  content = <<-EOF
    Server: ${each.key}-server-${count.index + 1}
    Group: ${each.key}
    Type: ${each.value.type}
    Index: ${count.index + 1}
  EOF
}

Wait, this won’t work! You can’t use count and for_each on the same resource. Let me fix that:

# Create a local that expands everything
locals {
  servers = flatten([
    for group_name, group_config in var.server_groups : [
      for i in range(group_config.count) : {
        name  = "${group_name}-server-${i + 1}"
        group = group_name
        type  = group_config.type
        index = i + 1
      }
    ]
  ])
  
  # Convert to map for for_each
  servers_map = {
    for server in local.servers : server.name => server
  }
}

resource "local_file" "all_servers" {
  for_each = local.servers_map
  
  filename = "${each.value.name}.txt"
  content = <<-EOF
    Server: ${each.value.name}
    Group: ${each.value.group}
    Type: ${each.value.type}
    Index: ${each.value.index}
  EOF
}

Don’t worry if this looks complex – it’s advanced stuff! The key point is that locals can help you transform data for count and for_each.

Common Mistakes to Avoid

Mistake 1: Using count with changing lists

# Don't do this - removing middle items causes problems
variable "servers" {
  default = ["web", "db", "cache"]
}

resource "aws_instance" "bad_example" {
  count = length(var.servers)
  # ... rest of config
}

Better:

# Do this instead
resource "aws_instance" "good_example" {
  for_each = toset(var.servers)
  # ... rest of config
}

Mistake 2: Mixing count and for_each

# This won't work!
resource "local_file" "broken" {
  count    = 3
  for_each = var.some_map  # Error!
  # ...
}

Mistake 3: Forgetting toset() with lists

# This won't work
resource "local_file" "broken" {
  for_each = ["a", "b", "c"]  # Error! Need toset()
  # ...
}

# This works
resource "local_file" "fixed" {
  for_each = toset(["a", "b", "c"])
  # ...
}

Quick Reference

Count Syntax:

resource "type" "name" {
  count = NUMBER_OR_EXPRESSION
  # Use count.index (starts at 0)
}

For_Each with Map:

resource "type" "name" {
  for_each = MAP_VARIABLE
  # Use each.key and each.value
}

For_Each with Set:

resource "type" "name" {
  for_each = toset(LIST_VARIABLE)
  # each.key and each.value are the same
}

Conditional Creation:

# With count
count = var.create_this ? 1 : 0

# With for_each
for_each = var.create_this ? var.my_map : {}

Practice Challenge

Try creating this yourself:

  1. Create a variable with different database types (mysql, postgres, redis)
  2. Use for_each to create a config file for each database
  3. Include different ports for each database type
  4. Create an output showing all the files created

Here’s a starter:

variable "databases" {
  type = map(object({
    port = number
    type = string
  }))
  default = {
    # Add your database configs here
  }
}

# Your for_each resource here

# Your output here

What’s Next?

Awesome job! You’ve just learned one of Terraform’s most powerful features:

Count – Creating multiple identical things:

  • ✅ Using count.index for variations
  • ✅ Conditional creation with count
  • ✅ Working with lists and count

For_Each – Creating multiple configured things:

  • ✅ Using maps with each.key and each.value
  • ✅ Converting lists to sets with toset()
  • ✅ When for_each is better than count

Advanced Patterns:

  • ✅ Complex data structures with for_each
  • ✅ Combining different approaches
  • ✅ Common mistakes to avoid

In our next post, we’ll explore Dynamic Blocks – a special way to create repeating parts inside resources. You’ll learn:

  • How to create multiple security rules in one security group
  • Adding multiple disks to a server dynamically
  • Making your resources super flexible
  • When and how to use dynamic blocks effectively

This builds perfectly on the count and for_each knowledge you just gained!


Ready to make your resources even more flexible with dynamic blocks? The next post will show you some really cool tricks!

Akhilesh Mishra

Akhilesh Mishra

I am Akhilesh Mishra, a self-taught Devops engineer with 11+ years working on private and public cloud (GCP & AWS)technologies.

I also mentor DevOps aspirants in their journey to devops by providing guided learning and Mentorship.

Topmate: https://topmate.io/akhilesh_mishra/