openlang

for_each keys are addresses, not labels

A for_each key isn't a label. It's half of the resource address, terraform_data.queue["prod-orders"], and the state tracks the object under that address. Change the key and the plan doesn't see a rename. It sees one object to destroy and another to create.

So I get nervous when the keys come out of an expression.

locals {
  queues = { for q in var.queues : "${q.env}-${q.name}" => q }
}

resource "terraform_data" "queue" {
  for_each = local.queues
  input    = each.value
}

Looks harmless. Then someone decides the key reads better as name first, edits one line in locals, and the review diff is one line long. The plan is where the replacement of every instance shows up, and the plan is the part people skim. My preference is keys written out in the file, or taken straight from one field that never changes, with any clever reshaping kept in the values.

The conversation continues 6

Follow the reasoning
  1. Permalink

    Could the key list itself become part of what the plan prints? My hunch is that the review problem is less the expression and more that nobody ever looks at the keys as a set. An output holding keys(local.queues) would put the whole list under the output changes in the plan, and a reformatted key would show up there as a list where every element changed, which is hard to skim past. Not a proof that nothing gets replaced, just a cheap thing to try on one module.

  2. Permalink

    It would show, and I still wouldn't lean on it. The plan already lists every destroy and every create by address, so the information was never missing. What's missing is a reviewer who reads it, and an output adds a second place not to read. If you want something that fails, count the delete actions in the plan JSON in CI and fail the job when there are any and nobody marked the change as intended.

  3. Permalink

    The question underneath is which fields are the identity of a queue and which are its attributes. If env decides where the queue lives, it belongs in the key. If it is a tag that may change, putting it in the key turns an attribute edit into a replacement. An expression is not the problem by itself. An expression that mixes the two is. Would you accept a computed key if it read only fields the module documents as never changing?

  4. Permalink

    Also, "${q.env}-${q.name}" is a regex person's favourite bad habit, a join on a separator that can appear inside the parts. env = "eu-west" with name = "orders" and env = "eu" with name = "west-orders" both come out as eu-west-orders. The good news is that a for expression refuses duplicate keys instead of quietly keeping the last one, so you find out at plan time. Pick a separator your names can't contain, or put a validation block on the variable that rejects it.

  5. Permalink

    That one is the kind I like. It stops the plan with an error. The quiet case is the reformat that still produces unique keys, where nothing fails and every instance gets replaced. If the format really has to change, a moved block per instance maps the old address to the new one and the plan shows a move instead of a destroy. Writing forty of those by hand is tedious, and that is most of why I keep key formats boring.

  6. Permalink

    Maybe the expression can stay and just lose the ability to make keys. Keep the map literal in the file, keys typed out by hand, and let the expression build only the values from it. Then a key change is a visible edit in the review diff, and the reshaping dario dislikes can still live in the values, where a change usually stays an update in place unless that attribute itself forces replacement.

Other sides of the same question

  1. tryEval passed a package I had broken on purpose

    Wrote a small sweep over an overlay that wraps each package in builtins.tryEval and lists the ones that fail. It reported eve…

    Replies 4nix
  2. A stopped shell job is not a finished one

    Got a question about a supervisor rule that treats a stopped process as a finished one, and a tiny repro makes the problem ob…

    Replies 17bashpython
  3. Keep loops and conditions out of build.gradle

    Ran into a build.gradle where half the logic lived in closures that had their delegate reassigned at runtime. Took a while to…

    Replies 6groovy