Terraform Optional Object Type Attributes

Terraform Optional Object Type Attributes

If you read my previous posts, you already know that I am big fan of using for_each and object variables when I'm building my modules.

For quite some time, I've been waiting for a particular feature to exit beta and this is the optional object type attribute. The Optional Object type attribute, was in beta for quite some time since Terraform 0.14 and now in Terraform 1.3 (released end of September 2022) it's GA. So to get this straight, this is not a new feature, but now it is 100% ready to be used in production use cases.

When you are building a generic module and you want to offer a lot of possibilities for the people that are going to use it, you will use objects.

Nevertheless, this created a big problem in the past: all the attributes had to be provided by the person using that module and of course, no one will ever need to configure everything a module offers. This meant that you had to use an any type, but if you like to generate documentation with tfdocs, the variables part wouldn't be very helpful. The module code was also pretty ugly, with a lot of lookups to set default values and whatnot.

What does this actually do?

There are two things that you can actually do with this feature when you are using object variables:

  • Set object parameter as optional
  • Set default values to the object's parameters

Why is this important?

You can build better modules, with less code and the documentation will be astonishing when you generate it with tfdocs, making you aware of all of the configurable parameters.

Example Usage

I will show you how I've changed a Terraform Module and what are the differences on the main.tf and variables.tf files.

Old main.tf

resource "helm_release" "this" {
  for_each         = var.helm
  name             = each.value.name
  chart            = each.value.chart
  repository       = lookup(each.value, "repository", null)
  version          = lookup(each.value, "version", null)
  namespace        = lookup(each.value, "namespace", null)
  create_namespace = lookup(each.value, "create_namespace", false) ? true : false

  values = [for yaml_file in lookup(each.value, "values", []) : file(yaml_file)]
  dynamic "set" {
    for_each = lookup(each.value, "set", [])
    content {
      name  = set.value.name
      value = set.value.value
      type  = lookup(set.value, "type", "auto")
    }
  }
  dynamic "set_sensitive" {
    for_each = lookup(each.value, "set_sensitive", [])
    content {
      name  = set_sensitive.value.name
      value = set_sensitive.value.value
      type  = lookup(set_sensitive.value, "type", "auto")
    }
  }
}

As you see, in the old version of the code, I had a lot of lookups and I had to provide the default values on the main.tf version.

New main.tf

resource "helm_release" "this" {
  for_each         = var.helm
  name             = each.value.name
  chart            = each.value.chart
  repository       = each.value.repository
  version          = each.value.version
  namespace        = each.value.namespace
  create_namespace = each.value.create_namespace

  values = [for yaml_file in each.value.values : file(yaml_file)]
  dynamic "set" {
    for_each = each.value.set
    content {
      name  = set.value.name
      value = set.value.value
      type  = set.value.type
    }
  }
  dynamic "set_sensitive" {
    for_each = each.value.set_sensitive
    content {
      name  = set_sensitive.value.name
      value = set_sensitive.value.value
      type  = set_sensitive.value.type
    }
  }
}

0 lookups, less code, easier to read and understand.

Old variables.tf

variable "helm" {
  type        = any
  description = <<-EOT
    name                   = string
    chart                  = string
    repository             = string
    version                = string
    repository             = string
    namespace              = string
    create_namespace       = bool
    values                 = list(string)
    set                    = list(object({
        name   = string
        value  = string
        type   = string
    }))
    set_sensitive          = list(object({
        name   = string
        value  = string
        type   = string
    }))
  EOT
}

In the above variables.tf, I've provided the parameters to the description of the variable, but of course, I did this to help users understand the attributes of my variable. This is not how it's supposed to be done, but I wanted to make tfdocs generate a somewhat readable documentation.

New variables.tf

variable "helm" {
  type = map(object({
    name             = string
    chart            = string
    repository       = optional(string, null)
    version          = optional(string, null)
    namespace        = optional(string, null)
    create_namespace = optional(bool, false)
    values           = optional(list(string), [])
    set = optional(list(object({
      name  = string
      value = string
      type  = optional(string, "auto")
    })), [])
    set_sensitive = optional(list(object({
      name  = string
      value = string
      type  = optional(string, "auto")
    })), [])
  }))
  description = "Helm release parameters"
  default     = {}
}

In the new example, we are using the powerful optional attribute. The same thing that was done by a lookup in the resource code, we can now do with this keyword. This is the Optional syntax: optional(parameter_type, default_value). The simplicity of it, is exactly what we needed in order to speed up module development and to keep up with the maintenance for them.

I am very thrilled with this feature and I totally recommend checking it out.