Meta-arguments work on any resource or module block. They decide how many instances exist, in which order they are built and how changes replace them.
flowchart TB M["var.apps map"] --> FE["for_each = var.apps"] FE --> A1["docker_container.app["api"]"] FE --> A2["docker_container.app["web"]"] L["list of 3"] --> CO["count = 3"] CO --> C0["docker_volume.data[0..2]"] LC["lifecycle"] --> A1 %% step M: A map or set of strings is the input for for_each; each key becomes one instance. %% step FE: for_each addresses instances by key, so removing one key removes only that instance. %% step A1: Inside the block, each.key and each.value give the current element. %% step CO: count creates numbered instances; removing an item from the middle shifts every index after it. %% step LC: lifecycle rules change how Terraform replaces or ignores changes for that resource.
count or for_each
variable "apps" {
type = map(object({ image = string, port = number }))
}
resource "docker_container" "app" {
for_each = var.apps # keys: "api", "web"
name = "shop-${each.key}"
image = each.value.image
ports {
internal = 8080
external = each.value.port
}
}
count |
for_each |
|
|---|---|---|
| Address | docker_volume.data[1] |
docker_container.app["web"] |
| Remove the middle item | Every later index shifts and is replaced | Only that key is destroyed |
| Best for | N identical copies, or count = var.enabled ? 1 : 0 |
Anything with a natural key |
for_each takes a map or a set of strings (toset(var.names)), and its keys must be known at plan time: you cannot use an ID that is computed during apply.
dynamic blocks
Generate repeated nested blocks from a collection:
dynamic "ports" {
for_each = each.value.extra_ports
content {
internal = ports.value
external = ports.value + 10000
}
}
Use them sparingly: a config that is all dynamic blocks is hard to read.
depends_on
Terraform orders resources from references. Add depends_on = [docker_network.shop] only for hidden dependencies that no argument expresses.
lifecycle
| Setting | Effect |
|---|---|
create_before_destroy = true |
Build the replacement first, then delete the old one (less downtime) |
prevent_destroy = true |
Any plan that would destroy it fails |
ignore_changes = [labels] |
Do not revert changes made outside Terraform to these arguments |
replace_triggered_by = [terraform_data.release] |
Replace when another resource or attribute changes |
Traps: switching a resource from count to for_each changes every address, so plan wants to destroy and recreate (fix with moved blocks, module 2); and prevent_destroy does not protect against deleting the block itself from the code.