conditional-statementsterraformterraform-variables

Terraform - add key,value to map based on a conditional


I'm trying to create a map in locals where a key is created dynamically based on the value of another variable, and can't find a functional way of doing this in Terraform.

The map contains subnet configuration parameters, in the format:

  <subnet-name> = {
    subnet_id = <subnet-id> 
    nsg_id    = <nsg-id>
    rtbl_id   = <rtbl-id>
  }

And I'm creating it like this:

  snet_mappings = {
    for key in keys(module.subnet.snet-ids) : key => { 
      snet_id = module.subnet.snet-ids[key], 
      nsg_id  = module.nsg.nsg-ids[var.snet_config[key]["nsg"]], 
      rtbl_id = module.route-table.rtbl-ids[var.snet_config[key]["route_table"]]
    }
  }

With the following variable acting as the lookup for the NSG/Route Table to add from the ones created:

snet_config = {
    "testsub" = {
        "address_space" = ["10.10.0.0/24"]
        "nsg"           = "nsg-001"
        "route_table"   = "rtbl-001"
    }
}

This works perfectly so long as I define an NSG and a Route Table every time in the snet_config map. It doesn't work for a case like this however where I don't want the Route Table to be set, so I leave it blank:

snet_config = {
    "testsub" = {
        "address_space" = ["10.10.0.0/24"]
        "nsg"           = "nsg-001"
        "route_table"   = ""
    }
}

This is because it tries to lookup the Route Table ID with an empty string, which therefore fails as it doesn't match a key in the route-table.rtbl-ids map. In the instance where route table isn't set, I actually want my map to look like this:

  <subnet-name> = {
    subnet_id = <subnet-id> 
    nsg_id    = <nsg-id>
  }

i.e. the rtbl_id key isn't set at all. Is it possible to conditionally create keys? I've tried playing around with Terraform conditionals but can't find a working solution.


Solution

  • If you are optionally specifying the key, then you can use the merge, keys, and contains functions with a ternary to accomplish this:

    snet_mappings = {
      for key in keys(module.subnet.snet-ids) : key => merge(
        { 
          snet_id = module.subnet.snet-ids[key], 
          nsg_id  = module.nsg.nsg-ids[var.snet_config[key]["nsg"]],
        },
        contains(keys(var.snet_config[key]), "route_table") ? { rtbl_id = module.route-table.rtbl-ids[var.snet_config[key]["route_table"]] } : {}
      )
    }
    

    alternatively with can:

    snet_mappings = {
      for key in keys(module.subnet.snet-ids) : key => merge(
        { 
          snet_id = module.subnet.snet-ids[key], 
          nsg_id  = module.nsg.nsg-ids[var.snet_config[key]["nsg"]],
        },
        can(module.route-table.rtbl-ids[var.snet_config[key]["route_table"]]) ? { rtbl_id = module.route-table.rtbl-ids[var.snet_config[key]["route_table"]] } : {}
      )
    }
    

    The null coealescing functionality with coalesce cannot be used here as it pertains to an optional key.

    If instead you specify an empty string for route_table when unused and always specify it as a key, then you would conditional off the length of the string:

    snet_mappings = {
      for key in keys(module.subnet.snet-ids) : key => merge(
        { 
          snet_id = module.subnet.snet-ids[key], 
          nsg_id  = module.nsg.nsg-ids[var.snet_config[key]["nsg"]],
        },
        len(module.route-table.rtbl-ids[var.snet_config[key]["route_table"]]) > 0 ? { rtbl_id = module.route-table.rtbl-ids[var.snet_config[key]["route_table"]] } : {}
      )
    }
    

    In all three situations the merge function ensure the extra key-value pair only exists in the returned map based on the conditional, and therefore this satisfies the desired functionality.