count, for_each & depends_on
Meta-arguments are special arguments that apply to any resource or module block, regardless of its type. They control how Terraform creates, updates, and destroys resources — letting you create multiple instances from one block, express explicit ordering dependencies, and fine-tune replacement behavior. They're what separate beginner configurations from production-grade ones.
What Are Meta-Arguments?
Terraform resource blocks normally describe exactly one infrastructure object. Meta-arguments extend what a single block can do:
| Meta-argument | Purpose |
|---|---|
count | Create N copies of a resource using an integer |
for_each | Create one copy per element in a map or set |
depends_on | Declare explicit ordering dependencies |
lifecycle | Control create/update/destroy behavior |
provider | Select a non-default provider configuration |
count
count accepts any non-negative integer. Terraform creates that many instances of the resource, each with its own separate state entry:
resource "aws_instance" "web" {
count = 3
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t3.micro"
tags = {
Name = "web-${count.index}"
}
}
This creates three EC2 instances: aws_instance.web[0], aws_instance.web[1], and aws_instance.web[2]. Inside the block, count.index is the zero-based index of the current instance.
You can reference individual instances or all of them:
# Reference a specific instance
output "first_ip" {
value = aws_instance.web[0].private_ip
}
# Collect all private IPs into a list
output "all_ips" {
value = aws_instance.web[*].private_ip
}
count is commonly driven by a variable to make the number configurable:
variable "instance_count" {
type = number
default = 2
}
resource "aws_instance" "app" {
count = var.instance_count
ami = data.aws_ami.ubuntu.id
instance_type = "t3.small"
}
If you have count = 3 and remove the second item from a list used to set names, Terraform destroys [1] and [2] and recreates [1] with the new values. This is why for_each is safer for named resources.
Conditional resources with count
Setting count = 0 effectively removes a resource. This is the idiomatic way to make a resource optional:
variable "create_bastion" {
type = bool
default = false
}
resource "aws_instance" "bastion" {
count = var.create_bastion ? 1 : 0
ami = data.aws_ami.ubuntu.id
instance_type = "t3.micro"
}
Reference it with care — if count = 0, aws_instance.bastion is an empty list, so use one(aws_instance.bastion) or aws_instance.bastion[0] only when count is guaranteed non-zero.
for_each
for_each iterates over a map or set of strings, creating one resource instance per element. Each instance is keyed by the map key or set value — stable keys mean stable state entries:
variable "buckets" {
type = map(string)
default = {
logs = "us-east-1"
backups = "us-west-2"
assets = "eu-west-1"
}
}
resource "aws_s3_bucket" "store" {
for_each = var.buckets
bucket = "myapp-${each.key}"
tags = {
Region = each.value
}
}
Inside the block, each.key is the map key and each.value is the map value. This creates three buckets with stable addresses: aws_s3_bucket.store["logs"], aws_s3_bucket.store["backups"], aws_s3_bucket.store["assets"].
Iterating over a set of strings:
variable "availability_zones" {
type = set(string)
default = ["us-east-1a", "us-east-1b", "us-east-1c"]
}
resource "aws_subnet" "private" {
for_each = var.availability_zones
vpc_id = aws_vpc.main.id
availability_zone = each.key # for sets, each.key == each.value
cidr_block = cidrsubnet(aws_vpc.main.cidr_block, 4, index(tolist(var.availability_zones), each.key))
}
Iterating over a list of objects requires converting to a map first, since Terraform needs stable string keys:
variable "users" {
type = list(object({
name = string
email = string
role = string
}))
}
resource "aws_iam_user" "team" {
for_each = { for u in var.users : u.name => u }
name = each.key
tags = {
Email = each.value.email
Role = each.value.role
}
}
The { for u in var.users : u.name => u } expression is a for expression that converts the list to a map keyed by name. This pattern is extremely common in real configurations.
count vs for_each
| count | for_each | |
|---|---|---|
| Key type | Integer index (0, 1, 2…) | String key from map/set |
| Best for | Identical copies, conditional resources | Named resources, objects with distinct config |
| Reorder safety | Dangerous — index shift destroys downstream | Safe — keys are stable strings |
| Reference syntax | resource.name[0] | resource.name["key"] |
| All instances | resource.name[*].attr | values(resource.name)[*].attr |
Rule of thumb: use count only for truly identical resources or conditional creates. Use for_each for anything with distinct names, configurations, or where the set might change.
depends_on
Terraform automatically infers ordering from references — if resource A references an attribute of resource B, Terraform knows to create B first. But sometimes there's a dependency that isn't expressed through attribute references: for example, an application depends on an IAM policy being fully propagated before it can call AWS APIs.
depends_on lets you encode these hidden dependencies explicitly:
resource "aws_iam_role_policy_attachment" "app" {
role = aws_iam_role.app.name
policy_arn = aws_iam_policy.app.arn
}
resource "aws_instance" "app" {
ami = data.aws_ami.ubuntu.id
instance_type = "t3.small"
iam_instance_profile = aws_iam_instance_profile.app.name
# The instance's startup script calls AWS APIs.
# IAM propagation takes time — wait for the policy to be attached first.
depends_on = [aws_iam_role_policy_attachment.app]
}
depends_on accepts a list of resource or module references. It forces Terraform to fully create/update/destroy the listed resources before touching the block that declares it.
depends_on is a blunt instrument — it serializes operations that Terraform could otherwise parallelize. It also makes the dependency invisible to readers who don't check it. Prefer implicit dependencies (reference the resource's attributes) wherever possible. Reserve depends_on for genuinely non-obvious ordering requirements like IAM propagation delays or external provisioning scripts.
depends_on also works on modules:
module "app" {
source = "./modules/app"
depends_on = [module.network]
}
lifecycle
The lifecycle block controls how Terraform handles create, update, and destroy operations for a specific resource. It has four arguments:
create_before_destroy
By default, Terraform destroys the old resource before creating the replacement. For resources where downtime is unacceptable, reverse this:
resource "aws_instance" "app" {
ami = data.aws_ami.ubuntu.id
instance_type = "t3.small"
lifecycle {
create_before_destroy = true
}
}
Terraform creates the new instance first, then destroys the old one. Useful for load-balanced instances and database replicas.
prevent_destroy
Prevents Terraform from destroying the resource — any plan that would destroy it fails with an error:
resource "aws_rds_cluster" "primary" {
cluster_identifier = "prod-db"
engine = "aurora-postgresql"
lifecycle {
prevent_destroy = true
}
}
ignore_changes
Tells Terraform to ignore drift in specific attributes — useful for attributes managed outside Terraform at runtime:
resource "aws_autoscaling_group" "app" {
min_size = 2
max_size = 10
lifecycle {
ignore_changes = [desired_capacity, tag]
}
}
replace_triggered_by
Forces resource replacement when another resource or attribute changes — even if the resource itself hasn't changed:
resource "aws_instance" "app" {
ami = data.aws_ami.ubuntu.id
instance_type = "t3.small"
lifecycle {
# Replace the instance whenever the launch template changes
replace_triggered_by = [aws_launch_template.app.id]
}
}
provider
When you have multiple configurations of the same provider (e.g., multiple AWS regions), use provider to select which one applies:
provider "aws" {
region = "us-east-1"
}
provider "aws" {
alias = "west"
region = "us-west-2"
}
resource "aws_s3_bucket" "east" {
bucket = "myapp-east"
# uses the default provider (us-east-1)
}
resource "aws_s3_bucket" "west" {
bucket = "myapp-west"
provider = aws.west # use the aliased provider
}
Key Takeaways
countcreates N identical instances indexed by integer. Use it for conditional resources (count = var.enabled ? 1 : 0) and truly identical copies.for_eachcreates one instance per map key or set element with stable string keys. Prefer it overcountfor named resources — reordering won't cascade destroys.- Convert lists to maps with
{ for item in list : item.key => item }to usefor_eachwith object lists. depends_onencodes non-obvious ordering requirements. Use it sparingly — it serializes parallel work and hides intent. Implicit references are better.lifecycleblocks fine-tune creation, replacement, and destruction.create_before_destroyeliminates downtime during replacement.prevent_destroyprotects critical resources.ignore_changesexempts runtime-managed attributes.providerselects a non-default provider alias — essential for multi-region and multi-account configurations.