Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse varius enim in eros elementum tristique. Duis cursus, mi quis viverra ornare, eros dolor interdum nulla, ut commodo diam libero vitae erat. Aenean faucibus nibh et justo cursus id rutrum lorem imperdiet. Nunc ut sem vitae risus tristique posuere.
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!
It is important to note however, that the optional
feature is not currently released, it will come out with v1.3
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 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 formatWe 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, the first precondition
we have 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.
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!
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 example above, 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.
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!
This article was originally published on Brendan Thompson's blog. You can follow Brendan @BrendanLiamT on Twitter.
No credit card required.