WintelGuy.com

Validating Terraform Configurations: From Formatting to Check Blocks

Contents

Introduction

Infrastructure as Code (IaC) enables teams to manage infrastructure in the same manner as application code, but it also introduces a familiar risk: configuration errors can propagate quickly and at scale. A small typo, an invalid value, or an incorrect assumption about resource behavior can lead to failed deployments, service outages, or costly remediation efforts. For this reason, validating Terraform configurations as early as possible in the workflow is critical.

Terraform provides a rich set of built-in validation mechanisms that help detect errors on different stages of infrastructure provisioning process. These capabilities span multiple layers, from basic syntax and formatting checks, variable constraints, to resource-level assertions and cross-configuration validations. When used effectively, they act as guardrails that improve reliability and confidence in Terraform code.

This article explores Terraform's native functionality for code and resource validation, focusing on:

  • Code validation and formatting
  • Variable validation
  • Resource validation using preconditions and postconditions
  • The check block for broader, configuration-wide assertions

Together, these features enable Terraform configurations to fail fast, display meaningful error messages, and enforce assumptions directly in code. By understanding when and how each type of validation is evaluated, practitioners can design safer modules, catch mistakes earlier, and create infrastructure definitions that are both self-documenting and resilient.

Back to Top

Code Validation and Formatting

Terraform includes built-in commands that help validate configuration files and enforce consistent formatting before any infrastructure changes are planned or applied. Let's review in more details what Terraform's native tooling does, and does not validate.

Code Formatting with terraform fmt

The terraform fmt command automatically formats Terraform configuration files according to HashiCorp's canonical style. It standardizes indentation, spacing, and argument alignment, making configurations easier to read and review.

Key characteristics of terraform fmt include:

  • Automatic rewriting of files to match Terraform style conventions, unless disabled with -write=false
  • Consistent formatting across all configuration files (.tf) and variables files (.tfvars)
  • Support for recursive formatting across all sub-directories with -recursive
  • Viewing the changes with -diff

Because formatting is deterministic, terraform fmt is often used as a baseline quality gate in development workflows and CI pipelines. While it does not validate code syntax, it improves readability and helps identify issues more easily.

Note: Formatting issues rarely break Terraform execution, but inconsistent style can obscure logic and increase the likelihood of human error.

Syntax and Configuration Validation with terraform validate

The terraform validate command performs a static analysis of Terraform configuration files to ensure they are syntactically valid and internally consistent. This includes:

  • Verifying correct block structure and argument types
  • Ensuring required arguments are present
  • Checking references between resources, variables, and outputs
  • Validating expressions and functions

Unlike terraform plan, validation does not contact providers or remote APIs. It only evaluates the configuration itself. As a result, it is fast and safe to run repeatedly, even in environments without credentials or backend access.

Common use cases include:

  • Catching syntax errors early during development
  • Validating module structure before publishing or reuse
  • Running pre-commit or CI checks without provisioning infrastructure

Note: To verify configuration in the context of a particular run (a specific target workspace, input variable values, etc.), use the terraform plan command instead, which includes an implied validation check.

Back to Top

Variable Validation

Variable validation allows Terraform configurations to enforce constraints on input values before those values are used to create or modify infrastructure. By defining explicit rules for acceptable input, variable validation helps catch configuration errors early and makes module interfaces safer and more self-documenting.

Variable validation is implemented directly within variable blocks using a validation sub-block. Each validation rule consists of a condition that must evaluate to true and a corresponding error message that is displayed if the condition fails.

Defining Validation Rules

variable "<LABEL>" { type = <TYPE> # Type constraint default = <DEFAULT_VALUE> # Default value description = "<DESCRIPTION>" # Description sensitive = <true|false> # Whether the values is sensitive nullable = <true|false> # Whether the value can be 'null' ephemeral = <true|false> # Whether the values is ephemeral validation { # Validation rule condition = <EXPRESSION> # Conditional expression error_message = "<ERROR_MESSAGE>" # Error message } }

A variable validation block has two required arguments:

  • condition - a boolean expression that determines whether the value is acceptable
  • error_message - a human-readable message displayed when validation fails

Multiple validation rules (validation {...} blocks) can be defined for a single variable.

Validation conditions can use Terraform expressions, functions, and operators, allowing for simple checks or more complex logic. Common examples include:

  • Ensuring numeric values fall within an expected range
  • Restricting string values to a known set of options
  • Verifying formats using regular expressions
  • Enforcing non-empty values

Example:

# main.tf locals { supported_regions = tolist(["us-east-1", "us-east-2", "us-west-1", "us-west-2"]) } variable "region" { description = "Region to use" default = "us-east-1" type = string validation { condition = contains(local.supported_regions, var.region) error_message = "Provide a region from the list: ${join(",", local.supported_regions)}" } }
$ terraform plan -var "region=us-central-1" Planning failed. Terraform encountered an error while generating this plan. ╷ │ Error: Invalid value for variable │ │ on main.tf line 7: │ 7: variable "region" { │ ├──────────────── │ │ local.supported_regions is list of string with 4 elements │ │ var.region is "us-central-1" │ │ Provide a region from the list: us-east-1,us-east-2,us-west-1,us-west-2 │ │ This was checked by the validation rule at main.tf:12,3-13.

Validation Timing and Behavior

Variable validation is evaluated during the planning phase, after variable values are known but before any resources are created or modified.

When a variable value does not meet its validation criteria, that is, when the condition expression evaluates to false, Terraform aborts the operation with a clear, targeted error message. This ensures that invalid inputs are rejected before they can affect resource configuration.

When variables have default values or are marked as optional, validation logic should account for these cases explicitly.

Limitations of Variable Validation

Despite its flexibility, variable validation has defined limits. It cannot:

  • Inspect runtime resource attributes
  • Validate values returned by providers

For scenarios that depend on resource behavior or cross-resource assumptions, Terraform provides additional mechanisms, such as preconditions, postconditions, and check blocks, which are covered in later sections.

Back to Top

Resource Validation with Preconditions and Postconditions

While variable validation ensures that inputs are well-formed, it cannot validate assumptions about how those values are actually used inside resources. Terraform addresses this gap with preconditions and postconditions, which allow validation rules to be attached directly to resources and evaluated during the planning or apply phases.

Preconditions and postconditions make it possible to express expectations and constants about resource behavior, rather than just input values.

Defining Preconditions and Postconditions

resource "<TYPE>" "<LABEL>" { # ... lifecycle { # ... precondition { # Validation rule condition = <EXPRESSION> # Conditional expression error_message = "<ERROR_MESSAGE>" # Error message } postcondition { # Validation rule condition = <EXPRESSION> # Conditional expression error_message = "<ERROR_MESSAGE>" # Error message } } }

Both precondition and postcondition blocks consist of:

  • A condition expression that must evaluate to true for an operation to proceed.
  • An error_message displayed when the condition fails.

precondition and postcondition blocks can be included in the lifecycle meta-argument of a resource block, data source block, or ephemeral block. postcondition can be also used inside an output block.

Preconditions:

  • Terraform evaluates precondition blocks after generating a plan but before a resource is created or updated. A resource is only created when a related condition is met. If a precondition fails, Terraform aborts the operation.
  • Terraform evaluates precondition blocks after evaluating count and for_each meta-arguments (if present). As a result, Terraform can evaluate the precondition separately for each instance and makes the each.key and count.index objects available in the conditions.

Preconditions are most useful when:

  • A resource or module requires values to meet certain constraints that are context-specific
  • Validation depends on multiple inputs or computed values

Example:

# main.tf variable "environments" { type = list(string) default = ["prod", "dev"] } variable "max_length" { default = 20 } variable "home_dir" { type = string default = "/home/user" } locals { file_names = [for v in var.environments : "file_pre-${v}.cfg"] } resource "local_file" "file_pre" { count = length(var.environments) filename = "${var.home_dir}/${local.file_names[count.index]}" content = "version=1.0" lifecycle { precondition { condition = length(local.file_names[count.index]) <= var.max_length error_message = "File name must be ${var.max_length} characters or less." } } }
$ terraform plan -var 'environments=["production", "dev"]' Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols: + create Terraform planned the following actions, but then encountered a problem: # local_file.file_pre[1] will be created + resource "local_file" "file_pre" { + content = "version=1.0" ... + filename = "/home/user/file_pre-dev.cfg" + id = (known after apply) } Plan: 1 to add, 0 to change, 0 to destroy. ╷ │ Error: Resource precondition failed │ │ on main.tf line 29, in resource "local_file" "file_pre": │ 29: condition = length(local.file_names[count.index]) <= var.max_length │ ├──────────────── │ │ count.index is 0 │ │ local.file_names is tuple with 2 elements │ │ var.max_length is 20 │ │ File name must be 20 characters or less. ╵
$ terraform console -var 'environments=["production", "dev"]' > local.file_names[0] "file_pre-production.cfg" > length(local.file_names[0]) 23 >

Preconditions are evaluated before any changes are made, they act as guardrails and help prevent invalid infrastructure configuration.

Although preconditions and variable validation can sometimes appear to overlap, they serve different purposes:

  • Variable validation enforces correctness at the input boundary
  • Preconditions enforce correctness at the resource boundary

Postconditions:

  • Terraform evaluates postcondition block after a resource has been created or updated. If a postcondition fails, Terraform reports an error even though the resource change may have already occurred. Postcondition failures prevent changes to other resources that depend on the failing resource.
  • Terraform provides a special self object that can be used in the conditional expression to refer the resource under evaluation.

Postconditions are particularly useful when:

  • A resource's final state depends on provider behavior
  • Certain outcomes cannot be validated in advance
  • You want explicit confirmation that an operation succeeded as expected

Example:

# main.tf variable "home_dir" { type = string default = "/home/user" } variable "content" { type = string default = "version=1.0" } locals { expected_hash = sha256("version=1.0") } resource "local_file" "file_post" { filename = "${var.home_dir}/file_post.cfg" content = var.content lifecycle { postcondition { condition = self.content_sha256 == local.expected_hash error_message = "File content validation failed." } } }
$ terraform apply -auto-approve ... $ terraform apply -auto-approve -var "content=version=1.1" local_file.file_post: Refreshing state... [id=f1af4003baac597259e2d2b420470de7ce76b9e6] Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols: -/+ destroy and then create replacement Terraform will perform the following actions: # local_file.file_post must be replaced -/+ resource "local_file" "file_post" { ~ content = "version=1.0" -> "version=1.1" # forces replacement ... ~ content_sha256 = "56c551fa41901e6377865d2f87c6d514cafdcdfa020867d0be9f320e11bc3731" -> (known after apply) ~ content_sha512 = "ed2bdcac4a8d22372ad4ae26ce1e458c4bb02f9f619b4da8e33bf04ef54bb50b83379e954968ca1f14cd199ce4f2a52ce63027af16715a41d38cbe299e42396e" -> (known after apply) ~ id = "f1af4003baac597259e2d2b420470de7ce76b9e6" -> (known after apply) # (3 unchanged attributes hidden) } Plan: 1 to add, 0 to change, 1 to destroy. local_file.file_post: Destroying... [id=f1af4003baac597259e2d2b420470de7ce76b9e6] local_file.file_post: Destruction complete after 0s local_file.file_post: Creating... local_file.file_post: Creation complete after 0s [id=4589ed205643339cad0a3a7c7602aab647586056] ╷ │ Error: Resource postcondition failed │ │ on main.tf line 23, in resource "local_file" "file_post": │ 23: condition = self.content_sha256 == local.expected_hash │ ├──────────────── │ │ local.expected_hash is "56c551fa41901e6377865d2f87c6d514cafdcdfa020867d0be9f320e11bc3731" │ │ self.content_sha256 is "55ecea351fdd52ddf8c58735300ecc262f859b75f5eb777f063cf7b80330ae55" │ │ File content validation failed.
$ cat ~/file_post.cfg version=1.1

Unlike preconditions, postconditions may fail after changes have already been applied, which makes them better suited for verification rather than prevention.

When Resource Validation Is Not Enough

Preconditions and postconditions apply to individual resources or data sources. They are not designed to:

  • Verify relationships across multiple resources
  • Validate configuration-wide rules
  • Perform global consistency checks

For those use cases, Terraform provides the check block, which enables broader validation across an entire configuration. This is covered in the next section.

Back to Top

The check Block

The check block provides a way to define standalone, configuration-wide validations that are not tied to any single resource or variable. It is designed for expressing assumptions and invariants that span multiple values, resources, or computed results within a Terraform configuration.

Unlike variable validation or resource-level preconditions, check blocks are defined at the top level of a module and act as explicit assertions about the overall configuration state.

Purpose and Characteristics

check "<LABEL>" { data "<TYPE>" "<LABEL>" { # Data source (Optional) <DATA_BLOCK_CONFIGURATION> depends_on = [<RESOURCE.ADDRESS>] # Upstream resource(s) } assert { # Assertion block (Required) condition = <EXPRESSION> # Conditional expression error_message = "<ERROR_MESSAGE>" # Error message } }

Each check block contains one or more assert blocks, and each assertion defines:

  • A condition that must evaluate to true
  • An error_message that explains the failure

A check block may also include an optional data block specifying a data source to use for validation. This data source can only be referenced within its parent check block.

Terraform executes the check block as the last step of plan or apply operation, after Terraform has planned or provisioned the infrastructure. When a check block's assertion fails, Terraform reports a warning and continues executing the current operation. The check block is the only validation that does not block operations.

If a check block assertion depends on a data block with an explicitly declared depends_on relationship, and the upstream resource has planned changes, Terraform does not evaluate the assertion during the plan phase. In this case, Terraform reports Check block assertion known after apply in the plan output and defers evaluation of the check until after the apply phase, once the upstream resource has been created or updated.

When to Use a check Block

The check block is ideal when:

  • Validation logic does not belong to a single resource
  • Multiple resources must satisfy a shared constraint
  • Enforcing configuration-wide rules
  • Validating infrastructure functionality vs its mere existence

check Block Example

The following Terraform code includes a check block intentionally configured with two assertions containing complementary conditional expressions. Using this example we can demonstrate check block behavior during various Terraform deployment phases.

# main.tf variable "home_dir" { type = string default = "/home/user" } variable "target_ver" { type = string default = "version=1.0" } locals { filename = "${var.home_dir}/file_check.cfg" } resource "local_file" "file_check" { filename = local.filename content = var.target_ver } check "file_check" { data "local_file" "file_check" { filename = local.filename # depends_on = [local_file.file_check] } assert { condition = data.local_file.file_check.content == var.target_ver error_message = join("", [ "Current: \"${data.local_file.file_check.content}\", ", "Target: \"${var.target_ver}\"" ]) } assert { condition = data.local_file.file_check.content != var.target_ver error_message = join("", [ "Current: \"${data.local_file.file_check.content}\"; ", "No changes" ]) } }

First, initialize and apply the configuration to create file_check.cfg.

Explore check block behavior by running plan and apply with different target_ver values. For example:

$ terraform apply -var target_ver="version=1.1" local_file.file_check: Refreshing state... [id=f1af4003baac597259e2d2b420470de7ce76b9e6] data.local_file.file_check: Reading... data.local_file.file_check: Read complete after 0s [id=f1af4003baac597259e2d2b420470de7ce76b9e6] Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols: -/+ destroy and then create replacement <= read (data resources) Terraform will perform the following actions: # data.local_file.file_check will be read during apply # (config will be reloaded to verify a check block) <= data "local_file" "file_check" { + content = "version=1.0" ... + filename = "/home/user/file_check.cfg" + id = "f1af4003baac597259e2d2b420470de7ce76b9e6" } # local_file.file_check must be replaced -/+ resource "local_file" "file_check" { ~ content = "version=1.0" -> "version=1.1" # forces replacement ... ~ id = "f1af4003baac597259e2d2b420470de7ce76b9e6" -> (known after apply) # (3 unchanged attributes hidden) } Plan: 1 to add, 0 to change, 1 to destroy. ╷ │ Warning: Check block assertion failed │ │ on main.tf line 29, in check "file_check": │ 29: condition = data.local_file.file_check.content == var.target_ver │ ├──────────────── │ │ data.local_file.file_check.content is "version=1.0" │ │ var.target_ver is "version=1.1" │ │ Current: "version=1.0", Target: "version=1.1" ╵ Do you want to perform these actions? Terraform will perform the actions described above. Only 'yes' will be accepted to approve. Enter a value: yes local_file.file_check: Destroying... [id=f1af4003baac597259e2d2b420470de7ce76b9e6] local_file.file_check: Destruction complete after 0s local_file.file_check: Creating... local_file.file_check: Creation complete after 0s [id=4589ed205643339cad0a3a7c7602aab647586056] data.local_file.file_check: Reading... data.local_file.file_check: Read complete after 0s [id=4589ed205643339cad0a3a7c7602aab647586056] ╷ │ Warning: Check block assertion failed │ │ on main.tf line 37, in check "file_check": │ 37: condition = data.local_file.file_check.content != var.target_ver │ ├──────────────── │ │ data.local_file.file_check.content is "version=1.1" │ │ var.target_ver is "version=1.1" │ │ Current: "version=1.1"; No changes ╵ Apply complete! Resources: 1 added, 0 changed, 1 destroyed.

To explore impact of explicitly configured resource dependency on check block behavior, uncomment the depends_on = [local_file.file_check] line and re-run various plan and apply commands. For example:

$ terraform apply 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.local_file.file_check will be read during apply # (depends on a resource or a module with changes pending) <= data "local_file" "file_check" { + content = (known after apply) ... + filename = "/home/user/file_check.cfg" + id = (known after apply) } # local_file.file_check will be created + resource "local_file" "file_check" { + content = "version=1.0" ... + filename = "/home/user/file_check.cfg" + id = (known after apply) } Plan: 1 to add, 0 to change, 0 to destroy. ╷ │ Warning: Check block assertion known after apply │ │ on main.tf line 29, in check "file_check": │ 29: condition = data.local_file.file_check.content == var.target_ver │ ├──────────────── │ │ data.local_file.file_check.content is a string │ │ var.target_ver is "version=1.0" │ │ The condition could not be evaluated at this time, a result will be known when this plan is applied. ╵ ╷ │ Warning: Check block assertion known after apply │ │ on main.tf line 37, in check "file_check": │ 37: condition = data.local_file.file_check.content != var.target_ver │ ├──────────────── │ │ data.local_file.file_check.content is a string │ │ var.target_ver is "version=1.0" │ │ The condition could not be evaluated at this time, a result will be known when this plan is applied. ╵ Do you want to perform these actions? Terraform will perform the actions described above. Only 'yes' will be accepted to approve. Enter a value: yes local_file.file_check: Creating... local_file.file_check: Creation complete after 0s [id=f1af4003baac597259e2d2b420470de7ce76b9e6] data.local_file.file_check: Reading... data.local_file.file_check: Read complete after 0s [id=f1af4003baac597259e2d2b420470de7ce76b9e6] ╷ │ Warning: Check block assertion failed │ │ on main.tf line 37, in check "file_check": │ 37: condition = data.local_file.file_check.content != var.target_ver │ ├──────────────── │ │ data.local_file.file_check.content is "version=1.0" │ │ var.target_ver is "version=1.0" │ │ Current: "version=1.0"; No changes ╵ Apply complete! Resources: 1 added, 0 changed, 0 destroyed.

As illustrated by the example, a check block is a powerful mechanism for validating complex conditions throughout the Terraform workflow. By leveraging provider-specific and generic data sources (local_file, local_command, http, etc.) a check block can enforce configuration-wide assumptions and verify expected behavior beyond the scope of individual resources.

check Blocks vs Other Validation Mechanisms

Terraform validation features each serve a distinct role:

  • Variable validation - Ensures input values are acceptable.
  • Preconditions and postconditions - Enforce expectations tied to individual resources.
  • check blocks - Validates global assumptions and cross-resource rules.

check blocks allow to expand validation logic to the configuration-wide level.

Back to Top

Terraform Validation Mechanisms at a Glance

In this article, we reviewed several native Terraform mechanisms, each designed to perform validation at a specific boundary and stage of the workflow. The following summary provides a condensed overview of these techniques, highlighting their scope, evaluation phase, and primary purpose. This overview is intended to help you quickly understand the differences between validation mechanisms and choose the appropriate ones for your Terraform configurations.

Code Validation and Formatting

  • Mechanism: terraform fmt, terraform validate
  • Boundary: Entire configuration (syntax and structure)
  • Workflow Phase: Before planning
  • Stops Operation: Yes (on validation failure)
  • Failure Behavior: Execution halts on formatting or validation errors
  • Primary Purpose: Ensure syntactic correctness and consistent code style

Variable Validation

  • Mechanism: validation blocks in variable definitions
  • Boundary: Module input interface
  • Workflow Phase: Plan phase (after variable values are known)
  • Stops Operation: Yes
  • Failure Behavior: Plan fails immediately
  • Primary Purpose: Enforce acceptable input values and protect module boundaries

Resource Preconditions

  • Mechanism: precondition blocks
  • Boundary: Individual resource or data source
  • Workflow Phase: Plan phase (before resource changes)
  • Stops Operation: Yes
  • Failure Behavior: Plan fails; no changes are applied
  • Primary Purpose: Prevent invalid or unsafe resource configuration

Resource Postconditions

  • Mechanism: postcondition blocks
  • Boundary: Individual resource or data source
  • Workflow Phase: Apply phase (after resource changes)
  • Stops Operation: Yes (after changes complete)
  • Failure Behavior: Terraform reports an error after apply and exits with failure status
  • Primary Purpose: Verify that resource outcomes meet expectations

Check Blocks

  • Mechanism: check blocks with assert statements
  • Boundary: Entire module or configuration
  • Workflow Phase:
    • Plan phase (when assertions can be evaluated)
    • Apply phase (when assertions are known only after apply)
  • Stops Operation: No
  • Failure Behavior: Terraform reports warnings; plan and apply operations continue and complete
  • Primary Purpose:
    • Validate configuration-wide assumptions
    • Provide explicit verification of expected behavior
    • Document design intent without enforcing hard stops

Choosing the Right Validation Mechanism

  • Use code validation to catch structural errors early
  • Use variable validation to protect module boundaries
  • Use preconditions to guard resource configuration
  • Use postconditions to verify resource outcomes
  • Use check blocks to evaluate global assumptions and design intent

No single mechanism replaces the others. Instead, effective Terraform configurations layer these features to fail fast where possible and verify correctness where necessary.

Back to Top

Conclusion

Terraform's native validation features form a layered model that enables configurations to fail fast, communicate intent clearly, and ensure validity at multiple levels. Rather than relying on a single validation step, Terraform encourages validation at the appropriate boundary, starting with code structure, continuing through input validation, and extending to resource behavior and configuration-wide assumptions.

Each validation mechanism serves a distinct role. Code validation establishes a reliable foundation, variable validation protects module interfaces, preconditions and postconditions enforce resource-level expectations, and check blocks capture global invariants that transcend individual resources. Used together, these techniques create a defense-in-depth strategy that improves safety and maintainability without introducing external dependencies.

By thoughtfully applying the right validation at the right stage of the workflow, Terraform practitioners can reduce risk, improve clarity, and make infrastructure changes more predictable over time.

Back to Top