Disable resource that has all attributes as optional

Hey guys,
It is possible to disable a resource that has all attributes as optional like iam_instance_profile?

I’m trying to write an IAM module that conditionally creates an instance_profile and associates the IAM role with it, but the resource is created even if all attributes are empty.

Example:

# This works and generates values when needed and I don't have any idea on how to disable it.
resource "aws_iam_instance_profile" "it" {}

My module looks like this:

locals {
  roles = tomap({
    for key, value in var.roles : key => {
      ...
      ...
      instance_profile     = tobool(try(value.instance_profile, false))
      ...
      ...
    }
  })
}

resource "aws_iam_role" "it" {
  for_each = local.roles
  name     = each.key
  ...
  ...
}

resource "aws_iam_instance_profile" "it" {
  for_each = {
    for role in keys(resource.aws_iam_role.it) : role => {
      profile = lookup(local.roles[role], "instance_profile") ? role : null
    }
  }
  name = each.value.profile
  role = each.value.profile
}

The thing is that the resource tries to assign the values if I don’t provide anything:

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  # module.iam.aws_iam_instance_profile.it["profile_disabled"] will be created
  + resource "aws_iam_instance_profile" "it" { # This resource shouldn't be created
      + arn         = (known after apply)
      + create_date = (known after apply)
      + id          = (known after apply)
      + name        = (known after apply)
      + path        = "/"
      + tags_all    = (known after apply)
      + unique_id   = (known after apply)
    }

  # module.iam.aws_iam_instance_profile.it["profile_enabled"] will be created
  + resource "aws_iam_instance_profile" "it" { # This is OK
      + arn         = (known after apply)
      + create_date = (known after apply)
      + id          = (known after apply)
      + name        = "profile_enabled"
      + path        = "/"
      + role        = "profile_enabled"
      + tags_all    = (known after apply)
      + unique_id   = (known after apply)
    }

  # module.iam.aws_iam_role.it["profile_disabled"] will be created
  + resource "aws_iam_role" "it" {
      + arn                   = (known after apply)
      + assume_role_policy    = jsonencode(
            {
              + Statement = [
                  + {
                      + Action    = "sts:AssumeRole"
                      + Effect    = "Allow"
                      + Principal = {
                          + Service = "ec2.amazonaws.com"
                        }
                    },
                ]
              + Version   = "2012-10-17"
            }
        )
      + create_date           = (known after apply)
      + force_detach_policies = false
      + id                    = (known after apply)
      + managed_policy_arns   = (known after apply)
      + max_session_duration  = 3600
      + name                  = "profile_disabled"
      + path                  = "/"
      + tags_all              = (known after apply)
      + unique_id             = (known after apply)

      + inline_policy {
          + name   = (known after apply)
          + policy = (known after apply)
        }
    }

  # module.iam.aws_iam_role.it["profile_enabled"] will be created
  + resource "aws_iam_role" "it" {
      + arn                   = (known after apply)
      + assume_role_policy    = jsonencode(
            {
              + Statement = [
                  + {
                      + Action    = "sts:AssumeRole"
                      + Effect    = "Allow"
                      + Principal = {
                          + Service = "ec2.amazonaws.com"
                        }
                    },
                ]
              + Version   = "2012-10-17"
            }
        )
      + create_date           = (known after apply)
      + force_detach_policies = false
      + id                    = (known after apply)
      + managed_policy_arns   = (known after apply)
      + max_session_duration  = 3600
      + name                  = "profile_enabled"
      + path                  = "/"
      + tags_all              = (known after apply)
      + unique_id             = (known after apply)

      + inline_policy {
          + name   = (known after apply)
          + policy = (known after apply)
        }
    }

Plan: 4 to add, 0 to change, 0 to destroy.

Any ideas on how to conditionally disable the iam_instance_profile resource?

Hi @fanfoni,

If you look at the plan output, you can see that you are trying to create an instance called "profile_disabled", when I think what you want is to have no instances at all. I don’t think what you’re looking to do has anything to do with optional attributes, you need to start with the correct for_each value for the resource.

I’m not sure I understand what your expected values are here from the example locals and for_each expression, but the goal here is to not have the key exist for any resources you do not want to create. This would mean filtering out the profile_disabled key and value from the for_each expression. One possibility is adding an additional if clause in the final for expression.

Hello, @jbardin thank you for your reply, but that output is creating a role named profile_enabled and another one named profile_disabled as examples to make easier to understand.

So what is happening is like this:

The role named profile_enabled is creating the profile correctly like this (this is. right):

 + resource "aws_iam_instance_profile" "it" {
      + arn         = (known after apply)
      + create_date = (known after apply)
      + id          = (known after apply)
      + name        = "profile_enabled". # <- Name of the profile (same as the role name)
      + path        = "/" # <- This is default
      + role        = "profile_enabled". # <- Role Attached
      + tags_all    = (known after apply)
      + unique_id   = (known after apply)
    }

The role named profile_disabled is creating the profile even with all values empty (this is wrong):

+ resource "aws_iam_instance_profile" "it" {
      + arn         = (known after apply)
      + create_date = (known after apply)
      + id          = (known after apply)
      + name        = (known after apply)   # <- Ran anyways with no name and no role attribute
      + path        = "/" # <- This is default
      + tags_all    = (known after apply)
      + unique      = (known after apply)
}

The issue is that is possible to create the resource “aws_iam_instance_profile” without any attributes.

So I have no clue on how to conditionally disable the resource because it will always be created even without attributes in it.

# This resource will work like this anywhere in the code.
resource "aws_iam_instance_profile" "anything" {}

The idea is to have a single module to create roles, but not all roles will be Instance Profiles.

I this case, any ideas on how to disable a resource that works like this without creating another submodule just for it?

~/test ❯ ls
main.tf

 ~/test ❯ cat main.tf

resource "aws_iam_instance_profile" "anything" {}

 ~/test ❯ terraform init

Initializing the backend...

Initializing provider plugins...
- Finding latest version of hashicorp/aws...
- Installing hashicorp/aws v3.53.0...
- Installed hashicorp/aws v3.53.0 (signed by HashiCorp)

Terraform has created a lock file .terraform.lock.hcl to record the provider
selections it made above. Include this file in your version control repository
so that Terraform can guarantee to make the same selections by default when
you run "terraform init" in the future.

Terraform has been successfully initialized!

You may now begin working with Terraform. Try running "terraform plan" to see
any changes that are required for your infrastructure. All Terraform commands
should now work.

If you ever set or change modules or backend configuration for Terraform,
rerun this command to reinitialize your working directory. If you forget, other
commands will detect it and remind you to do so if necessary.

 ~/test ❯ terraform plan

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  # aws_iam_instance_profile.anything will be created
  + resource "aws_iam_instance_profile" "anything" {
      + arn         = (known after apply)
      + create_date = (known after apply)
      + id          = (known after apply)
      + name        = (known after apply)
      + path        = "/"
      + tags_all    = (known after apply)
      + unique_id   = (known after apply)
    }

Plan: 1 to add, 0 to change, 0 to destroy.

─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────

Note: You didn't use the -out option to save this plan, so Terraform can't guarantee to take exactly these actions if you run "terraform apply" now.

Hi @fanfoni, what I’m trying to get at is that there is no concept of a “disabled” resource, you either have a resource instance or you do not. Not assigning any attributes does not prevent a resource from being created, though it may result in an error if there are required attributes.

What you can do is create 0 instances of a resource by using count, or removing the keys in the for_each value.

Take this simplified example

locals {
  instances = {
    enabled  = "ok"
    disabled = "notok"
  }
}

resource "null_resource" "unfiltered" {
  for_each = local.instances
  triggers = {
    (each.key) = each.value
  }
}

resource "null_resource" "filtered" {
  for_each = { for k, v in local.instances :
    k => v if k != "disabled"
  }
  triggers = {
    (each.key) = each.value
  }
}

Running a plan will produce:

Terraform will perform the following actions:

  # null_resource.filtered["enabled"] will be created
  + resource "null_resource" "filtered" {
      + id       = (known after apply)
      + triggers = {
          + "enabled" = "ok"
        }
    }

  # null_resource.unfiltered["disabled"] will be created
  + resource "null_resource" "unfiltered" {
      + id       = (known after apply)
      + triggers = {
          + "disabled" = "notok"
        }
    }

  # null_resource.unfiltered["enabled"] will be created
  + resource "null_resource" "unfiltered" {
      + id       = (known after apply)
      + triggers = {
          + "enabled" = "ok"
        }
    }

You can see filtering the disabled key from the for_each value prevents terraform from planning to create null_resource.filtered["disabled"] at all.

Thank you @jbardin,

I was with a misconception regarding how for_each works …

I always thought that “for_each” would work like “count” under the hood and that’s why they couldn’t be together inside the same resource, so in this case, I thought it was a bug or something like that because “count = 0” works.

Maybe it would be better if the instance profile was inside the iam_role resource like this and not standalone…

I honestly thought it was a cleaner way to accomplish this … but that’s Ok.

Thanks for all your help

Hey @jbardin,
I was re-reading your messages and terraform documentation and the if clause solved the issue.

I had a really big misconception on how these loops and conditionals work with Terraform.

I need to polish this a little bit, but now at least work as intended.

Thank you once again.

resource "aws_iam_instance_profile" "it" {
  for_each = {
    for key, value in resource.aws_iam_role.it : key => value
    if lookup(local.roles[key], "instance_profile")
  }
  name = each.key
  role = each.key
  tags = each.value.tags
}