The reason the first example worked is due to the behavior of the conditional operator. That operator requires that both of the result arguments have the same type, but as a convenience it tries to find a single type that they can both be converted to and makes that conversion automatically instead of returning an error if possible.
[ ... ] constructs a tuple value, and a tuple can be converted to a set. Because of that, when you use both
["sep", "fusion", "trip"] and
toset(["data-ingest"]) as conditional results, Terraform will select
set(string) as the most general type those two values can both convert to (the second one being a no-op conversion) and so it behaves as if you had written it this way:
for_each = var.separate_namespaces ? toset(local.hubs) : toset(["data-ingest"])
Terraform has these automatic conversions primarily to allow for conveniences such as using strings containing digits where numbers are expected or using
[ ... ] (tuples) where lists and sets are expected, but that design tradeoff also has the downside that sometimes it’s not obvious what conversion was done, like in this case.
In your example with only
local.hubs and no conditional, the language runtime no longer has that hint for automatic type conversion, and so the tuple value is rejected by
for_each argument was intentionally designed not to perform the usual automatic conversion to set, because we (the Terraform team, when doing usability tests on the
for_each feature) saw that the two automatic behaviors we tried (either automatically converting to a set, or automatically using the list indices as instance keys) both had results that were surprising in a way that could be harmful when misunderstood, and so the explicit error check turned out to be the most appropriate compromise. However, when you put other operators from the language in the mix, like the conditional operator in your example, their rules can potentially override the stricter checking Terraform does for the