Search code examples
azureterraformterraform-provider-azureterraform-modules

Making object field optional and adding a block to resource definition only if the field has been set


I have following problem:

We have a module responsible for creating an Azure Event Hubs namespace along with multiple event hubs. The list of event hubs is represented as a list of objects. I want to expand this module with support for Azure Event Hubs Data Capture, however this needs to be present only for some event hubs. I started with adding a capture_description field to the appropriate variable definition of the modules variables definition like here:

variable "event_hubs" {
  description = "List of Event Hubs that will live in the namespace."
  type = list(object({
    name                = string
    message_retention   = number
    partition_count     = number
    consumer_groups     = list(string)
    capture_description = object({
        enabled             = bool
        encoding            = string
        interval_in_seconds = number
        size_limit_in_bytes = number
        skip_empty_archives = bool
        destination         = object({
            name                = string
            archive_name_format = string
            blob_container_name = string
            storage_account_id  = string
        })
    })
  }))
  default = []
}

The problem is that I want the capture_description to be optional so it can be unset for some elements of the list. Then, in main.tf there is a foreach loop creating a resource for each element of the list from variables.tf. I came up with a following solutioon based on "dynamic" block and foreach loop that should have 0 or 1 iterations depending on whether the capture_description has been set. It look like this:

# Create the Event Hub(s)
resource "azurerm_eventhub" "ehub" {
  for_each = local.event_hubs

  name                = each.value.name
  partition_count     = each.value.partition_count
  message_retention   = each.value.message_retention
  namespace_name      = azurerm_eventhub_namespace.namespace.name
  resource_group_name = azurerm_eventhub_namespace.namespace.resource_group_name

  dynamic "capture_description" {
    for_each = each.capture_description == null ? [] : list(var.capture_description)

    content {
        enabled = capture_description.enabled
        encoding = capture_description.encoding
        interval_in_seconds = capture_description.interval_in_seconds
        size_limit_in_bytes = capture_description.size_limit_in_bytes
        skip_empty_archives = capture_description.skip_empty_archives
        destination         = {
            name                = capture_description.destination.name
            archive_name_format = capture_description.destination.archive_name_format
            blob_container_name = capture_description.destination.blob_container_name
            storage_account_id  = capture_description.destination.storage_account_id
        }    
    }
  }
}

However I have serious doubts whether it's going to work as expected. Are you able to evaluate my proposed solution and suggests something that has a non-zero chance of working? Thanks in advance!


Solution

  • To declare an attribute of an object type constraint as being optional you can use the optional modifier.

    variable "event_hubs" {
      description = "List of Event Hubs that will live in the namespace."
      type = list(object({
        name                = string
        message_retention   = number
        partition_count     = number
        consumer_groups     = list(string)
        capture_description = optional(object({
            enabled             = bool
            encoding            = string
            interval_in_seconds = number
            size_limit_in_bytes = number
            skip_empty_archives = bool
            destination         = object({
                name                = string
                archive_name_format = string
                blob_container_name = string
                storage_account_id  = string
            })
        }))
      }))
      default = []
    }
    

    The optional modifier means that when converting an object that doesn't have that attribute Terraform will automatically insert that attribute with the default value null.

    You can use that fact in a dynamic block by using the splat operator [*] to concisely convert the value from a single value that might be null into a list with either zero or one elements:

      dynamic "capture_description" {
        for_each = each.capture_description[*]
    
        content {
          # ...
        }
      }
    

    The [*] operator will produce [] (empty sequence) if its operand is null, or a single-element sequence containing the operand if the operand is not null. Therefore you can use capture_description.value inside the content block to refer to the capture_description object passed in by the caller.