Infrastructure at Scale: How Enterprises Use Terraform for_each for Complex Apps
Business Pain: Deploying a modern microservices application involves coordinating many cloud services, which is error-prone and slow when done manually.
Terraform Solutions:
The for_each and count Meta-Arguments:
Concept: Dynamically creating multiple similar resources based on a map or list.
Business Value:
Efficiency: Create 10, 50, or 100 identical resources (like subnets, security groups, or IAM users) with a few lines of code.
Example: for_each = var.environments to create a full set of resources for each item in a list [“dev”, “staging”, “prod”].
Simple Explanation:
“Think of Your App Like a Restaurant Kitchen”
# A simple app is like a food truck - one of everything
resource "aws_instance" "server" {
ami = "ami-12345"
instance_type = "t3.medium"
}
# A complex app is like a restaurant kitchen - multiple stations
locals {
kitchen_stations = {
"grill" = { type = "c5.large", count = 2 }
"fryer" = { type = "t3.medium", count = 3 }
"prep" = { type = "t3.small", count = 4 }
"dessert" = { type = "m5.large", count = 1 }
}
}
# for_each creates each unique station
resource "aws_instance" "kitchen_station" {
for_each = local.kitchen_stations
instance_type = each.value.type
count = each.value.count # Multiple identical stations
tags = {
Station = each.key
Role = "cooking"
}
}
Beginner-Friendly Breakdown:
What Makes an App “Complex”?
- Multiple services (frontend, backend, database, cache, queue)
- Different environments (dev, staging, prod)
- Various configurations per service
- Scaling requirements
How for_each & count Help:
# Problem: Manual configuration for each service
# ❌ DON'T do this:
resource "aws_instance" "frontend" { /* config */ }
resource "aws_instance" "backend" { /* config */ }
resource "aws_instance" "database" { /* config */ }
resource "aws_instance" "cache" { /* config */ }
# ✅ DO this instead:
locals {
services = {
frontend = { instance_type = "t3.medium", port = 80 }
backend = { instance_type = "t3.large", port = 8080 }
database = { instance_type = "r5.large", port = 5432 }
cache = { instance_type = "cache.t3.medium", port = 6379 }
}
}
resource "aws_instance" "service" {
for_each = local.services
instance_type = each.value.instance_type
user_data = templatefile("${each.key}-init.sh", { port = each.value.port })
tags = {
Service = each.key
}
}
Topic 1: “From Monolith to Microservices”
Post Content:
“When we split our monolith into microservices, our Terraform configs exploded from 200 to 2000+ lines. Here’s how for_each
saved us:”
# Before: Monolithic configuration
resource "aws_instance" "monolith" {
instance_type = "x2.16xlarge" # One massive server
# ... 100+ lines of config
}
# After: Microservices with for_each
locals {
microservices = {
user_service = { instance_type = "t3.medium", team = "identity" }
order_service = { instance_type = "t3.large", team = "checkout" }
payment_service = { instance_type = "t3.medium", team = "finance" }
catalog_service = { instance_type = "t3.large", team = "product" }
# 10+ more services...
}
}
resource "aws_instance" "microservice" {
for_each = local.microservices
instance_type = each.value.instance_type
tags = {
Service = each.key
Team = each.value.team
CostCenter = "CC-${each.value.team}"
}
}
# Results:
# ✅ 80% less code
# ✅ Consistent tagging
# ✅ Easy to add new services
# ✅ Team-specific resource tracking
Topic 2: “Multi-Environment Management”
Post Content:
“Managing dev, staging, and prod used to mean maintaining 3 separate Terraform configurations. Now we use one config with for_each
:”
locals {
environments = {
dev = {
instance_sizes = { web = "t3.small", api = "t3.small", db = "db.t3.small" }
node_counts = { web = 1, api = 1, db = 1 }
auto_scaling = false
}
staging = {
instance_sizes = { web = "t3.medium", api = "t3.medium", db = "db.t3.medium" }
node_counts = { web = 2, api = 2, db = 1 }
auto_scaling = true
}
prod = {
instance_sizes = { web = "t3.large", api = "t3.large", db = "db.r5.large" }
node_counts = { web = 3, api = 3, db = 2 }
auto_scaling = true
}
}
}
# Create complete environments with nested for_each
resource "aws_instance" "app_servers" {
for_each = {
for env, config in local.environments :
"${env}-web" => {
instance_type = config.instance_sizes.web
count = config.node_counts.web
environment = env
}
}
instance_type = each.value.instance_type
count = each.value.count
tags = {
Environment = each.value.environment
Role = "web"
}
}
Topic 3: “Database & Cache Layer Scaling”
Post Content:
“Our e-commerce platform needed multiple databases and cache clusters with different configurations. Here’s our for_each
solution:”
locals {
data_stores = {
# Primary databases
"postgres-primary" = {
engine = "postgres"
instance_class = "db.r5.large"
multi_az = true
backup_retention = 35
}
"mysql-sessions" = {
engine = "mysql"
instance_class = "db.t3.medium"
multi_az = false
backup_retention = 7
}
# Cache layers
"redis-sessions" = {
engine = "redis"
node_type = "cache.t3.medium"
num_cache_nodes = 2
}
"memcached-queries" = {
engine = "memcached"
node_type = "cache.t3.small"
num_cache_nodes = 3
}
}
}
resource "aws_db_instance" "database" {
for_each = {
for k, v in local.data_stores : k => v
if contains(["postgres", "mysql"], v.engine)
}
identifier = "${var.app_name}-${each.key}"
engine = each.value.engine
instance_class = each.value.instance_class
multi_az = each.value.multi_az
backup_retention_period = each.value.backup_retention
tags = {
DataTier = "persistent"
Service = each.key
}
}
resource "aws_elasticache_cluster" "cache" {
for_each = {
for k, v in local.data_stores : k => v
if contains(["redis", "memcached"], v.engine)
}
cluster_id = "${var.app_name}-${each.key}"
engine = each.value.engine
node_type = each.value.node_type
num_cache_nodes = each.value.num_cache_nodes
tags = {
DataTier = "cache"
Service = each.key
}
}
When to use Count vs for_Each
Use Count when:
- You just need N identical resources (array-like).
- You’re okay addressing them by index:
aws_instance.web[0]
. - Your input is a number or an ordered list where order doesn’t matter long-term.
Use for_each
when:
- Each instance is tied to a unique key (set/map-like).
- You want stable addressing by key:
aws_instance.web["blue"]
. - You’ll add/remove specific items over time and want minimal churn (no index shifts).
- You need to read fields from items (e.g., name, tags, az) and treat each differently.
Pitfalls to remember
- Changing the order of a list with
count
can force recreations because indexes shift. - Changing a key in
for_each
is seen as “delete old key, create new key”. for_each
requires a set or map; convert lists withtoset()
or to maps withtomap()
(or afor
-expression).- Don’t use
count
andfor_each
on the same resource.
Detailed Comparison Table
Aspect | count | for_each |
---|---|---|
Data Structure | Lists | Maps/Sets |
Resource Identity | resource[0] , resource[1] | resource["key"] |
Modification Safety | Risky (index-based) | Safe (key-based) |
Unordered Data | Not suitable | Perfect |
Code Readability | Medium | High |
Business Use Cases
Use Case 1: Environment Management (Use for_each
)
Business Problem: Standardizing multiple environments with different configurations
# Business need: Different scaling for each environment
locals {
environments = {
dev = {
instance_type = "t3.small"
min_nodes = 1
max_nodes = 3
}
staging = {
instance_type = "t3.medium"
min_nodes = 2
max_nodes = 5
}
prod = {
instance_type = "t3.large"
min_nodes = 3
max_nodes = 10
}
}
}
# for_each is perfect - each environment has unique config
resource "aws_autoscaling_group" "app" {
for_each = local.environments
name_prefix = "app-${each.key}-"
min_size = each.value.min_nodes
max_size = each.value.max_nodes
instance_type = each.value.instance_type
tags = {
Environment = each.key
CostCenter = each.key == "prod" ? "CC-001" : "CC-002"
}
}
Why for_each
? Each environment has different business requirements (cost, performance).
Use Case 2: Multiple Identical Servers (Use count
)
Business Problem: Horizontal scaling of identical web servers
# Business need: Load-balanced identical web servers
variable "web_server_count" {
description = "Number of identical web servers for load distribution"
type = number
default = 3
}
# count is perfect - all servers are identical
resource "aws_instance" "web_server" {
count = var.web_server_count # Easy to scale up/down
ami = "ami-12345678"
instance_type = "t3.medium"
tags = {
Name = "web-server-${count.index}"
Role = "web"
Tier = "public"
}
}
# Load balancer configuration
resource "aws_lb_target_group_attachment" "web" {
count = var.web_server_count
target_group_arn = aws_lb_target_group.web.arn
target_id = aws_instance.web_server[count.index].id
}
Why count
? All web servers are identical – perfect for horizontal scaling.
Use Case 3: Regional Deployment (Use for_each
)
Business Problem: Multi-region deployment with region-specific settings
# Business need: Compliance with data sovereignty laws
locals {
regions_config = {
"us-east-1" = {
cidr_block = "10.1.0.0/16"
compliance = "hipaa"
business_unit = "healthcare"
}
"eu-west-1" = {
cidr_block = "10.2.0.0/16"
compliance = "gdpr"
business_unit = "europe-retail"
}
"ap-southeast-1" = {
cidr_block = "10.3.0.0/16"
compliance = "local"
business_unit = "asia-expansion"
}
}
}
# for_each is essential - each region has different business rules
resource "aws_vpc" "regional" {
for_each = local.regions_config
cidr_block = each.value.cidr_block
tags = {
Region = each.key
Compliance = each.value.compliance
BusinessUnit = each.value.business_unit
}
}
Why count
? Simple on/off switch for resource creation.
The SAFETY of for_each
# SAFE - Each resource has stable identity
resource "aws_security_group" "app" {
for_each = toset(["web", "app", "database"])
name = "sg-${each.key}"
}
# Removing "app" from the set only destroys that specific resource
# No unintended destruction/recreation of other resources
Decision Framework for Business
Choose count
when:
- Identical Resources: All instances are the same
- Simple Scaling: Just need more of the same thing
- Feature Flags: Conditional resource creation
- Temporary Resources: Test environments, ephemeral workloads
Choose for_each
when:
- Unique Configurations: Each resource has different settings
- Stable Infrastructure: Production environments where stability matters
- Business Logic: Different departments, cost centers, compliance needs
- Map Data: You have natural keys for your resources
Final Thoughts for Beginners
Remember These 3 Rules:
- “If they’re identical, use
count
“ – Perfect for horizontal scaling - “If they’re unique, use
for_each
“ – Essential for different configurations - “When in doubt, start with
for_each
“ – More stable and predictable
Business Value You’re Delivering:
Skill | Business Impact |
---|---|
Using count | Faster scaling, cost optimization |
Using for_each | Consistent environments, reduced errors |
Combining both | Enterprise-grade infrastructure automation |