Skip to content →

Testing strategies in Terraform

Testing is an essential part of any infrastructure-as-code (IaC) project, and Terraform is no exception. In this blog post, we will explore various testing techniques that can be used to ensure the reliability and stability of your Terraform deployments. Starting from the very basic formatting test to spinning up infrastructure with automatic verification tests.

I made a GitHub repo showcasing the techniques here: https://github.com/fredrkl/gh-terraform-flows

Terraform fmt

Not so much testing as it is cleaning up, but terraform format (fmt) is a good tool to start verifying your code is up to your standard. It is part of the Terraform CLI and ensures the code aligns with the canonical format and style guidelines established by HashiCorp.

terraform fmt {file|folder}

One example is changing:

resource "azurerm_resource_group" "example" {
  name = "adding-resource-group"
  location = "eastus"
}

to:

resource "azurerm_resource_group" "example" {
  name     = "adding-resource-group"
  location = "eastus"
}

With the pre-commit project and the pre-commit-terraform plugin, you can make sure all checked-in files are formatted properly.

Terraform validate

The easiest and most often starting point for testing our terraform code is arguably terraform validate. It will go through our terraform files and check for the following:

  • Variable usage
  • Reference consistency
  • Provider schema validation
  • Cycle detection

TFLint

TFLint is a static analysis tool for Terraform code that helps identify potential errors, security vulnerabilities, and best practice violations in your Terraform configurations. TFLint goes beyond just syntax and checks:

  • Hard-coded values in configurations
  • Resource naming inconsistencies
  • Specific cloud provider misconfigurations

TFLint is configured through a separate configuration file.

One example that terraform validate won't catch that TFLint will is missing the required version parameter for a cloud provider and for Terraform.

terraform {
  required_version = ">= 1.6"

  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "3.77.0"
    }
  }
}

Input validation

Input validation in Terraform is an important aspect of ensuring the reliability and correctness of your infrastructure deployments. By validating input parameters, you can catch potential errors and prevent misconfigurations before they cause environmental issues.

Input validation in Terraform can be achieved using input variables and their corresponding validation rules. By defining input variables with specific types and constraints, you can ensure that the values provided by users meet the required criteria. Terraform provides built-in functions like string, number, and bool to validate and convert input values to the desired types. Additionally, you can use conditional expressions and assertions to enforce further validation rules.

Here is one example of location.

variable "location" {
  type        = string
  description = "The location for the resource group."

  validation {
    condition     = contains(["eastus", "northeurope"], var.location)
    error_message = "The location must be between eastus and westus."
  }
}

This ensures that the input value for location is either eastus or northeurope.

Pre and post-conditions

Preconditions and postconditions came in Terraform v1.2.0 and are important concepts in testing and verifying the code. They are both specified on the resource lifecycle.

Preconditions specify conditions that must be true for the resource to be created and can be used to verify the existence of required resources, validate input parameters, or check for specific conditions. With input parameters validation, we know that the parameters are valid according to regex checks, but with preconditions, we can also check non-input parameters to resources or modules. The precondition checks can use Terraform data and other resources as references in the check. Precondition helps other developers to see any invalid changes to the code.

Postconditions are checks done after the resource creation is done and help with verifying the desired outcome.

Here is an example.

resource "azurerm_storage_account" "storage" {
  location                 = "eastus"
  name                     = "examplestorageaccount"
  account_replication_type = "LRS"
  resource_group_name      = azurerm_resource_group.example.name
  account_tier             = "Standard"
  lifecycle {
    precondition {
      condition     = azurerm_resource_group.example.location == "eastus"
      error_message = "The selected resource group must be located in the eastus region."
    }

    postcondition {
      condition     = self.id != ""
      error_message = "The storage account was not created successfully."
    }
  }
}

resource "azurerm_resource_group" "example" {
  name     = "terraform-workflowgroup"
  location = var.location // change this to "northeurope" to see the error
}

Conftest

Not a Terraform feature but a tool to write tests against structured data such as JSON and YAML. Conftest is based on Open Policy Agent, and the tests are written in Rego.

One way of using contest with Terraform is by exporting the plan as JSON and running tests against it. One example of a test is:

package main

deny[msg] {
	# The setup
	resource := input.resource_changes[_].change.after.name

	# The test
	resource == "adding-resource-group"
	msg = "You cannot add a resource group"
}

deny[msg] {
	resource_location := input.planned_values.root_module.resources[_].values.location
	resource_location == "eastus"

	msg = "You cannot create resources in westus"
}

This test simply avoids any new resource group with the name adding-resource-group , or adding any resource to the eastus region.

Checks

Terraform Checks came in version 1.5 and are a bit different from the previous validation checks mentioned in that they run part of the planor applybut does not affect the overall status of the operation. Here is an example from the Terraform documentation(https://developer.hashicorp.com/terraform/language/checks)

check "health_check" {
  data "http" "terraform_io" {
    url = "<https://www.terraform.io>"
  }

  assert {
    condition = data.http.terraform_io.status_code == 200
    error_message = "${data.http.terraform_io.url} returned an unhealthy status code"
  }
}

It simply queries the endpoint's status and fails for any status code other than 200.

This check, in particular, could instead be part of a monitoring tool such as https://learn.microsoft.com/en-us/azure/architecture/patterns/health-endpoint-monitoring. However, other checks that use data sources or part of the infrastructure under Terraform control make more sense to apply checks to, for example:

check "health_check" {
  assert {
    condition     = azurerm_resource_group.example.id != ""
    error_message = "${azurerm_resource_group.example.name} is up and running"
  }
}

The status of each check is recorded in the state file.

Test

Terraform test was introduced in version 1.6 to renew the old experimental testing functionality with the same name. Now, we can run plan or apply our infrastructure and then make assertions against it. Like a unit and integration test in “regular” programming languages.

provider "azurerm" {
  features {}
}

variables {
  name_prefix = "test"
  location    = "eastus"
}

run "valid_resource_group" {

  command = plan

  assert {
    condition     = azurerm_resource_group.example.name == "test-tf-workflowgroup"
    error_message = "Resource group name is not as expected"
  }
}

Naming the file with endings .tftest.hcl instructs Terraform to pick it up when running terraform test.

For each unit test, the terraform test creates the infrastructure and runs the assertions before tearing it down.

Be aware that there are combinations of Checks and Terraform Test that will not work. One example is running the Test with a command plan and then having a Check that has a condition that is only processable after an apply.

Trivy

Now that we know the code is formatted correctly has validations to constraint and avoid misconfigurations, and that our terraform is working as expected with checks and tests, we are moving more into the territory of checking for best practices and security vulnerabilities.

One tool that can help is Trivy from Aqua Security.

This terraform code is perfectly valid according to our previous checks and tests; however, creating a storage account that accepts HTTP traffic instead of HTTPS is a bad idea.

resource "azurerm_storage_account" "invalid" {
  name                     = "invalidstorageaccount"
  account_replication_type = "LRS"
  resource_group_name      = azurerm_resource_group.example.name
  account_tier             = "Standard"
  location                 = azurerm_resource_group.example.location

  enable_https_traffic_only = false
}

Running trivy config scan locally, as part of a .pre-commit hook, or in our CI pipeline picks this up and gives the following feedback:

HIGH: Account does not enforce HTTPS.
════════════════════════════════════════
You can configure your storage account to accept requests from secure connections only by setting the Secure transfer required property for the storage account.

When you require secure transfer, any requests originating from an insecure connection are rejected.

Microsoft recommends that you always require secure transfer for all of your storage accounts.

See <https://avd.aquasec.com/misconfig/avd-azu-0008>
────────────────────────────────────────
 main.tf:44
   via main.tf:37-45 (azurerm_storage_account.invalid)
────────────────────────────────────────
  37   resource "azurerm_storage_account" "invalid" {
  38     name                     = "invalidstorageaccountchange"
  39     account_replication_type = "LRS"
  40     resource_group_name      = azurerm_resource_group.example.name
  41     account_tier             = "Standard"
  42     location                 = azurerm_resource_group.example.location
  43
  44 [   enable_https_traffic_only = false
  45   }
────────────────────────────────────────

The demo repo shows how to transfer any violations to GH Security Code scanning using GH Actions. The example shows up in GH like this:

GitHub Security Scanning issue

Summary

In this post, we discussed several testing techniques and validation methods in Terraform, which are key to ensuring the stability and reliability of your Terraform deployments. We looked at tools such as Terraform fmt, Terraform validate, TFLint, and Conftest for code verification and static analysis. Furthermore, we delved into the importance of input validation, preconditions and postconditions, and Terraform checks. With the new feature Terraform test, we can make assertions against an plan or apply command. Finally, we looked at the Trivy tool for best practices and security vulnerability checks.

These techniques and tools provide a comprehensive testing framework for your Terraform code, enhancing its quality and robustness.

Published in programming

Subscribe
Notify of
guest

0 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
0
Would love your thoughts, please comment.x
()
x