Map Object with Optional Attributes

I am attempting to write a module to build subnets from a map variable looking something like this…

   subnets = {
     "subnet1" = {
       "address_prefix" = "10.0.1.0/24"
     }
     "subnet2" = {
       "address_prefix" = "10.0.2.0/24"
       "service_endpoints" = [
         "Microsoft.KeyVault",
         "Microsoft.Storage",
       ]
     }
   }

Used like so in the call to the module…

module "subnets" {
  source               = "../../.."
  resource_group_name  = azurerm_resource_group.rsg.name
  virtual_network_name = azurerm_virtual_network.vnet.name
  subnets              = local.settings.virtual_network.subnets
}

Because map attributes must be of the same type, I’m attempting to use a variable like this in the module.

variable "subnets" {
  description = "A map of subnets"
  type = map(object({
    address_prefix    = string
    service_endpoints = list(string)
  }))
}

However, this way requires that every subnet has service_endpoints, which is not always the case. Is there a way to make that optional? The resource I’m using in the module uses for_each…

resource "azurerm_subnet" "subnet" {
  for_each = var.subnets

  name                 = each.key
  resource_group_name  = var.resource_group_name
  virtual_network_name = var.virtual_network_name
  address_prefix       = each.value["address_prefix"]
  service_endpoints    = each.value["service_endpoints"]
}

I haven’t found a way to do optional atributes, either.
But, using empty maps and lists produce the expected result with dynamic and for_any

Not possible yet apparently?

Can you share your solution? I’m also stuck in this situation and can’t use optional as I have older version of TF

Hey all, late to the party here. I faced this issue recently and I hope this can help anyone who finds this post.

The code below can normalize the parent map so that each child map can have the exact same key/value pairs.

locals {
  subnets_defaults = {
    "address_prefix" = ""
    "service_endpoints" = []
  }
  subnets_normed = {for k, v in var.subnets: k =>  merge(local.subnets_defaults,v)}
}

Add default values to the subnets_defaults map. A new normalized map (subnets_normed) will be created which can be used in for_each loops.

If any subnet map lacks one of the default properties, it will be added with the value specified in subnets_defaults. If the property already exists, it will be left as-is; existing properties have priority over defaults.

If you want default properties to have priority over existing, flip v and local.subnets_defaults in the merge function.

Note that Hashicorp is working on an experimental feature that does the same here. I prefer my way since it’s easier to implement.

While searching for an answer, I ran across a blog post which referenced an experimental feature (June 2022) which is the optional() modifier, example:

variable "subnets" {
  description = "A map of subnets"
  type = map(object({
    address_prefix    = string
    service_endpoints = optional(list(string))
  }))
}

The value will then be null.
When trying to use the variable, I wrapped the assignment in a try() like so:

resource "azurerm_subnet" "subnet" {
  for_each = var.subnets

  name                 = each.key
  resource_group_name  = var.resource_group_name
  virtual_network_name = var.virtual_network_name
  address_prefix       = each.value["address_prefix"]
  service_endpoints    = try(each.value["service_endpoints"], "")
}

My use case is different (and I needed a default value), so I did not test the code above, but that should prevent an error when attempting to access the null or non-existent value.

The feature is no longer experimental and is officially documented here.


Note: Gi0rgi0s referred to the experimental feature above, but I noticed it after posting, giving credit where due! :+1: