Please note that this post, first published over a year ago, may now be out of date.
If you are producing Terraform modules for your teams, you want them to be easy and safe to use. Since the Terraform 1.2 release, you can set advanced conditions on resources, data sources and outputs. These conditions let you code-in safety and compliance guarantees where previously Terraform might throw an exception or - worse - wrongly mark the deploy as a success.
In this article I’m going to talk briefly about the features of the Terraform 1.2 release before concentrating on the new conditions system.
Previously in this blog we have talked about the new features of various Terraform releases. It’s exciting to see many of the longstanding niggles finally getting ironed out and becoming strengths instead of a pain in the neck.
Terraform 1.2
We work with many customers to help them write good-practice Terraform and improve their working methods around it. One of the areas of functionality in Terraform’s definition language which has had a huge buff recently (well, since 0.13) is the ability to place validation conditions on variables.
Announced in May 2022, Terraform 1.2 expands on this feature by adding the ability to set conditions on resources, data sources and module outputs. Further, it allows you to choose whether a condition should be evaluated before or after the properties or values are considered.
This is an important step forward with the types of validation that are available to module authors, as variable validation can only react to values that are known during a plan
operation, whereas the new conditions are able to perform validation checks on values that can only be known during the apply
operation.
CI/CD Integration Improvements
Firstly, included in this release are various usability improvements for working with CI/CD pipelines in the cloud, building on support for the cloud
block that was added in Terraform 1.1. The changes aim to allow more cloud-based CI/CD systems to be integrated, e.g. Terraform Enterprise, by providing standard environment variables that can receive the names of workspaces, the URL for a Terraform endpoint, and the organisation name to be used.
By allowing these to be injected from environment variables, the values can be fed into a Terraform run by a CLI in a much cleaner fashion without having to perform file modifications.
This means that you’re more likely to be able to keep the exact same Terraform source code across your entire platform, only having to change the environment variable values depending upon the context.
Terraform Cloud Run Tasks
Secondly, this release supports the Terraform Cloud Run Tasks feature from the CLI, allowing you to see the results of third-party tools that are integrated with a workflow in the output of an apply
run. This enables both engineers and scripts to be able to see and reason about those results and take action where necessary.
Preconditions and Postconditions
Finally, we come to the new conditions system.
This feature is the result of iteration on improving the lot of developers who create and maintain Terraform modules. Validation of input parameters has been possible for a while, and it allows inputs to be given a basic sanity check with custom messages shown in the event of a violation.
This provides an interesting mix of being able to both test assumptions and to make guarantees about things that are generated. With these elements you can form a more solid contract about what a module can and cannot do.
One of the key advantages is that it helps empower teams to make infrastructure changes while feeling that they should be safe to promote through to production (provided that the conditions they have added all make sense).
Problems should get picked up earlier instead of potentially having to wait for huge Terraform apply
operations to timeout before you can see any lurking errors. This could be a massive boon for SaaS platforms that roll out deployments across multiple separate customer estates.
Failing early means a shorter feedback loop which means engineers can focus on any issues more easily.
Another advantage, for instance if you have compliance rules in your organisation, is that it’s possible to write modules that more easily guarantee resources are created which obey those rules. For example, if a module defines an encrypted EC2 instance it could inspect the instance that it creates and fail if this has somehow been subverted to be un-encrypted (whether by accident or artifice). Previously, the details of the created resource could not be reacted to in a meaningful way.
Adding checks into your code means it’s easier to make promises about behaviour. If you’re sharing modules between teams, you get faster feedback when those guarantees might not be met. If you’re doing SemVer, putting the responsibilities in code makes it easy to spot when those guarantees change and you need to bump a version.
Preconditions
Checking your inputs is handy - for example, if you’re taking an IP address as an input, you can reject any invalid syntax instantly. Preconditions let you go beyond syntax checks and actually validate the data.
You can set validation checks on resources, data sources and outputs, so that custom error messages can be shown before the values are used in an apply
operation. Where Terraform can spot a problem at plan
time, you get errors then, too.
Please note that a precondition
is the only condition which is valid in an output
block.
Below shows a precondition
being used in a resource
block, utilised to check whether an ACM certificate is in the correct state to be used. For this example imagine that var.public_certificate_domain
is an input to your module that comes from remote state.
data "aws_acm_certificate" "public" {
domain = var.public_certificate_domain
}
resource "aws_lb_listener" "https" {
load_balancer_arn = aws_lb.public.arn
port = "443"
protocol = "HTTPS"
ssl_policy = "ELBSecurityPolicy-2016-08"
certificate_arn = data.aws_acm_certificate.public.arn
default_action {
type = "forward"
target_group_arn = aws_lb_target_group.website.arn
}
lifecycle {
precondition {
condition = data.aws_acm_certificate.public.status == "ISSUED"
error_message = "The certificate for this Load Balancer Listener must be in state ISSUED."
}
}
}
If the condition fails then the resource will not be created and the following error would be seen in the output:
╷
│ Error: Resource precondition failed
│
│ on modules/lb/main.tf line 18, in resource "aws_lb_listener" "https":
│ 8: condition = data.aws_acm_certificate.public.status == "ISSUED"
│ ├────────────────
│ │ data.aws_acm_certificate.status is "PENDING_VALIDATION"
│
│ The certificate for this Load Balancer Listener must be in state ISSUED.
╵
Postconditions
You can also write checks to be performed on resources after Terraform finds or creates them; this lets you make guarantees about the infrastructure that you use. The following example uses a postcondition
block to determine whether a VPC that has been passed to a module has the enable_dns_support
property set:
# This is an example taken from
# https://learn.hashicorp.com/tutorials/terraform/custom-conditions#add-a-postcondition
data "aws_vpc" "app" {
id = var.aws_vpc_id
lifecycle {
postcondition {
condition = self.enable_dns_support == true
error_message = "The selected VPC must have DNS support enabled."
}
}
}
Note that the self
object used in the condition is unique to the postcondition
block, where it represents the live resource that has been selected. When used in a resource
block it would represent the resource that has been just created.
Outputs
You can give an output
block a precondition and that can provide extra guarantees about what a module produces. For instance - if you know that you are going to be emitting a value which is a crucial dependency somewhere downstream.
In the following example a precondition in the output
block determines whether the VPC Peering Connection that the module creates is in an active state before its ID is emitted.
It’s arguable that the validation should only need to be done in a postcondition on the resource
block, but there’s an angle of defence-in-depth, here - supposing someone removed that postcondition due to an extra parameter being added to the module, or simply in error?
The precondition on the output
block guarantees that the module will produce a VPC Connection that’s active, and that guarantee is in an output
which is a logical place to look for such a contract, just as the variables are a logical place to look for input validation rules.
I mean, if you’re writing a contract you may as well make it easy to find.
resource "aws_vpc_peering_connection" "mothership" {
peer_owner_id = var.peer_owner_id
peer_vpc_id = data.aws_vpc.mothership.id
vpc_id = data.aws_vpc.satellite.id
auto_accept = true
}
output "vpc_peering_id" {
description = "The ID of the Peering connection between the satellite and the mothership VPCs"
value = aws_vpc_peering_connection.mothership.id
lifecycle {
precondition {
condition = aws_vpc_peering_connection.mothership.accept_status == "active"
error_message = "The VPC Peering Connection to the mothership VPC is not active."
}
}
}
Wrapping up
The new Terraform conditions system provides a powerful set of tools to create contracts for infrastructure modules, helping guarantee compliance and guarding against misuse, accidental or otherwise. Teams can put contracts into code as well as a README, and let a computer take care of enforcement.
As part of a wide ecosystem, open source module authors might need to support a wider range of Terraform versions - at least for now. For your internal infrastructure code, consider depending on at least Terraform 1.2 so that authors can use these new features with confidence.
It’s important to reason with these new capabilities carefully, as rushing into their use may slow you down, but for crucial modules which may be called thousands of times across your estates, making guarantees about what they produce can increase your teams’ confidence in what they are building.
Keeping on top of all the latest features can feel like an impossible task. Is practical infrastructure-modernisation an area you are interested in hearing more about? Book a free chat with us to discuss this further.
This blog is written exclusively by The Scale Factory team. We do not accept external contributions.