Pattern to handle optional dynamic blocks

What’s a good way to handle optional dynamic blocks, depending on existence of map keys?

Example: Producing aws_route53_record resources, where they can have either a “records” list, or an “alias” block, but not both. I’m using Terraform 0.12.6.

I’m supplying the entire Route53 zone YAML as a variable, through yamldecode():

zone_id: Z987654321
- name:
  type: A
  ttl: 60

- name:
  type: A
    name: ""
    zone_id: "Z12345678"
    evaluate_target_health: false

Terraform config:

#variable "zone_data" {
#  type = object({
#    zone_id = string
#    records = list(object({
#      name    = string
#      type    = string
#      ttl     = any          # optional
#      records = list(string) # optional
#      alias   = object({     # optional
#        name                   = string
#        zone_id                = string
#        evaluate_target_health = bool
#      })
#    }))
#  })
variable "zone_data" {}

resource "aws_route53_record" "record" {
  for_each = { for v in var.zone_data.records : "${}-${v.type}" => v }
  zone_id  = var.zone_data.zone_id
  name     =
  type     = each.value.type
  ttl      = lookup(each.value, "ttl", null)     # ttl is optional
  records  = lookup(each.value, "records", null) # records is optional

  dynamic "alias" {
    for_each = lookup(each.value, "alias", {})
    content {
      name                   =
      zone_id                = each.value.zone_id
      evaluate_target_health = each.value.evaluate_target_health

What’s a good way to handle the optional “alias” (and “ttl”, “records”) block here?

I’m currently getting errors like these:

  42:       zone_id                = each.value.zone_id
    | each.value is object with 4 attributes

This object does not have an attribute named "zone_id".

Is lookup() the right thing to use when keys may be missing?

Hi @sveniu!

The for_each argument inside a dynamic block is expecting a collection to use as the basis for repetition. In your case it looks like each.value.alias is a single object rather than a list, and so what you need for for_each is either a single-item list when the object is set or a zero-length list when it is null.

Fortunately, that particular situation is a special power of the [*] splat operator when used on something that isn’t a list:

  dynamic "alias" {
    for_each = each.value.alias[*]
    content {
      name                   =
      zone_id                = each.value.zone_id
      evaluate_target_health = each.value.evaluate_target_health

each.value.alias[*] will return [each.value.alias] if each.value.alias is non-null, or [] if it is null.

The other tricky part in this case is that we need to ensure that the records you are iterating over always have the same object type, which means that either records or alias will need to be present and null rather than absent. We can do that normalization with a for expression to transform the result of loading the YAML:

locals {
  zone_data_raw = yamldecode(file("${path.module}/zone_data.yaml"))
  zone_data = {
    zone_id = zone_data_raw.zone_id
    records = [for r in zone_data_raw.records : {
      name    =
      type    = r.type
      ttl     = lookup(r, "ttl", null)
      alias   = lookup(r, "alias", null)
      records = lookup(r, "records", null)

The local.zone_data value should conform to the type constraint you originally wrote for variable "zone_data", and sets alias up in a way that the above technique of using [*] will work.

When working with data loaded from external files (in JSON or YAML format) it’s often a good idea to do something like the above just to ensure that the data in the file conforms to the expected shape and normalize where necessary. This helps ensure that any errors dealing with the file can get reported close to where the file is loaded, rather than downstream when the value is used, and that can be helpful for future maintainers that might not get the YAML structure right on the first try.

1 Like