DevOps Peach

Advanced Terraform · Modules and Code Structure · Lesson 3 of 5

count, for_each, dynamic Blocks and lifecycle

Create many instances with count or for_each (prefer for_each for anything keyed), generate nested blocks with dynamic, and control replacement with lifecycle.

Practice interactively →

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.

Practice interactively →