Terraform
Terraform
June 22, 2023

Terraform Check - New Terraform Feature

By
Brendan Thompson
With the release of Terraform v1.5 we get some pretty incredible features added to the language. Today we are going to be talking about one of them, the check block. This allows us to assert things about our provisioned resources. Unlike the precondition and postcondition however a check is non-blocking, this means that it will not prevent your Terraform code from running it will simply warn the engineer that the assertion has failed.

First off lets look at a very basic example.

resource "scratch_string" "this" {
  in = "meow"
}

resource "scratch_string" "that" {
  in = "woof"
}

check "this" {
  data "http" "this" {
    url = "https://kvdb.io/QmQfhGhDgNTVEANY6vXfFk/sounds/cat"
  }

  assert {
    condition     = scratch_string.this.in == data.http.this.response_body
    error_message = "Err: the data source doesn't match the resource."
  }
}

check "that" {
  data "http" "that" {
    url = "https://kvdb.io/QmQfhGhDgNTVEANY6vXfFk/sounds/cat"
  }

  assert {
    condition     = scratch_string.that.in == data.http.that.response_body
    error_message = "Err: the data source doesn't match the resource."
  }
}
Firstly we are creating two resources scratch_string.this and scratch_string.that will run a check against a remote key-value store to ensure that they both match what is in the KV store with check. As part of the check block we are able to define a data block and one or more assert blocks. The assert block takes the same form that we are used to in precondition and postcondition checks with the condition attribute that requires to return a boolean and the error_message attribute that is shown when the check fails.
We can actually make our error_message more useful by using string interprolation, this allows us to bring in information from either the data block or other parts of the code so that our errors are more contextual and meaningful. Below is an example using a fake resource.
assert {
  condition = ...
  error_message = format("Err: User (%s) no it group.", some_resource.user.name)
}

Our result looks like:

data.http.that: Reading...
data.http.this: Reading...
data.http.this: Read complete after 0s [id=https://kvdb.io/QmQfhGhDgNTVEANY6vXfFk/sounds/cat]
data.http.that: Read complete after 0s [id=https://kvdb.io/QmQfhGhDgNTVEANY6vXfFk/sounds/cat]

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
  + create
 <= read (data resources)

Terraform will perform the following actions:

  # data.http.that will be read during apply
  # (config will be reloaded to verify a check block)
 <= data "http" "that" {
      + body             = "meow"
      + id               = "https://kvdb.io/QmQfhGhDgNTVEANY6vXfFk/sounds/cat"
      + response_body    = "meow"
      + response_headers = {
          ...
        }
      + status_code      = 200
      + url              = "https://kvdb.io/QmQfhGhDgNTVEANY6vXfFk/sounds/cat"
    }

  # data.http.this will be read during apply
  # (config will be reloaded to verify a check block)
 <= data "http" "this" {
      + body             = "meow"
      + id               = "https://kvdb.io/QmQfhGhDgNTVEANY6vXfFk/sounds/cat"
      + response_body    = "meow"
      + response_headers = {
          ...
        }
      + status_code      = 200
      + url              = "https://kvdb.io/QmQfhGhDgNTVEANY6vXfFk/sounds/cat"
    }

  # scratch_string.that will be created
  + resource "scratch_string" "that" {
      + id = (known after apply)
      + in = "woof"
    }

  # scratch_string.this will be created
  + resource "scratch_string" "this" {
      + id = (known after apply)
      + in = "meow"
    }

Plan: 2 to add, 0 to change, 0 to destroy.
scratch_string.this: Creating...
scratch_string.that: Creating...
scratch_string.this: Creation complete after 0s [id=7d2182f0-0f0d-11ee-b7ef-aacb117da4fc]
data.http.that: Reading...
data.http.this: Reading...
scratch_string.that: Creation complete after 0s [id=7d21889a-0f0d-11ee-b7ef-aacb117da4fc]
data.http.that: Read complete after 0s [id=https://kvdb.io/QmQfhGhDgNTVEANY6vXfFk/sounds/cat]
data.http.this: Read complete after 0s [id=https://kvdb.io/QmQfhGhDgNTVEANY6vXfFk/sounds/cat]
╷
│ Warning: Check block assertion failed
│ 
│   on main.tf line 35, in check "that":
│   35:     condition     = scratch_string.that.in == data.http.that.response_body
│     ├────────────────
│     │ data.http.that.response_body is "meow"
│     │ scratch_string.that.in is "woof"
│ 
│ Err: the data source doesn't match the resource.
╵

Apply complete! Resources: 2 added, 0 changed, 0 destroyed.
As can be seen the first check for the scratch_string.this resource is all hunky-dory as there are no warnings shown in our apply output, with the second one however we were not so lucky. You can see we have a Warning: Check block assertion failed we expected our in attribute to have the value of meow like the KV store however it actually has the value of woof. This check would also yield the same result on the Plan phase as well.

Limitations

The biggest limitation I have found thus far is that you're limited to only having a single data block in your check, there might be scenarios where it makes sense to have multiple items to check against however this is not currently possible.

Real-world Scenarios

The example I have used is one the provides easy to understand context of how the check block works however it isn't what I would call real-world , below are a few examples of where the check block would be useful.
  • Validating a firewall rule exists for your public IP
  • Ensure there is a route present to allow for routing of your newly provisioned service
  • Validating an API is private
  • User/Identity is in a particular group/role

Closing Out

We have gone through the amazing new feature that is the check block that came out with Terraform v1.5 this opens up a whole new world of possibilities when it comes to testing our infrastructure and ensuring that it exists in the state we expect it to. This means that using technologies such as Terratest for validating our resultant infrastructure (think Unit Tests) becomes less likely to be required. I still think however it is a fantastic option for powerful Integration Testing.

Try out the Check feature using Scalr today!

Note: While this blog references Terraform, everything mentioned in here also applies to OpenTofu. New to OpenTofu? It is a fork of Terraform 1.5.7 as a result of the license change from MPL to BUSL by HashiCorp. OpenTofu is an open-source alternative to Terraform that is governed by the Linux Foundation. All features available in Terraform 1.5.7 or earlier are also available in OpenTofu. Find out the history of OpenTofu here.

Start using the Terraform platform of the future.

A screenshot of the modules page in the Scalr Platform