4.05 Terraform Meta-Arguments
Overview
Meta-arguments are special arguments that can be used inside any resource block to change how Terraform creates, updates, or destroys that resource — independent of the resource's own type-specific arguments.
Abstract
Meta-arguments like depends_on, lifecycle, count, and for_each modify resource behavior at the Terraform engine level rather than configuring the underlying infrastructure itself. count and for_each both turn a single resource block into multiple instances, but for_each keys those instances by value instead of position, avoiding the index-shift problem that count has.
Why It Matters in Production
Real infrastructure often needs more than a single instance of a resource, explicit ordering between resources, or custom create/destroy behavior. Meta-arguments give you that control directly in HCL, without resorting to external scripting or manual repetition — keeping the resource lifecycle declarative and version-controlled.
Key Concepts
| Meta-Argument | Purpose |
|---|---|
depends_on |
Defines an explicit dependency between resources when Terraform can't infer it automatically |
lifecycle |
Customizes create/update/destroy behavior (create_before_destroy, prevent_destroy, ignore_changes) |
count |
Creates multiple instances of a resource from a single block; resulting resource becomes a list, indexed [0], [1], [2]... |
count.index |
Inside a resource using count, refers to the current iteration's index (0-based) — used to pick a unique value per instance, e.g. from a list variable |
for_each |
Creates multiple instances of a resource from a map or set; resulting resource becomes a map keyed by value, avoiding count's index-shift problem |
each.value / each.key |
Inside a resource using for_each, refers to the current iteration's value or key |
Common Use Cases
- Using
depends_onwhen one resource relies on another but Terraform's automatic dependency graph can't detect the relationship through attribute references alone. - Using
lifecyclerules to control resource replacement order or prevent accidental destruction, as covered in lifecycle rules documentation. - Needing to provision multiple similar resources — such as several files, servers, or storage buckets — without duplicating the same resource block over and over.
- Using
countcombined with a list variable andcount.indexto give each resource instance a unique value (e.g. a unique filename) instead of creating identical copies. - Using the built-in
length()function withcountso the number of resource instances automatically tracks the size of a list variable, instead of a hardcoded number. - Using
for_eachwith a map or set so resource instances are keyed by value rather than position, avoiding unrelated replacements when an element is added or removed from the middle of a collection.
Example Configuration or Commands
The problem: creating multiple resources
A shell script can easily create several files in a loop:
-rw-r--r-- 1 root root 0 Sep 9 02:04 pet2
-rw-r--r-- 1 root root 0 Sep 9 02:04 pet1
-rw-r--r-- 1 root root 0 Sep 9 02:04 pet3
| Iteration | filename |
|---|---|
| 1 | /root/pet1 |
| 2 | /root/pet2 |
| 3 | /root/pet3 |
A single Terraform resource block, by contrast, only describes one resource instance:
Repeating this block manually for every file isn't practical at scale. Terraform's count meta-argument solves this directly.
count — basic usage
Adding count to a resource block tells Terraform to create that many instances:
local_file.pet[2]: Creating...
local_file.pet[0]: Creating...
local_file.pet[1]: Creating...
local_file.pet[0]: Creation complete after 0s
[id=7e4db4fbfdbb108bdd04692602bae3e9bd1e1b68]
local_file.pet[2]: Creation complete after 0s
[id=7e4db4fbfdbb108bdd04692602bae3e9bd1e1b68]
local_file.pet[1]: Creation complete after 0s
[id=7e4db4fbfdbb108bdd04692602bae3e9bd1e1b68]
Apply complete! Resources: 3 added, 0 changed, 0 destroyed.
The resource is now a list, indexed pet[0], pet[1], pet[2]. The problem: every instance still uses the same var.filename value, so all three "instances" point at the same file rather than three distinct files — defeating the purpose.
count with a list variable and count.index
To give each instance a unique filename, use a list variable and reference the current iteration with count.index:
variable "filename" {
default = [
"/root/pets.txt",
"/root/dogs.txt",
"/root/cats.txt",
"/root/cows.txt",
"/root/ducks.txt"
]
}
count.index starts at 0, so pet[0] picks up var.filename[0] (pets.txt), pet[1] picks up index 1 (dogs.txt), and pet[2] picks up index 2 (cats.txt). Even though the list above has five elements, count = 3 means only the first three are used.
Using length() to size count automatically
A static count = 3 won't pick up new elements added to the list later. Use the built-in length() function so count always matches the list's size:
With the five-element filename list above, length(var.filename) evaluates to 5, so Terraform creates five instances instead of three — no manual count update needed when the list changes.
The index-shift problem
count has a significant drawback: resource instances are identified purely by their numeric index, not by their content. Removing an element from the middle (or start) of the list shifts every later index down by one, and Terraform treats that as a change to each shifted resource.
Given three files via count.index:
Removing /root/pets.txt from the list leaves:
# local_file.pet[0] must be replaced
-/+ resource "local_file" "pet" {
directory_permission = "0777"
file_permission = "0777"
~ filename = "/root/pets.txt" -> "/root/dogs.txt" # forces replacement
}
# local_file.pet[1] must be replaced
-/+ resource "local_file" "pet" {
directory_permission = "0777"
file_permission = "0777"
~ filename = "/root/dogs.txt" -> "/root/cats.txt" # forces replacement
}
# local_file.pet[2] will be destroyed
| Resource | Resource Update | Action |
|---|---|---|
pet[0] |
/root/pets.txt -> /root/dogs.txt |
Destroy and Replace |
pet[1] |
/root/dogs.txt -> /root/cats.txt |
Destroy and Replace |
pet[2] |
Does not exist | Destroy |
Removing the first element shifted dogs.txt into index 0 and cats.txt into index 1, so Terraform sees pet[0] and pet[1] as changed (forcing replacement) and pet[2] as no longer needed (destroyed) — even though only one file was actually meant to be removed. The end state is correct after apply, but two files were unnecessarily destroyed and recreated to get there. This is resolved using for_each instead of count, covered in the next lecture.
Note
To inspect resource instances created by count as a list, add an output value referencing the resource:
pets = [
{
"directory_permission" = "0777"
"file_permission" = "0777"
"filename" = "/root/pets.txt"
"id" = "da39a3ee5e6b4b0d3255bfef95601890afd80709"
},
{
"directory_permission" = "0777"
"file_permission" = "0777"
"filename" = "/root/dogs.txt"
"id" = "da39a3ee5e6b4b0d3255bfef95601890afd80709"
},
{
"directory_permission" = "0777"
"file_permission" = "0777"
"filename" = "/root/cats.txt"
"id" = "da39a3ee5e6b4b0d3255bfef95601890afd80709"
}
]
for_each — fixing the index-shift problem
for_each replaces count and creates one instance per element of a map or set, using each.value (and each.key, for maps) instead of an index:
Trying this directly with a list variable fails, because for_each only accepts a map or a set:
Error: Invalid for_each argument
on main.tf line 2, in resource "local_file" "pet":
2: for_each = var.filename
The given "for_each" argument value is unsuitable: the "for_each"
argument must be a map, or set of strings, and you have provided a value
of type list of string.
There are two ways to fix this. First, change the variable's type to set(string):
variable "filename" {
type = set(string)
default = [
"/root/pets.txt",
"/root/dogs.txt",
"/root/cats.txt"
]
}
Terraform will perform the following actions:
# local_file.pet["/root/cats.txt"] will be created
+ resource "local_file" "pet" {
+ directory_permission = "0777"
+ file_permission = "0777"
+ filename = "/root/cats.txt"
}
... <output trimmed>
Plan: 3 to add, 0 to change, 0 to destroy.
Second — and this keeps the variable's type as a list — wrap the reference with the built-in toset() function to convert it to a set at the point of use:
variable "filename" {
type = list(string)
default = [
"/root/pets.txt",
"/root/dogs.txt",
"/root/cats.txt"
]
}
Terraform will perform the following actions:
# local_file.pet["/root/cats.txt"] will be created
+ resource "local_file" "pet" {
+ directory_permission = "0777"
+ file_permission = "0777"
+ filename = "/root/cats.txt"
}
... <output trimmed>
Plan: 3 to add, 0 to change, 0 to destroy.
Now repeat the same change made earlier with count — remove /root/pets.txt from the list:
Terraform will perform the following actions:
# local_file.pet["/root/pets.txt"] will be destroyed
+ resource "local_file" "pet" {
+ directory_permission = "0777"
+ file_permission = "0777"
+ filename = "/root/pets.txt"
}
... <output trimmed>
Plan: 0 to add, 0 to change, 1 to destroy.
Only local_file.pet["/root/pets.txt"] is destroyed. The dogs.txt and cats.txt instances are untouched, because they're identified by their value, not by a position that shifts when an element is removed.
Inspecting for_each output
resource "local_file" "pet" {
filename = each.value
for_each = toset(var.filename)
}
output "pets" {
value = local_file.pet
}
pets = {
"/root/cats.txt" = {
"directory_permission" = "0777"
"file_permission" = "0777"
"filename" = "/root/cats.txt"
"id" = "da39a3ee5e6b4b0d3255bfef95601890afd80709"
}
"/root/dogs.txt" = {
"directory_permission" = "0777"
"file_permission" = "0777"
"filename" = "/root/dogs.txt"
"id" = "da39a3ee5e6b4b0d3255bfef95601890afd80709"
}
}
count vs for_each output
# count output (a list)
pets = [
{
"directory_permission" = "0777"
"file_permission" = "0777"
"filename" = "/root/pets.txt"
"id" = "da39a3ee5e6b4b0d3255bfef95601890afd80709"
},
{
"directory_permission" = "0777"
"file_permission" = "0777"
"filename" = "/root/dogs.txt"
"id" = "da39a3ee5e6b4b0d3255bfef95601890afd80709"
},
{
"directory_permission" = "0777"
"file_permission" = "0777"
"filename" = "/root/cats.txt"
"id" = "da39a3ee5e6b4b0d3255bfef95601890afd80709"
}
]
# for_each output (a map, keyed by value)
pets = {
"/root/cats.txt" = {
"directory_permission" = "0777"
"file_permission" = "0777"
"filename" = "/root/cats.txt"
"id" = "da39a3ee5e6b4b0d3255bfef95601890afd80709"
}
"/root/dogs.txt" = {
"directory_permission" = "0777"
"file_permission" = "0777"
"filename" = "/root/dogs.txt"
"id" = "da39a3ee5e6b4b0d3255bfef95601890afd80709"
}
}
count produces a list, indexed by position; for_each produces a map, keyed by value. That's the core reason for_each avoids the unwanted replacements seen with count.
Note
Beyond depends_on, lifecycle, count, and for_each, Terraform has other meta-arguments such as provisioner, provider, and backend configuration — covered later in the course.
depends_on
Used when Terraform can't automatically infer a dependency between two resources:
resource "local_file" "pet" {
filename = var.filename
content = var.content
depends_on = [
random_pet.my-pet
]
}
resource "random_pet" "my-pet" {
prefix = var.prefix
separator = var.separator
length = var.length
}
Here, local_file.pet is explicitly told to wait for random_pet.my-pet, even though no attribute reference between them exists to make that dependency obvious to Terraform.
lifecycle
Used to control how a resource is replaced or whether it can be destroyed at all:
resource "local_file" "pet" {
filename = "/root/pets.txt"
content = "We love pets!"
file_permission = "0700"
lifecycle {
create_before_destroy = true
}
}
This ensures a replacement resource is created before the old one is destroyed, rather than Terraform's default destroy-first behavior.
Best Practices
Best Practices
- Reach for meta-arguments before reaching for external scripts — they keep resource behavior declarative and visible in the same configuration.
- Use
depends_onsparingly; prefer implicit dependencies through attribute references whenever possible, since they're easier to follow and maintain. - Combine
lifecyclerules with loop meta-arguments thoughtfully — replacement behavior applies per-instance when resources are looped. - Always pair
countwith a list variable andcount.indexwhen each instance needs a unique value — usingcountalone just creates identical copies. - Use
length(var.list_name)forcountinstead of a hardcoded number so the resource count automatically tracks the list's size. - Prefer
for_eachovercountwhenever list elements might be added, removed, or reordered, to avoid the index-shift replacement problem. - When migrating a list variable to
for_each, usetoset()if you want to keep the variable typed as a list, or change the variable's type toset(string)if duplicates should never be allowed.
Security Best Practices
Security
- Explicit
depends_onrelationships can hide the real reason a dependency exists; document why it was added so a future change doesn't break ordering for unclear reasons. - When provisioning multiple resource instances via loops, ensure sensitive values (credentials, secrets) aren't duplicated across instances in a way that increases exposure.
Do and Don't
| ✅ Do | ❌ Don't |
|---|---|
Use depends_on only when implicit dependency detection fails |
Add depends_on everywhere "just in case" |
Use lifecycle to express intended create/destroy behavior in code |
Manage resource ordering or protection outside of Terraform with manual scripts |
Pair count with a list variable and count.index for unique values |
Use count alone and expect distinct resource instances |
Use length() to size count dynamically |
Hardcode count to a number that will go stale as the list changes |
Use for_each when list order or membership may change |
Use count on lists that will have elements removed from the middle |
Use toset() or a set(string) type to satisfy for_each's map/set requirement |
Pass a raw list directly to for_each and expect it to work |
Common Mistakes
Common Mistakes
- Trying to replicate a shell
forloop's behavior by duplicating resource blocks instead of using Terraform's loop meta-arguments. - Overusing
depends_onfor dependencies Terraform would have inferred automatically from resource attribute references. - Forgetting that meta-arguments like
lifecycleapply at the resource block level, not globally across the configuration. - Setting
countto a number without indexing into a list, resulting in identical duplicate resources instead of distinct ones. - Hardcoding
countto a static number and forgetting to update it (or uselength()) when the underlying list changes. - Removing an element from the middle or start of a list used with
count, not realizing every later index shifts down and triggers unnecessary destroy-and-replace operations. - Passing a list variable directly to
for_eachwithout converting it to a set or map first, resulting in an "Invalid for_each argument" error.
Troubleshooting
# Confirm resource creation order respects an explicit dependency
terraform plan
# Inspect the dependency graph to verify depends_on is working as expected
terraform graph
# Inspect all instances created by count (a list) or for_each (a map) via an output value
terraform output
# Check whether a count- or for_each-based change will cause unwanted replacement before applying
terraform plan
Real-World Examples
Infra Team — Making an Implicit Dependency Explicit
Scenario: A team provisioned an application server that needed to wait for an IAM role to exist, but no attribute reference connected the two resources directly.
Problem: Terraform occasionally tried to create the server before the IAM role was ready, causing intermittent apply failures.
Solution: Added depends_on to explicitly force the IAM role to be created first.
Outcome: Apply failures from race conditions between the two resources stopped occurring.
Migration Project — Avoiding a Shell-Script Workaround
Scenario: A team needed to provision a handful of nearly identical storage resources and was tempted to wrap Terraform in a shell script loop.
Problem: Scripting around Terraform broke the declarative model — state tracking and plan/apply visibility were lost for the looped resources.
Solution: Used Terraform's loop meta-arguments (count/for_each) instead of external scripting, keeping everything inside native Terraform configuration.
Outcome: All resource instances were fully tracked in state and visible in plan/apply output, with no external tooling required.
SaaS Team — Unexpected Replacements from a Count Index Shift
Scenario: A team used count with a list variable to provision several near-identical config files, indexed by count.index.
Problem: Removing one entry from the middle of the list caused terraform plan to show two unrelated resources being destroyed and replaced, alongside the one intentional deletion — a surprise that nearly went unnoticed before apply.
Solution: Caught the unexpected diff during plan review and migrated the resource to for_each using toset(), which keys instances by value rather than position.
Outcome: Future list changes only affected the specific resource that was added or removed, with no unrelated replacements.
Quick Recap
- Meta-arguments change resource behavior at the Terraform engine level, separate from the resource's own type-specific arguments.
depends_ondefines an explicit dependency when Terraform can't infer one automatically.lifecyclecustomizes create, update, and destroy behavior for a resource.countcreates multiple instances of a resource, turning it into an indexed list ([0],[1],[2]...).countalone creates identical duplicates; pair it with a list variable andcount.indexfor unique values per instance.length(var.list)sizescountdynamically so it tracks the list automatically.countidentifies instances by position, so removing a middle/start element shifts every later index — causing unintended destroy-and-replace operations.for_each(next lecture) solves this.for_eachcreates one instance per element of a map or set (not a list), usingeach.valueandeach.keyinstead ofcount.index.- A raw list passed to
for_eachraises an error; convert it withtoset()or change the variable's type toset(string). countproduces a list of instances (indexed by position);for_eachproduces a map of instances (keyed by value) — which is why removing an element only affects that specific instance underfor_each.- Other meta-arguments exist beyond these four —
provisioner,provider, andbackend— covered later in the course.
Interview / Revision Notes
- Q: What is a meta-argument in Terraform? A special argument usable in any resource block that changes how Terraform manages that resource, rather than configuring the underlying infrastructure.
- Q: Name the meta-arguments covered so far.
depends_on,lifecycle,count, andfor_each. - Q: Why would you need
depends_onif Terraform builds a dependency graph automatically? Because Terraform can only infer dependencies from attribute references; some relationships aren't expressed that way and must be declared explicitly. - Q: What happens if you set
count = 3without indexing into a variable? Terraform creates three identical instances of the same resource rather than three distinct ones. - Q: How do you give each
count-based instance a unique value? Reference a list variable withcount.index, e.g.var.filename[count.index]. - Q: How do you make
countautomatically match a list's size? Setcount = length(var.list_name)instead of a hardcoded number. - Q: What's the main drawback of
count? Instances are identified by numeric index, so removing an element from the middle of the list shifts later indices and causes unrelated resources to be destroyed and replaced. - Q: What solves the index-shift problem with
count? Usingfor_eachinstead, which keys instances by value rather than position. - Q: What data types does
for_eachaccept? A map, or a set of strings — not a list. - Q: How do you use a list variable with
for_each? Wrap it in the built-intoset()function, or change the variable's declared type toset(string). - Q: What expressions are used inside a resource with
for_eachinstead ofcount.index?each.value(andeach.keyfor maps). - Q: How does the resulting resource structure differ between
countandfor_each?countproduces a list indexed by position (pet[0],pet[1]...);for_eachproduces a map keyed by value (pet["/root/cats.txt"]...).