Meta Arguments
Terraform resource blocks normally describe one resource, but real infrastructure often needs several similar objects. You could copy a resource block ten times, but Terraform provides meta-arguments that let one declaration describe multiple instances and control relationships between them.
The most important ones here are count, for_each, and depends_on.
What Are Meta-Arguments?
Meta-arguments are special arguments understood by Terraform itself rather than by the provider as ordinary resource attributes.
For example:
resource "some_provider_server" "web" {
count = 3
name = "web-${count.index}"
}Here count is not an attribute that the provider’s server API necessarily understands. Terraform uses it to determine how many resource instances should exist.
The same idea applies to:
for_each
depends_onThese arguments affect how Terraform manages the resource.
Creating Instances with count
The simplest way to create several similar instances is count.
resource "some_provider_server" "web" {
count = 3
name = "web-${count.index}"
}Terraform creates three resource instances:
some_provider_server.web[0]
some_provider_server.web[1]
some_provider_server.web[2]count.index gives each instance a numeric index.
The names therefore become:
web-0
web-1
web-2The important concept is that count does not create one resource containing three servers. It creates three resource instances.
count and Resource Addresses
Without count:
some_provider_server.webWith:
count = 3Terraform has:
some_provider_server.web[0]
some_provider_server.web[1]
some_provider_server.web[2]Those addresses become part of Terraform’s state identity.
This matters when you later change the collection or count.
Why Indexes Can Become a Problem
Suppose you use a list:
variable "servers" {
default = [
"frontend",
"backend",
"worker"
]
}and:
resource "some_provider_server" "app" {
count = length(var.servers)
name = var.servers[count.index]
}The instances are:
app[0] → frontend
app[1] → backend
app[2] → workerNow remove backend:
variable "servers" {
default = [
"frontend",
"worker"
]
}The indexes become:
app[0] → frontend
app[1] → workerTerraform sees app[1] as the existing instance address, but its desired value has changed from backend to worker.
That means an index-based model can cause an existing instance to be repurposed or replaced depending on which resource attributes changed.
The important issue is identity.
With count, identity is numeric:
0
1
2That is sometimes exactly what you want, but not always.
for_each Gives Instances Keys
When each object has a meaningful identity, for_each is often a better fit.
For example:
variable "servers" {
default = {
frontend = "small"
backend = "medium"
worker = "small"
}
}
resource "some_provider_server" "app" {
for_each = var.servers
name = each.key
size = each.value
}Terraform creates:
some_provider_server.app["frontend"]
some_provider_server.app["backend"]
some_provider_server.app["worker"]Here the identity is the key.
frontend → small
backend → medium
worker → smallThis is much closer to how we naturally think about named infrastructure.
each.key and each.value
Inside a for_each resource:
name = each.key
size = each.valueFor:
{
frontend = "small"
backend = "medium"
}Terraform evaluates the instances as:
frontend → key=frontend, value=small
backend → key=backend, value=mediumSets with for_each
for_each can also work with a set of strings:
variable "servers" {
default = [
"frontend",
"backend",
"worker"
]
}
resource "some_provider_server" "app" {
for_each = toset(var.servers)
name = each.key
}The important property is that each set element becomes an instance key.
You should not depend on list ordering when using a set.
count vs for_each
The practical distinction is:
count | for_each |
|---|---|
| Numeric instance identity | Key-based instance identity |
resource.name[0] | resource.name["frontend"] |
| Good for identical or positional instances | Good for named/distinct instances |
| Collection changes can affect indexes | Keys remain tied to their instances |
| Often simpler for fixed quantities | Often safer for long-lived named resources |
A useful rule is:
Use
countwhen the number of instances is the important thing. Usefor_eachwhen the identity of each instance is important.
This is not an absolute rule, but it is a very useful default.
Changing a for_each Collection
Suppose you have:
for_each = {
frontend = "small"
backend = "medium"
worker = "small"
}and remove backend:
for_each = {
frontend = "small"
worker = "small"
}Terraform sees:
frontend → still exists
worker → still exists
backend → removedThe other keys do not shift.
That is the major identity advantage over a positional list.
The real infrastructure associated with frontend remains associated with:
some_provider_server.app["frontend"]rather than becoming “whatever is now at index 0.”
depends_on as a Meta-Argument
depends_on is also a Terraform meta-argument:
resource "some_provider_server" "web" {
name = "web"
depends_on = [
some_provider_security.policy
]
}It adds an explicit dependency to the graph.
As discussed in the previous chapter, prefer natural resource references when possible:
network = some_provider_network.main.idUse depends_on when the dependency is real but Terraform cannot infer it from an expression.
Do not use it simply to make Terraform execute resources in the order you happen to prefer.
Meta-Arguments and State Identity
count and for_each are especially important because they change resource addresses.
Compare:
some_provider_server.webwith:
some_provider_server.web[0]and:
some_provider_server.web["frontend"]These are different resource instance addresses.
Terraform uses those addresses when tracking instances in state.
This is why changing from one addressing scheme to another is not merely a cosmetic configuration change. Terraform may otherwise interpret it as removing one resource and creating another.
That becomes especially important when you later learn about state-aware refactoring in Chapter 14.
Warning
Before changing count/for_each structure on an existing resource, inspect the resulting plan carefully. You are changing how Terraform identifies resource instances.
Choosing Between Them
Use count when you genuinely want positional instances:
worker-0
worker-1
worker-2Use for_each when the instances have stable identities:
frontend
backend
workerIf you find yourself worrying about what happens when an item is inserted or removed from a list, that is often a sign that keys would describe your infrastructure better than indexes.
The next chapter looks at a different class of Terraform behavior: what happens when a resource argument changes and Terraform cannot update the existing object in place.