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
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