Conditionally create resources when a for_each loop is applied

Hello,

I have a problem, as show in the title when I use this inside a dynamic block it works:

dynamic "frontend_ip_configuration" {
    for_each = length(keys(var.frontend_conf_private)) > 0 ? [1] : []

    content {
    ...
    }
}

But, if I use the same strategy in a resource, by example:

resource "azurerm_firewall_policy" "firewall_policy_child" {
  for_each = var.type == "child" ? [1] : []
 
  name                = var.name
  resource_group_name = var.resource_group_name
  location            = var.location
}

I received this error:

Error: Invalid for_each argument
│ 
│   on modules\firewall_policy\firewall_policy.tf line 27, in resource "azurerm_firewall_policy" "firewall_policy_child":
│   27:   for_each = var.type == "child" ? [1] : []
│     ├────────────────
│     │ var.type is a string
│
│ The given "for_each" argument value is unsuitable: the "for_each" argument must be a map, or set of strings, and you have provided a value of type list of string.
╵

As I see in the documentation, for_each read only maps and sets, if I use the function toset() in the resource I solved the problem but my doubt is, why it works in dynamic and not in resource?

I’m using terraform:

$ terraform --version
Terraform v1.7.4
on windows_amd64

Hi @oscarenzo,

I’ve not spotted this before but you are indeed correct.

It appears that the for_each meta argument which you use at a resource level is different from the for_each that is available in a dynamic block.

I did a quick documentation check and this behaviour is detailed. Specifically:

for_each meta argument: The for_each meta-argument accepts a map or a set of strings…

vs.
Dynamic Blocks - Configuration Language

  • “A dynamic block acts much like a for expression”
  • “The for_each argument provides the complex value to iterate over.”
  • “…the for_each argument accepts any collection or structural value.…”

So it does seem that the valid types to pass into the for_each meta-argument do differ from the for_each argument which is part of the dynamic block and this is illustrated by your examples.

Does seem strange that they are inconsistent in this. I wonder if one of the Terraform team (perhaps @apparentlymart ?) could explain why this is the case.

Aside from this, and as you show, the way to pass a ‘list’ into a for_each meta argument is to use the toset() function.

Happy Terraforming

With for_each in a resource or module block, Terraform must somehow select a key to track each instance. When you use a map Terraform uses the map keys as the instance keys, and when you use a set of strings Terraform uses those strings.

A list isn’t valid in that context because that data type doesn’t offer a clear answer for what the instance keys ought to be. Early experimental versions of for_each allowed lists and used the numeric list element indices as the instance keys, making it therefore behave more like count, but we repeatedly saw people being confused by that behavior during testing and so made it forbidden instead to encourage authors to explicitly translate their lists into maps and thus explicitly state what instance key scheme Terraform should use.

dynamic blocks have significantly different requirements since they are not tracked in any way by Terraform and instead they literally generate zero or more blocks of the given type (as if you had written them out manually) and then the provider itself decides how to interpret those blocks. Since there are no “instance keys” for a dynamic block – a nested block has no special address for Terraform to track it by – there is no need for the restriction there and so it accepts any type for which there is a meaningful idea of visiting each element in turn.

Using a list as the for_each value for a dynamic block can be important if the provider chooses to interpret the blocks of that type as being in a meaningful order and so reordering them would constitute a change to be applied, because none of the other collection types can preserve an arbitrary element order. On the other hand, Terraform is the one responsible for tracking the instances of a resource or module and Terraform never considers what order they are declared in, so using a list in that context is never needed.

Brilliant! Thanks for the explanation @apparentlymart. Certainly makes sense now you have described the internals. I love learning this stuff :slight_smile:

Both this difference (that I had just not picked up on before) and the explanation why is definitely going to find it’s way into the Advanced Terraform training I run internally (with a link to this thread).