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 2count.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
(socount.index + 1 = 1
) - Second file:
count.index = 1
(socount.index + 1 = 2
) - Third file:
count.index = 2
(socount.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
istrue
:count = 1
(creates 1 file) - If
create_backup
isfalse
: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 mapeach.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 propertiesjsonencode()
– 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:
- Create a variable with different database types (mysql, postgres, redis)
- Use for_each to create a config file for each database
- Include different ports for each database type
- 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!