skip to content

A Terraform config generates CloudFront ordered_cache_behavior blocks with a dynamic block whose for_each is a map. Why is that risky, and what would you use instead?

level: seniorimportance: nice to knowfreq 26%

answer

  1. some nested blocks are a sequence
  2. maps iterate sorted by key
  3. key names would decide precedence
  4. lists preserve configuration order
  5. ordering must be explicit somewhere

basics

~20 s

A map iterates in lexical key order, so the precedence of order-sensitive blocks follows key names rather than intent, and adding or renaming a key silently reshuffles them. Drive order-sensitive nested blocks from a list, where configuration order is preserved.

solid answer

~50 s

CloudFront evaluates `ordered_cache_behavior` blocks in the order they appear — the first matching path pattern wins. A dynamic block over a **map** emits blocks in lexical key order, because Terraform maps are sorted, so precedence ends up decided by how someone happened to name the keys. Rename `api` to `zz-api`, or add a key that sorts early, and the routing changes with no obvious diff intent — a real outage shape, since the block contents look identical in review. For anything order-sensitive I drive the dynamic block from a **list**, where configuration order is preserved and `.key` is the index, and I keep the list in one place so its order is reviewable. If I need map-like lookup ergonomics as well, I sort explicitly with a `for` expression over an ordered list of keys rather than relying on incidental key names.

go deeper

for a junior

Remember that a list preserves the order you wrote and a map is iterated in sorted key order — the collection type is a real decision, not a style choice.

for a middle

Explain why a sorted map is deterministic yet still wrong here: the ordering is decided by key names rather than by anyone's intent.

for a senior

Show how the failure actually reaches production — a clean-looking plan, disjoint patterns hiding the problem, and an overlap introduced months later — and encode the invariant instead of trusting review.

for a principal

Own the convention: order-sensitive configuration must be explicit in a named structure, and plan-time policy or preconditions should enforce it rather than reviewer attention.

## Some nested blocks are a sequence, not a set Most repeated nested blocks are unordered — a security group's `ingress` rules mean the same thing in any order. A minority are genuinely ordered, and for those the block position *is* configuration. CloudFront's `ordered_cache_behavior` is the classic case: the distribution evaluates behaviours in order and the first matching path pattern wins, so moving a `/*` catch-all above `/api/*` silently swallows the API traffic. When you generate such blocks, you have handed the ordering decision to whatever iteration order the collection has. ## What each collection type does to order - **List** — configuration order is preserved, and `.key` is the index. What you read top-to-bottom in the variable is what is emitted. - **Map** — Terraform maps are sorted by key, so elements come out in lexical key order. `"10-api"` precedes `"2-static"` because it sorts as a string, not a number. - **Set** — no meaningful author-controlled order at all; treat it as unordered. The map behaviour is the dangerous one because it *looks* stable. It is deterministic — the same map always yields the same order — so a test suite passes and a plan looks clean. It is just not the order anyone chose deliberately. ## The failure shape Someone adds a behaviour with the key `assets`. It sorts before `api`. The generated distribution now evaluates the assets path pattern first. If those patterns are disjoint nothing happens, and the team learns nothing. Six months later a pattern is broadened, an overlap appears, and traffic goes to the wrong origin. The plan showed the truth — blocks reordered — but a plan for a CloudFront distribution is hundreds of lines and reordered blocks in a list attribute read as noise. The deeper issue: the code stated *what* behaviours exist and left *ordering* implicit. Order-sensitive configuration has to be explicit somewhere, and key naming is the worst possible place for it, because renaming a key is normally a cosmetic change. ## What to do instead **Use a list, and keep it ordered by intent.** ```hcl variable "cache_behaviors" { type = list(object({ path_pattern = string target_origin_id = string })) } dynamic "ordered_cache_behavior" { for_each = var.cache_behaviors content { path_pattern = ordered_cache_behavior.value.path_pattern target_origin_id = ordered_cache_behavior.value.target_origin_id # remaining required arguments omitted for brevity } } ``` The order in the variable is the order that ships, and a reviewer sees a reordering as a diff on the list. **If you want a map for ergonomics, sort explicitly.** Keep the map for lookup, and derive the ordered list from an explicit key sequence: ```hcl locals { behavior_order = ["api", "assets", "default"] ordered = [for k in local.behavior_order : var.behaviors[k]] } ``` Now ordering lives in a named local that says it is ordering, and adding a behaviour without placing it is an error rather than a silent reshuffle. **Add a precondition if the ordering has a rule.** A `precondition` in a `lifecycle` block can assert an invariant such as "the catch-all is last", turning a class of mistakes into a plan-time failure. The block must be written literally, since `lifecycle` cannot itself be generated. ## The general lesson Before generating any nested block, ask whether the provider treats the block sequence as meaningful. If it does, the collection type is not a style preference — it is the thing that encodes semantics, and lexical key order is never the semantics you meant.

  • How would you catch an accidental reordering before it reaches production?
    Read the plan for reordered blocks in the attribute rather than only for added or removed ones, and encode the invariant in code: a `precondition` in the resource's `lifecycle` block can assert that the catch-all pattern is last, failing the plan when the order breaks. A policy-as-code check on the plan can enforce the same rule across repositories.
  • Are there ordered nested blocks outside CloudFront where this matters?
    Yes — any block type the provider documents as ordered. ECS services take `ordered_placement_strategy` blocks whose sequence determines how placement is applied. The rule is to check the provider documentation for wording about order or precedence before generating; if the schema is a list with meaningful order, feed the dynamic block a list.

saying these in an interview costs you the question

  • Assumes a map preserves the order keys were written
  • Thinks iteration order is random and therefore untestable
  • Believes block order never matters to a provider
  • Renames map keys treating it as cosmetic
  • Says a set is fine because it is deterministic

context