TrademarkTrademark
Features
Documentation

Terraform v1.0 Features I Love

The new features Terraform released since the release of v1 which are extremely exciting.
Brendan ThompsonJuly 8, 2022
Terraform v1.0 Features I Love
Key takeaways
  • The post highlights three Terraform features introduced since v1.0: precondition and postcondition blocks, nullable input variables, and the optional object attribute.
  • precondition and postcondition blocks live in a resource's lifecycle block and let you check properties before and after a resource is created to enforce guardrails.
  • The nullable property lets an input variable default to null, so callers are not forced to pass a value and the code can handle the null case.
  • The optional attribute, arriving in v1.3, lets you mark individual object properties as optional and set per-property default values.

A handful of features have landed in Terraform since the release of v1, and a few of them have changed how I write my code. The ones I want to cover here are:

  • precondition & postcondition
  • nullable
  • optional

This post walks through each one with examples you can drop into your own Terraform.

Note that the optional feature is not currently released, it will come out with v1.3

What Do Precondition and Postcondition Blocks Do in Terraform?

Pre/Post Conditions live in a resource's (or data source's) lifecycle block. They give you more control over the resource's lifecycle conditions, and they're evaluated as early as possible. An output can use the precondition too.

Why would you use these conditions?

precondition / postcondition's are handy because they let you check properties or other resources before and after your resources are created. Say you create a Kubernetes cluster and want to make sure no public endpoints get created. That's a postcondition. Or you want to check that a particular tag is in your tags variable. That's a precondition.

These conditions are most useful inside a module, especially when the interface is a little more relaxed. You can let your engineers get creative with their implementations and still enforce guardrails.

Here's the format these conditions need, wherever you use them:

postcondition {
  condition     = self.encrypted
  error_message = "Err: the pre/post condition failed."
}

Similar to Input Variable Validation the precondition / postcondition have two fields:

  • condition - some sort of check against self, or external resources/variables that returns true or false
  • error_message - an error message to return to the engineer, this must be in sentence format

Here's a simple code example of this. Please note that there will be references to resources and variables that I won't show in the example but I will put the full script at the end of this post.

...
 
resource "azurerm_virtual_network" "this" {
  name                = format("vn-%s", local.suffix)
  location            = var.region
  resource_group_name = azurerm_resource_group.this.name
 
  address_space = var.network.address_space
  dns_servers   = var.network.dns_servers != null ? var.network.dns_servers : []
 
  tags = local.tags
 
  lifecycle {
    precondition {
      condition     = azurerm_resource_group.this.location == var.region
      error_message = "Err: resource group in incorrect region."
    }
 
    precondition {
      condition     = contains(keys(local.tags), "Environment")
      error_message = "Err: no environment tag present."
    }
  }
}
 
...

In the example above, the first precondition checks that the location property of our resource group matches var.region, which is handy for confirming resources land in the right place. The second checks that local.tags has a key named Environment.

Now here's a postcondition:

...
 
resource "azurerm_subnet" "this" {
  for_each = {
    for v in var.network.subnets :
    v.name => v
  }
 
  name                 = format("sn-%s-%s", local.suffix, each.value.name)
  resource_group_name  = azurerm_resource_group.this.name
  virtual_network_name = azurerm_virtual_network.this.name
 
  address_prefixes = each.value.address_space
 
  lifecycle {
    postcondition {
      condition     = length(self.delegation) == 0
      error_message = "Err: subnet delegation in on the subnet."
    }
  }
}
 
...

The example above shows a subnet we're creating, and once it exists we want to make sure there are no delegations. We check that with a postcondition. The self object is a special object that refers to the resource that's been created, like an each inside a for_each loop. A postcondition lets you check the state of a resource right after it's created.

These examples are a little contrived, but hopefully they show why precondition / postcondition's are worth reaching for.

How Does the Nullable Property Work for Input Variables?

Plenty of times I've defined an input variable that I don't want to pass in on every call. My usual fix was to give it a default with some "sensible" value. That works, but it isn't always what you want. Sometimes you want a variable that you only pass in when it's really needed. The nullable property covers that case: the default can be null, and you check for it and handle it in the code.

Here's an example.

...
 
variable "tags" {
  type = map(string)
  default = null
 
  nullable = true
}
 
locals {
  tags = merge(var.tags, { Environment = var.environment })
}
 
resource "azurerm_resource_group" "this" {
  name     = format("rg-%s", local.suffix)
  location = var.region
 
  tags = local.tags
}
 
...

In the example above, we have a tags input variable that we might not always want to give a real value. Normally we'd set the default to {} since this variable is of type map(string), and I'd still do that! But for this example, we set the default to null because we've got nullable set to true. Now we're not forced to pass a value for this input variable, and we can check at the point of use whether it's null. Sometimes a null check is easier than checking the contents.

This is handy when you have a module that lots of engineers consume and you want to add a new feature without breaking the existing callers. nullable makes that much easier.

What Does the Optional Attribute Add to Object Variables?

The final, and in my opinion the most exciting feature is optional!! This is coming out shortly with the v1.3 release of Terraform.

If, like me, you tend to reach for complex input variables built with object({ ... }), you've hit the same annoyance I have: all properties of that object were required. That usually left a fair few null / {} / [] / "" scattered through my code, which is pretty unpleasant to read. With optional you can mark a property on an object as optional, so you don't have to pass in every property when you don't need to. The optional attribute also lets you set a default value for a given property, much like the default you'd set on an entire input variable.

Here's an example.

...
 
variable "network" {
  type = object({
    address_space           = list(string)
    dns_servers             = optional(list(string))
    flow_timeout_in_minutes = optional(number, 15)
    subnets = optional(list(
      object({
        name          = string
        address_space = list(string)
      })
    ))
  })
 
  default = {
    address_space = ["10.0.0.0/23"]
    subnets = [
      {
        name          = "0"
        address_space = ["10.0.0.0/24"]
      }
    ]
  }
}
 
...

In the example above, we wrap the type of our dns_servers and flow_timeout_in_minutes properties in the optional() attribute, which tells Terraform these properties don't need to be set when we pass in our object. Keep in mind that if you don't pass them, the property is set to null when there's no default on the optional attribute, so any code that consumes it needs a null check.

Those null checks can add complexity, so think it through and use this feature with some caution.

Which Terraform v1.0-v1.3 Features Made This List?

That's a look at my favourite new features from v1.0 to v1.3 Terraform: precondition, postcondition, nullable input variables, and optional. I've been reaching for all of them in real code, and they've made my modules cleaner and easier to live with.

This article was originally published on Brendan Thompson's blog. 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.