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.