Terraform Count and For_Each – T5

Share

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!

Share
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/