TrademarkTrademark
Features
Documentation

Terraform Optionals On Complex Input Variables

Learn how to use the Terraform optional object type attribute in complex input variables.
Brendan ThompsonOctober 20, 2022
Terraform Optionals On Complex Input Variables
Key takeaways
  • Terraform v1.3 introduced the optional object type attribute, letting you mark individual fields in a complex object variable as optional.
  • Before optional() existed, engineers worked around optional fields by passing empty strings or splitting properties into separate variables with defaults, which got messy at scale.
  • Writing an attribute as optional(string) makes it optional, and optional(string, 'StorageV2') sets a per-attribute default value when none is supplied.
  • Using optional() lets you keep configuration in single complex objects with sensible per-property defaults instead of building separate variables for each optional field.

Terraform v1.3 shipped what I think is one of the most useful features since v1.0: the optional object type attribute. To see why it matters, let's start with a variable for creating a storage account on Azure.

The variables will be at their rawest form, no validation or description.

variable "storage_account" {
type = object({
  name                = string
  resource_group_name = string
  location            = string
  kind                = string
  tier                = string
  replication_type    = string
  })
}

This above example has all the properties we need to create a storage account on Azure, however, the kind property is actually optional and given that optional on an attribute doesn't exist yet, we will need to deal with that. There were two ways that I have seen commonly used:

storage_account = {
  name                = "sablt"
  resource_group_name = "rg-blt"
  location            = "australieast"
  kind                = "" //NOTE: We don't want to set this
  tier                = "Premium"
  replication_type    = "LRS"
}

This is what we would set either in a .tfvars file or a module call. We are passing in an empty string on kind and our code would deal with the occurrence of an empty string for us, but we still have to type those four pesky characters. So let's look at the next scenario, which actually requires more typing!

variable "storage_account" {
  type = object({
    name                = string
    resource_group_name = string
    location            = string
    tier                = string
    replication_type    = string
  })
}
 
variable "storage_account_kind" {
  type    = string
  default = ""
}

Now we have two variables, and this could be worse and that entire storage_account variable could be split out into its individual components. And yes, I have seen that happen. We have also set a default for the variable so that we don't ever have to pass anything in if we don't want/need to.

On the surface I would say this doesn't feel so bad, especially when it's something rather simple like this. But, if you were to extrapolate this to more complex situations or where there are many - potentially most - optional properties you are going to end up with a mess of variables. I personally always opt for complex objects as I feel it is far easier to see and understand what's going on and what the requirements of the code are!

Now let's see how the new feature handles the same case.

variable "storage_account" {
  type = object({
    name                = string
    resource_group_name = string
    location            = string
    kind                = optional(string)
    tier                = string
    replication_type    = string
  })
}

Above we can now see the kind is of type optional(string) this means that should we desire to set a value that value will be a string. It can go a step further though. What if we wanted to set a default value just for this property, well now you can!

variable "storage_account" {
  type = object({
    name                = string
    resource_group_name = string
    location            = string
    kind                = optional(string, "StorageV2")
    tier                = string
    replication_type    = string
  })
}

We probably aren't going to set a default value for this particular property as we more so want it to be optional, but the tier for instance we might want to default to something that is common for the business and you deviate from it on an as needs basis.

optional gives us a much better way to set up complex object variables. We can mark some properties as optional and give others a sensible default. No longer will we be required to build out separate variables for properties we want to be optional.

You can follow Brendan @BrendanLiamT on Twitter.

About the author
Brendan Thompsonsolutions engineer at Scalr
Brendan Thompson is a solutions engineer at Scalr, specializing in Terraform and cloud infrastructure.