Best way to handle optional object attribute in module

Hi,

I’ve got a module that builds a VM in Azure based largely on the contents of a map of objects that you pass it. I’ve used the experimental features to make some of these optional and set them values inside the module itself if none are passed.

This is my variable definition for the object:

variable "vm_specifications" {
  description = "Configuration parameters for each Virtual Machine specified"
  type = map(object({
    vm_size            = string
    zone               = string
    publisher          = string
    offer              = string
    sku                = string
    version            = string
    os_disk_type       = optional(string)
    admin_user         = string
    patch_class        = optional(string)
    scheduled_shutdown = optional(bool)
    monitor            = optional(bool)
    backup             = optional(bool)
    enable_host_enc    = optional(bool)

    network = map(object({
      vnet                = string
      vnet_resource_group = string
      subnet              = string
      ip_address          = string
      custom_dns_servers  = optional(list(string))
    }))
    data_disks = optional(map(object({
      size          = number
      lun           = number
      type          = string
      create_option = string
    })))
    tags = map(string)
  }))
}

This is working relatively well in most cases, however I’m struggling to get the data_disks to really be optional and I’m not sure the best way to progress. If I handle it as I have done with most of the other optional attributes, like this:

    data_disks = optional(map(object({
      size          = number
      lun           = number
      type          = string
      create_option = string
    })))

and then set it:

  vm_specifications = defaults(var.vm_specifications, {
    os_disk_type       = "Standard_LRS"
    admin_user         = "user"
    patch_class        = "none"
    scheduled_shutdown = false
    monitor            = false
    backup             = false
    data_disks         = {}
  })

I’m having issues with the places in the module it’s used inside a for expression because it’s a null value. I’m getting:

A null value cannot be used as the collection in a 'for' expression.

There are multiple cases where it’s used in a for but here’s one if it helps at all for context:

locals {
  # Collate disk info along with other parameters that allow disks to be linked to the VM that specified them
  data_disk_config = flatten([
    for vm_key, vm in var.vm_specifications : [
      for disk_key, disk in vm.data_disks : {
        vm_name       = vm_key
        disk_name     = disk_key
        size          = disk.size
        lun           = disk.lun
        type          = disk.type
        create_option = disk.create_option
        tags          = vm.tags
      }
    ]
  ])
}

Is there a better way to handle this? I feel like the flatten just needs to happen conditionally somehow but I’m probably missing something more straightforward.

Thanks

Hi @dal30011,

From your mention of the defaults function I think you must be using an earlier experimental version of this feature. The defaults function was never a stable feature, and recent Terraform versions that have stable support for optional attributes deal with default values in a different way.

In the stable versions of this feature you could define the attribute like this:

    data_disks = optional(
      map(object({
        size          = number
        lun           = number
        type          = string
        create_option = string
      })),
      {},
    )

The second “argument” to optional{} in the example above – acts as a default value to use whenever the attribute would otherwise be null, which means that this attribute’s value will always be compatible with a for expression.


To achieve that with the older version of Terraform you are using requires getting a similar effect with an inline call to a function like coalesce or defaults to replace the null values with non-null values as a separate step. For example:

  for disk_key, disk in coalesce(vm.data_disks, tomap({})) : /* ...etc */

This has a similar effect as the default value in the type constraint in the stable version of the feature, but does the replacement of null with an empty map as an explicit separate step rather than it being handled automatically by Terraform as part of the type conversion process.