Meta-Arguments Overview
Most arguments inside a resource or module block are defined by the provider — ami, instance_type, cidr, and so on. Meta-arguments are different: they are built into Terraform’s language itself and behave the same regardless of which provider or resource type you use. They control how many instances of a block to create, what it depends on, which provider configuration it uses, and how Terraform treats its lifecycle. Mastering these five arguments unlocks most of the patterns you will reach for in real configurations, and they work identically in OpenTofu.
The five meta-arguments at a glance
Every meta-argument solves a distinct problem. The table below summarizes what each one does and where it applies.
| Meta-argument | Available on | Purpose |
|---|---|---|
count | resource, data, module | Create a fixed number of identical instances, indexed 0..n-1 |
for_each | resource, data, module | Create one instance per key in a map or set, indexed by key |
depends_on | resource, data, module | Declare a hidden dependency Terraform cannot infer |
provider | resource, data, module | Select a non-default (aliased) provider configuration |
lifecycle | resource | Tune create/update/destroy behavior |
You may use either count or for_each on a given block, but never both at once.
count
count repeats a block a fixed number of times. Each instance is addressed by an index through count.index, and the whole set is referenced as a list.
resource "aws_instance" "worker" {
count = 3
ami = "ami-0c2b8ca1dc6cf8160"
instance_type = "t3.micro"
tags = {
Name = "worker-${count.index}"
}
}
This produces aws_instance.worker[0], [1], and [2]. count is ideal for genuinely identical, order-insensitive instances. Its weakness: because instances are tracked by position, removing the middle element shifts every later index and forces Terraform to destroy and recreate resources. See conditionals for the common count = var.enabled ? 1 : 0 toggle pattern.
for_each
for_each creates one instance per element of a map or a set of strings, addressing each by a stable key instead of a numeric position. This avoids the index-shifting problem entirely.
resource "aws_iam_user" "team" {
for_each = toset(["alice", "bob", "carol"])
name = each.value
}
resource "aws_s3_bucket" "logs" {
for_each = {
app = "myapp-app-logs"
web = "myapp-web-logs"
}
bucket = each.value
tags = { Tier = each.key }
}
Inside the block, each.key and each.value expose the current element. Resources become aws_iam_user.team["alice"] and so on. Because keys are stable, adding or removing one entry only affects that entry. Use for expressions to transform a list into the map for_each expects.
Prefer
for_eachovercountwhenever the instances are distinguishable. Switching an existingcountresource tofor_eachlater requiresterraform state mvfor every instance, so choosing well up front saves real pain.
depends_on
Terraform builds its dependency graph automatically from references between resources. depends_on covers the rare cases where a dependency is real but invisible — for example, an IAM policy that must exist before an application can assume a role, even though no attribute links them.
resource "aws_iam_role_policy" "app" {
name = "app-policy"
role = aws_iam_role.app.id
policy = data.aws_iam_policy_document.app.json
}
resource "aws_instance" "app" {
ami = "ami-0c2b8ca1dc6cf8160"
instance_type = "t3.small"
depends_on = [aws_iam_role_policy.app]
}
Use depends_on sparingly; an explicit reference (role = aws_iam_role.app.id) is always preferable because it documents the relationship and is harder to get wrong.
provider
When you define aliased provider configurations, the provider meta-argument points a resource at a specific one. The value is the provider name plus alias, written without quotes.
provider "aws" {
region = "us-east-1"
}
provider "aws" {
alias = "west"
region = "us-west-2"
}
resource "aws_s3_bucket" "replica" {
provider = aws.west
bucket = "myapp-replica-us-west-2"
}
Without this argument, a resource uses the default (unaliased) configuration. This is how you provision across multiple regions or accounts in one configuration.
lifecycle
The lifecycle nested block changes how Terraform plans changes to a resource. Its most-used arguments are listed below.
| Argument | Effect |
|---|---|
create_before_destroy | Provision the replacement before destroying the old instance |
prevent_destroy | Abort any plan that would destroy this resource |
ignore_changes | Skip drift on specified attributes managed outside Terraform |
replace_triggered_by | Force replacement when a referenced value changes |
resource "aws_db_instance" "main" {
identifier = "myapp-prod"
engine = "postgres"
instance_class = "db.t3.medium"
allocated_storage = 20
lifecycle {
prevent_destroy = true
ignore_changes = [allocated_storage]
}
}
A terraform plan that would delete this database stops with an error instead.
Output:
Error: Instance cannot be destroyed
on main.tf line 1:
1: resource "aws_db_instance" "main" {
Resource aws_db_instance.main has lifecycle.prevent_destroy set, but the plan
calls for this resource to be destroyed.
Best Practices
- Reach for
for_eachby default and fall back tocountonly for truly anonymous, fixed-size sets. - Never combine
countandfor_eachon the same block — Terraform rejects it at parse time. - Keep
depends_onas a last resort; model dependencies through attribute references whenever possible. - Add
create_before_destroy = trueto resources fronted by load balancers or DNS to avoid downtime during replacement. - Use
ignore_changesfor attributes mutated by autoscaling or external automation, but list specific attributes rather thanall. - Guard stateful resources (databases, buckets with data) with
prevent_destroyto stop accidental deletion.