Conditionally create resources when a for_each loop is involved

A technique for the conditional creation of resources is to have a line immediately after the one with name of the resource like:

count = var.create_resource ? 1 : 0

However, for a resource that contains a for_each loop this does not work.

Is there a way of coding a resource that includes a for_each loop in such away that the resources only get created if a boolean variable is set to true ?.

1 Like

Hi @chrisadkin,

The key thing about for_each is that it declares one resource instance for each element in the map (or set) assigned to it.

Building on that, the key to your question is to make sure that the for_each map has zero elements in the cases where you want to create nothing. If you have a sort of ā€œall or nothingā€ situation ā€“ where youā€™ll disable all of the elements together in the ā€œoffā€ case ā€“ then a relatively concise way to write it is to write a for expression with an if clause that always evaluates to true only if your condition is enabled:

  for_each = { for k, v in var.some_map : k => v if var.enabled }

Typically when writing an expression like this the if clause would contain a reference to k and/or v, but since this is an ā€œall or nothingā€ situation the result depends only on some external flag and doesnā€™t vary for each element.

If you do want to vary the behavior by element then you can certainly do that too. For example:

  for_each = {
    for k, v in var.some_map : k => v
    if contains(var.enabled_keys, k)
  }

(the above is assuming that var.enabled_keys is a set(string) value containing the keys of the var.some_map elements that ought to be enabled, just as a contrived example of an item-specific condition.)

9 Likes

You canā€™t have count and for_each on a data source, resource or module.
A trick to filter a map to remove items based on your condition.

variable "mymap" {
    type = map(object({
        attribute = string
        condition = bool
    }))

    default = {
        key1 = {
            attribute = "value"
            condition = true
        }
        key2 = {
            attribute = "value"
            condition = false
        }
        key3 = {
            attribute = "value"
            condition = true
        }
    }
}

resource "null_resource" "test" {
  for_each = { for k in compact([for k, v in var.mymap: v.condition ? k : ""]): k => var.mymap[k] }
}
1 Like

Hi @apparentlymart,
It is possible to conditionally create a resource with resource just in place and leaving resource address unchanged ?

e.g.

(starting code)

module "ec2one" {
  source = "..."
  ...
}

will produce resource address:

module.ec2one.<attributes>

(refactoring with count)

module "ec2one" {
  source = "..."
  count = var.ec2one_created ? 1 : 0  
  ...
}

will produce resource address:

module.ec2one[0].<attributes>

(refactoring with for_each)

module "ec2one" {
  source = "..."
  for_each = { for k in ["created"] : k => k if var.ec2one_created }
  ...
}

will produce resource address:

module.ec2one[0].created.<attributes>

(refactoring without address changes)

module "ec2one" {
  source = "..."
  <some conditional method not changing resource address>
  ...
}

will produce resource address as original:

module.ec2one.<attributes>

In reality there is no such thing as ā€œconditional createā€, just the ability to choose the number of resources to create (we just happen to only choose zero or one). Therefore there has to be a way to handle the count/for_each creating more than 1 resource - hence the change to either [1] or [ā€œsomethingā€].

So in short, no adding count of for_each would adjust the resource name, but you could easily update any existing resource using terraform state mv

I tried terrform import with some trouble.

Terraform import aws_instance force replacement for network_interface

I added count to an aws_instance (e.g. bastionaws1) to allow conditional creation, so I need to terraform import new bastionaws1[0] linked resources and remove old ones (basrionaws1). I correctly imported aws_instance, the instance has attached ENI so I also imported resource network_interface. On terraform plan I get that instance will be replaced becouse network_interface force replacement:

 # module.bastionaws1[0].aws_instance.this must be replaced
-/+ resource "aws_instance" "this" {
   ...
     + network_interface { # forces replacement
         + delete_on_termination = false
         + device_index          = 0
         + network_interface_id  = "eni-..."
       }

Not tried terraform state mv yet but probably Iā€™ll get same result.

Using terraform state mv worked

thanks @stuart-c

It would be very useful if terraform could handle single element list as a single normal resource. This could simplify later adoption of conditional resources creation avoiding complex, risky and boring refactoring.

FYI, before finding this discussion I solved the problem this way:

for_each = var.create-bastions ? toset(local.users) : []

Worked For Me ā„¢

3 Likes

Iā€™m trying to do something similar, but Iā€™m not sure I can apply these examples to my code. I have a terraform script that will create multiples website based upon the contents of a json file which Iā€™m loading as a local variable. I want to add a value to the json file to control if a site is created in the production environment.

So the json file looks something like this

[
    {
        "site_name": "demosite1",
        "site_url": "some.url.com",
        "create_prod_site" : true
    },
    {
        "site_name": "demosite2",
        "site_url": "another.url.com",
        "create_prod_site" : false
    }
]

I then loop over the json using a for_each loop like this.

module "site" {

    for_each = { for site in local.site_data : site.site_name => site }

    source            = "./site"
    site_name         = each.value.site_name
    site_url          = each.value.site_url

}

I would like to only create the resource if var.environment != ā€œprodā€ or each.value.site.create_prod_site == true.

Iā€™m not quite sure how to handle that, any help would be appreciated.

Hi,

I had this exact issue a week or two ago.

You can add a ā€˜ifā€™ condition to the for_each.

for_each = { for site in local.site_data : site.site_name => site if var.environment != "prod"}

Iā€™m not quite sure how to add both conditionals to the if, but Iā€™m sure someone will add that too :slight_smile:

2 posts were split to a new topic: For_each with an extra condition