Search code examples
terraformhcl

Terraform Ternary Condition Working in Reverse


I have a Terraform module calling a submodule, which also calls another submodule. The final module uses a ternary condition as part of some logic to determine whether a dynamic block should be omitted in a resource definition.

I'm going to only include the pertinent code here, else it would get unnecessarily complicated.

The first module call:

module "foobar" {
  source = "./modules/foobar"
  ...
  vpc_cidr = "10.0.0.0/16"
  # or vpc_cidr = null, or omitted altogether as the default value is null
  ...
}

The second module (in "./modules/foobar"):

module "second_level" {
  source = "./modules/second_level"
  ...
  vpc_config = var.vpc_cidr == null ? {} : { "some" = "things }
  ...
}

The third module (in "./modules/second_level"):

locals {
  vpc_config = var.vpc_config == {} ? {} : { this = var.vpc_config } 
}

resource "aws_lambda_function" "this" {
  ...
  dynamic "vpc_config" {
    for_each = local.vpc_config

    content {
      "some" = vpc_config.value["some"]
    }
  ...
}

This is all horribly simplified, as I'm sure you're already aware, and you might have some questions about why I'm doing things like in the second level ternary operator. I can only say that there are "reasons", but they'd detract from my question.

When I run this, I expect the dynamic block to be filled when the value of vpc_cidr is not null. When I run it with a value in vpc_cidr, it works, and the dynamic block is added.

If vpc_cidr is null however, I get an error like this:

│   32:       security_group_ids = vpc_config.value["some"]
│     ├────────────────
│     │ vpc_config.value is empty map of dynamic

The really odd this is that if I swap the ternary around so it's actually the reverse of what I want, like this: vpc_config = var.vpc_config == {} ? { this = var.vpc_config } : {} everything works as I want.

EDIT

Some more context after the correct answer, because what I'm asking for indeed looks strange.

Wrapping this map into another single-element map with a hard-coded key if it's not empty

I was originally doing this because I needed to iterate just once over the map in the for_each block (and it contains more than a single key), so I'm faking a single key by putting a dummy key in there to iterate over.

As @martin-atkins points out in the answer though, for_each can iterate over any collection type. Therefore, I've simplified the locals assignment like this:

locals {
  vpc_config = length(var.vpc_config) == 0 ? [] : [var.vpc_config]
}

This means that I can run a more direct dynamic block, and do what I really want, which is iterate over a list:

  dynamic "vpc_config" {
    for_each = local.vpc_config

    content {
      subnet_ids         = var.vpc_config["subnet_ids"]
      security_group_ids = var.vpc_config["security_group_ids"]
    }
  }

It's still a little hacky because I'm converting a map to a list of maps, but it makes sense more sense further up the chain of modules.


Solution

  • Using the == operator to compare complex types is very rarely what you want, because == means "exactly the same type and value", and so unlike many other contexts is suddenly becomes very important to pay attention to the difference between object types and map types, map types of different element types, etc.

    The expression {} has type object({}), and so a value of that type can never compare equal to a map(string) value, even if that map is empty. Normally the distinction between object types and map types is ignorable because Terraform will automatically convert between them, but the == operator doesn't give Terraform any information about what types you mean and so no automatic conversions are possible and you must get the types of the operands right yourself.

    The easiest answer to avoid dealing with that is to skip using == at all and instead just use the length of the collection as the condition:

    vpc_config = length(var.vpc_config) == 0 ? {} : { this = var.vpc_config } 
    

    Wrapping this map into another single-element map with a hard-coded key if it's not empty seems like an unusual thing to be doing, and so I wonder if this might be an XY Problem and there might be a more straightforward way to achieve your goal here, but I've focused on directly answering your question as stated.

    You might find it interesting to know that the for_each argument in a dynamic block can accept any collection type, so (unlike for resource for_each, where the instance keys are significant for tracking) you shouldn't typically need to create synthetic extra maps to fake conditional blocks. A zero-or-one-element list would work just as well for generating zero or one blocks, for example.