Smart Deployment with New Terraform Conditions

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.

An orchard row

Liz West

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.

Free Healthcheck

Get an expert review of your AWS platform, focused on your business priorities.

Book Now

Discover how we can help you.


Consulting packages

Advice, engineering, and training, solving common SaaS problems at a fixed price.

Learn more >

Growth solutions

Complete AWS solutions, tailored to the unique needs of your SaaS business.

Learn more >

Support services

An ongoing relationship, providing access to our AWS expertise at any time.

Learn more >