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:

Terraform is really picky about object contents

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!


References