Skip to content

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_on when one resource relies on another but Terraform's automatic dependency graph can't detect the relationship through attribute references alone.
  • Using lifecycle rules 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 count combined with a list variable and count.index to give each resource instance a unique value (e.g. a unique filename) instead of creating identical copies.
  • Using the built-in length() function with count so the number of resource instances automatically tracks the size of a list variable, instead of a hardcoded number.
  • Using for_each with 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:

#!/bin/bash

for i in {1..3}
do
   touch /root/pet${i}
done
ls -ltr /root/
-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:

resource "local_file" "pet" {
  filename = var.filename
  content  = var.content
}
variable "filename" {
  default = "/root/pets.txt"
}

variable "content" {
  default = "I love pets!"
}

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:

resource "local_file" "pet" {
  filename = var.filename
  count    = 3
}
terraform apply
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:

resource "local_file" "pet" {
  filename = var.filename[count.index]
  count    = 3
}
variable "filename" {
  default = [
    "/root/pets.txt",
    "/root/dogs.txt",
    "/root/cats.txt",
    "/root/cows.txt",
    "/root/ducks.txt"
  ]
}
ls /root
pets.txt
dogs.txt
cats.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:

resource "local_file" "pet" {
  filename = var.filename[count.index]
  count    = length(var.filename)
}

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:

variable "filename" {
  default = [
    "/root/pets.txt",
    "/root/dogs.txt",
    "/root/cats.txt"
  ]
}

Removing /root/pets.txt from the list leaves:

variable "filename" {
  default = [
    "/root/dogs.txt",
    "/root/cats.txt"
  ]
}
terraform plan
# 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:

output "pets" {
  value = local_file.pet
}
terraform output
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:

resource "local_file" "pet" {
  filename = each.value
  for_each = var.filename
}

Trying this directly with a list variable fails, because for_each only accepts a map or a set:

variable "filename" {
  default = [
    "/root/pets.txt",
    "/root/dogs.txt",
    "/root/cats.txt"
  ]
}
terraform plan
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 plan
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:

resource "local_file" "pet" {
  filename = each.value
  for_each = toset(var.filename)
}
variable "filename" {
  type = list(string)
  default = [
    "/root/pets.txt",
    "/root/dogs.txt",
    "/root/cats.txt"
  ]
}
terraform plan
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:

variable "filename" {
  type = list(string)
  default = [
    "/root/dogs.txt",
    "/root/cats.txt"
  ]
}
terraform plan
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
}
terraform output
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_on sparingly; prefer implicit dependencies through attribute references whenever possible, since they're easier to follow and maintain.
  • Combine lifecycle rules with loop meta-arguments thoughtfully — replacement behavior applies per-instance when resources are looped.
  • Always pair count with a list variable and count.index when each instance needs a unique value — using count alone just creates identical copies.
  • Use length(var.list_name) for count instead of a hardcoded number so the resource count automatically tracks the list's size.
  • Prefer for_each over count whenever list elements might be added, removed, or reordered, to avoid the index-shift replacement problem.
  • When migrating a list variable to for_each, use toset() if you want to keep the variable typed as a list, or change the variable's type to set(string) if duplicates should never be allowed.

Security Best Practices

Security

  • Explicit depends_on relationships 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 for loop's behavior by duplicating resource blocks instead of using Terraform's loop meta-arguments.
  • Overusing depends_on for dependencies Terraform would have inferred automatically from resource attribute references.
  • Forgetting that meta-arguments like lifecycle apply at the resource block level, not globally across the configuration.
  • Setting count to a number without indexing into a list, resulting in identical duplicate resources instead of distinct ones.
  • Hardcoding count to a static number and forgetting to update it (or use length()) 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_each without 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_on defines an explicit dependency when Terraform can't infer one automatically.
  • lifecycle customizes create, update, and destroy behavior for a resource.
  • count creates multiple instances of a resource, turning it into an indexed list ([0], [1], [2]...).
  • count alone creates identical duplicates; pair it with a list variable and count.index for unique values per instance.
  • length(var.list) sizes count dynamically so it tracks the list automatically.
  • count identifies 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_each creates one instance per element of a map or set (not a list), using each.value and each.key instead of count.index.
  • A raw list passed to for_each raises an error; convert it with toset() or change the variable's type to set(string).
  • count produces a list of instances (indexed by position); for_each produces a map of instances (keyed by value) — which is why removing an element only affects that specific instance under for_each.
  • Other meta-arguments exist beyond these four — provisioner, provider, and backend — 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, and for_each.
  • Q: Why would you need depends_on if 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 = 3 without 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 with count.index, e.g. var.filename[count.index].
  • Q: How do you make count automatically match a list's size? Set count = 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? Using for_each instead, which keys instances by value rather than position.
  • Q: What data types does for_each accept? 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-in toset() function, or change the variable's declared type to set(string).
  • Q: What expressions are used inside a resource with for_each instead of count.index? each.value (and each.key for maps).
  • Q: How does the resulting resource structure differ between count and for_each? count produces a list indexed by position (pet[0], pet[1]...); for_each produces a map keyed by value (pet["/root/cats.txt"]...).