Terraform's sets are an anti-pattern
2024-09-19
If you have worked with some moderately elaborate Terraform configurations you have likely run into the following error:
│ Error: Invalid for_each argument
│
│ on auto-turn-off.tf line 149, in resource "aws_iam_role" "scheduler_role":
│ 149: for_each = toset(local.instance_ids)
│ ├────────────────
│ │ local.instance_ids is tuple with 2 elements
│
│ The "for_each" set includes values derived from resource attributes that cannot be determined until apply, and so Terraform cannot determine the full set of keys that will identify the
│ instances of this resource.
│
│ When working with unknown values in for_each, it's better to use a map value where the keys are defined statically in your configuration and where only the values contain apply-time
│ results.
│
│ Alternatively, you could use the -target planning option to first apply only the resources that the for_each value depends on, and then apply a second time to fully converge.
Let's ignore the suggestion of using -target
to apply the configuration in
two steps. This is almost always worse than splitting the project in two.
The Terraform docs do say: "Do not overcomplicate", and "Do not nest modules too much". Turns out, in a similar fashion to the overly repeated argument against Go's simplicity, sometimes the world is just complicated.
Anyway, the above error occurs because some value, instance_ids
in this case,
is generated at runtime and Terraform has no way of determining its value until the
stack is applied. In other words, for_each
requires a static value.
locals {
instance_ids = [for i in module.ec2_instance : i.instance_id]
}
This is bad. Luckily I found a very simple solution. Instead of using a set, use a map, with statically known keys, and dynamically generated values.
locals {
instance_ids_map = {for i, val in module.ec2_instance : i => val.instance_id}
}
See, Terraform, does actually know, how many module.ec2_instance
items we
are creating. It does not need to know their values at planning time,
only their keys.
There are other ways to solve this problem, another alternative is to iterate
over the length of the set
and index the array based on the index. But this
is much less clean, and it forces you to use [i]
everywhere - or worse,
[each.value]
.
The actual problem is that Terraform recommends the use of toset()
and
for_each
in their documentation, which I think is an anti-pattern.