Post Terraform v1.0 features I love
Brendan Thompson • 30 June 2022 • 7 min read
There are a few features that have been introduced into Terraform since the release of v1 which I think are extremely exciting. Those features are:
precondition
&postcondition
nullable
optional
Today's post is going to go through these three features and show you how you can use them in your Terraform code!
optional
feature is not currently released, it will come out with v1.3
Pre & Post Conditions#
Pre/Post Conditions are part of a resources (or data sources) lifecycle
block, where you can
control the resources lifecycle conditions more deeply, as such these conditions are evaluated as
early as possible. An output
can also make use of the precondition
.
Why would you use these conditions?
precondition
/postcondition
's are extremely useful as they allow us to check properties or other
resources before and after our resources are created. For instance, we might create a Kubernetes
cluster and want to ensure that there are no public endpoints created, this would be done with a
postcondition
. Another example would be checking that a particular resource has a particular tag
is available in your tags
variable, this would be achieved using the precondition
.
I see these conditions as the most useful inside of a module, especially when the interface is a little more relaxed. This means we can allow our engineers to be creative with their implementations but still enforce guardrails.
Below is an example of the format that is required for these conditions wherever they are used:
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 againstself
, or external resources/variables that returnstrue
orfalse
error_message
— an error message to return to the engineer, this must be in sentence format
We will dive into a simple code example of this now. 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 above example, I have highlighted the two precondition
's we have. The first one is ensuring
that the location
property of our resource group matches var.region
, this is useful in validating
that resources are created in the correct place. The second is validating that local.tags
has a
key named Environment
.
Now let's have a look at 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."
}
}
}
...
Our example above shows a subnet that we are trying to create, once that resource is created we want
to ensure that there are no delegation
s. We are doing that check via a postcondition
. The self
object is a special object that refers to the resource that has been created, similar to an each
within a for_each
loop. Using postcondition
s allows us to validate the state of a created
resource immediately after its creation.
Whilst these examples are somewhat contrived hopefully they help to articulate
why precondition
/postcondition
's are useful.
Nullable Input Variables#
Over the years there have been many times where I have an input variable defined but I don't want to
pass it in every time I call my code, I would normally resolve this by creating a default with "sensible"
values in there. Whilst this might work it is not always what you want, on occasion you might want
to define a variable and only pass it in when it's really required one option is the nullable
property which means our default value can be null
and we can check and deal with that in the code!
To me this is extremely exciting, lets's have a look at 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 above example, we have a tags
input variable, which we might not always want to provide a
real value for. Traditionally we would set the default
to be {}
given that this variable is of
type map(string)
and to be honest even now I would still do this! However, for the sake of this
example, we can set the default to be null
as we have the property nullable
defined as
true
. By doing this we are not forced into passing a value for this input variable into our code
and can validate at the point of use if it is null
or not. In some cases, it is easier to do a
null
check over checking the contents.
One way this might be incredibly useful is if you have a module defined that is consumed by numerous
engineers and you want to add a new feature but ensure backwards compatibility utilising nullable
makes this a breeze!
Optional Input Variable Attributes#
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 opt for complex input variables using object({ ... })
then one thing that
has always been a struggle is the fact that all properties of that object were required, this
usually meant there were a fair few null
/{}
/[]
/""
in my code which, to be honest is pretty
unpleasant. With optional
we have the ability to mark a property on an object as optional, so we
don't have to pass in all the properties if we don't need to! The optional
attribute also allows
us to set a default value for a given property similar to a default value for an entire input
variable.
Let's have a look at 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"]
}
]
}
}
...
As you can see on the highlighted lines we are wrapping the type of our dns_servers
and
flow_timeout_in_minutes
properties in the optional()
attribute, this is letting Terraform know
that this property does not need to be set when we pass in our object. It is important to note
however, if we do not pass that object in that the property will be set to null
if no default value
is set on the optional
attribute so any code that consumes this will need to do a null
check.
By requiring our code to do null
checks it is possible that we might introduce more complexity, as
such I would advise caution and proper thought before using this feature.
Final Thoughts#
So today we have looked at my three favourite new features from v1.0
to v1.3
Terraform, precondition
,
postcondition
, nullable
input variables and optional
. All three of these are absolutely
fantastic features in my opinion and I am extremely excited that we have them!
With precondition
and postcondition
, we can validate/ensure the state of our resources,
data sources, and outputs. Having a nullable
input variable means we can keep our consumption
interface to a bare minimum, as well as an easier way to implement backwards compatibility. The
optional
attribute allows us to negate the requirement to pass in a particular property on an
object, it also allows us to set a default value for a given property!