We can have default values and nice data structures for our Terraform modules
TLDR
- By using local variables in combination with your normal variables you can let users of your modules pick and choose which parameters you want to be set while maintaining your data structure
- Demo repo here: mencarellic/terraform-aws-smart-defaults
Every engineering team that uses Terraform for production infrastructure eventually gets to a point where not everyone writing Terraform knows everything about the modules being used. The more complex modules might have dozens of parameters, some that are used rarely or in very specific configurations.
Of course there’s the argument for separating these cases out into their own modules, but eventually you’ll just have too many modules. Now there is a solution for this that’s built in and easy to use: The default option:
variable "bucket-acl" {
type = string
description = "The canned ACL to apply"
default = "private"
}
However, when you start having complex modules, you may opt to have more complex data structures, sticking with S3 for example:
variable "bucket" {
description = "Object containing bucket properties"
type = object(
{
name = string
acl = string
versioning = bool
}
)
default = {
name = null
acl = "private"
versioning = true
}
}
There’s a lot of benefit in a data structure like this:
- Helps developers understand the namespace and resources that a parameter may interact with.
- Reduces redundant parameter naming.
With a data structure above you can have an S3 resource that just references items in the bucket
object:
resource "aws_s3_bucket" "this" {
bucket = var.bucket.name
acl = var.bucket.acl
versioning {
enabled = var.bucket.versioning
}
}
However, what if I only want to set bucket.name
instead of the entire object? You’ll likely see something like this:
Why Not Just Use Optional()?
Now Terraform since 0.14
has had an optional()
experimental function: Experimental: Optional Object Type Attributes which looks and sounds great:
To mark an attribute as optional, use the additional optional(...)
modifier around its type declaration:
variable "with_optional_attribute" {
type = object({
a = string # a required attribute
b = optional(string) # an optional attribute
})
}
And then you read the next couple of lines and find out that Terraform just assigns unset optional parameters as null
. This means you’d still need to do something like this:
resource "aws_s3_bucket" "this" {
bucket = var.bucket.name
acl = var.bucket.acl == null ? "private" : var.bucket.acl
versioning {
enabled = var.bucket.versioning
}
}
That turns into a lot of overhead and becomes difficult to manage the fallback defaults.
Smart Defaults
That’s where what I call smart defaults come in. Leveraging some local variables, you can accomplish have a complex data structure in your module and call a single parameter in that structure without needing to specify the rest of them:
locals {
bucket-defaults = {
name = null
acl = "private"
versioning = true
}
bucket = merge(local.bucket-defaults, var.bucket)
}
variable "bucket" {
description = "Bucket parameters"
type = any
default = {}
}
With the local variables above, you’ll want to remove the type
argument from your variable declaration otherwise Terraform will still want all of these parameters set. Then you can move your default
declaration for the variable to the local block.
Your resource block ends up looking like:
resource "aws_s3_bucket" "this" {
bucket = local.bucket.name
acl = local.bucket.acl
versioning {
enabled = local.bucket.versioning
}
}
And your module instantiation can be as simple as:
module "my-test-bucket" {
source = "./terraform-aws-s3"bucket = {
name = "this-is-a-test-bucket"
}
}
That’s all there is to it. I have a demo repo up at mencarellic/terraform-aws-smart-defaults with the S3 example that is a little more fleshed out if you want to take a look.
Thanks for reading!