Terraform Validation, Preconditions, Postconditions and Checks

Tested on Ubuntu 26.04 LTS (Resolute Raccoon)
Package terraform 1.15.8-1
Applies to Any host with Terraform installed
Lab environment Single Ubuntu VM with Terraform — Terraform lab environment on Ubuntu
Privilege Normal user
Scope Language-level custom conditions — variable validation blocks, lifecycle precondition and postcondition on resources and data sources, output preconditions, check blocks with assert, evaluation timing, failure behavior, cross-variable references, error_message guidance, and distinction from terraform validate. Does not cover the terraform validate CLI tutorial, TFLint, Checkov, Sentinel, OPA, or policy-as-code frameworks.
Related guides Terraform variables
Terraform lifecycle meta-arguments
terraform validate command
Terraform functions
Terraform Associate certification course

Your pipeline accepts an environment value from a .tfvars file or a CI variable. You want Terraform to reject anything outside your allow-list before it creates infrastructure:

text
"environment" must be dev, test, or prod

A validation block on the variable is the first tool most teams reach for. Terraform also gives you preconditions, postconditions, and check blocks — each runs at a different point in the workflow and each fails differently.

This lesson walks through all four mechanisms on the built-in terraform_data resource so you do not need cloud credentials. Each scenario lives under ~/terraform-labs/terraform-validation-checks/ in its own subdirectory.

IMPORTANT
This article covers custom condition blocks in HCL (validation, precondition, postcondition, check). The terraform validate command checks configuration syntax and provider schema only — it does not run your business rules.
NOTE
Run terraform init once per lab subdirectory before plan or apply. Examples use terraform_data from the built-in terraform provider.

Terraform custom condition types

Terraform gives you four language-level ways to express condition and error_message rules. Pick the block that matches what you validate and when Terraform can see the values.

Mechanism Where it lives Typical purpose Fails at (1.15.8) Blocks plan? Blocks apply? Severity
validation variable block Reject bad input early plan (and apply) yes yes Error
precondition lifecycle on resource, data, or ephemeral resource; directly in output Assert assumption before the instance or output is finalized plan, or apply if required values were not known earlier yes yes Error
postcondition lifecycle on resource, data, or ephemeral resource Assert guarantee after attributes are known plan or apply, depending on when attributes become known yes yes Error
check + assert Root module check block Report drift or policy gaps without stopping the run plan and apply no no Warning

Every custom condition uses the same two arguments inside its nested block:

  • condition — must evaluate to true or Terraform treats the rule as failed
  • error_message — plain text shown when condition is false

Variable validation

Put a validation block inside a Terraform variable when the rule applies to the variable value itself — allowed environment names, port ranges, or string length.

Create the success lab directory and write the variable block:

bash
mkdir -p ~/terraform-labs/terraform-validation-checks/success
hcl
variable "environment" {
  type        = string
  description = "Deployment environment"
  default     = "dev"

  validation {
    condition     = contains(["dev", "prod"], var.environment)
    error_message = "environment must be dev or prod, not ${var.environment}."
  }
}

Save that as ~/terraform-labs/terraform-validation-checks/success/variables.tf. Terraform evaluates every validation block after type checking and before it builds the full dependency graph.

Initialize the success directory:

bash
cd ~/terraform-labs/terraform-validation-checks/success && terraform init

init downloads nothing extra here because terraform_data uses the built-in provider. A clean init ends with Terraform has been successfully initialized!

With default = "dev", plan should succeed. Add a minimal resource and run plan:

hcl
resource "terraform_data" "example" {
  input = var.environment
}

Save that as main.tf, then plan from the success directory:

bash
terraform plan
output
Terraform used the selected providers to generate the following execution
plan. Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  # terraform_data.example will be created
  + resource "terraform_data" "example" {
      + id     = (known after apply)
      + input  = "dev"
      + output = (known after apply)
    }

Plan: 1 to add, 0 to change, 0 to destroy.

The plan shows input = "dev", so the validation rule accepted the default.

Trigger a variable validation failure

Copy the success directory into a failure scenario with a bad default:

bash
cp -r ~/terraform-labs/terraform-validation-checks/success ~/terraform-labs/terraform-validation-checks/variable-validation-fail

Edit variable-validation-fail/variables.tf and set default = "staging". Plan from that directory:

bash
cd ~/terraform-labs/terraform-validation-checks/variable-validation-fail && terraform plan
output
Planning failed. Terraform encountered an error while generating this plan.


Error: Invalid value for variable

  on variables.tf line 1:
   1: variable "environment" {
    ├────────────────
    │ var.environment is "staging"

environment must be dev or prod, not staging.

This was checked by the validation rule at variables.tf:6,3-13.

Variable validation is an error — plan exits non-zero and apply never runs. The diagnostic names the variable, shows the actual value, and points at the failing validation block.


Multiple validation rules

One variable may contain multiple validation blocks. Give each rule its own focused error_message so the diagnostic clearly describes whichever rule fails.

Create ~/terraform-labs/terraform-validation-checks/variable-validation-multi/variables.tf:

hcl
variable "port" {
  type        = number
  description = "Service port with two validation rules"
  default     = 8080

  validation {
    condition     = var.port >= 1024
    error_message = "port must be at least 1024 (privileged ports not allowed)."
  }

  validation {
    condition     = var.port <= 65535
    error_message = "port must be at most 65535."
  }
}

Initialize and confirm the default port passes both rules:

bash
cd ~/terraform-labs/terraform-validation-checks/variable-validation-multi && terraform init

With init done, plan using the default port 8080:

bash
terraform plan
output
Plan: 1 to add, 0 to change, 0 to destroy.

Port 8080 clears both bounds. Override with a privileged port to exercise the lower-bound rule:

bash
terraform plan -var port=80
output
Error: Invalid value for variable

  on variables.tf line 1:
   1: variable "port" {
    ├────────────────
    │ var.port is 80

port must be at least 1024 (privileged ports not allowed).

This was checked by the validation rule at variables.tf:6,3-13.

Try a value above the upper bound to exercise the second rule:

bash
terraform plan -var port=70000
output
Error: Invalid value for variable

  on variables.tf line 1:
   1: variable "port" {
    ├────────────────
    │ var.port is 70000

port must be at most 65535.

This was checked by the validation rule at variables.tf:11,3-13.

Each validation block carries its own error_message, so splitting rules keeps diagnostics specific instead of one long compound sentence.


Preconditions

A precondition lives inside lifecycle on managed resources, data sources, and ephemeral resources. Output blocks also support precondition, but the block sits directly inside the output block — not inside lifecycle. Outputs do not support postcondition.

Use a precondition when a rule must hold before Terraform changes that instance or exposes that output — for example a feature flag must be true, or a data source must already exist.

Add a resource precondition to the success configuration in success/main.tf:

hcl
variable "enable_feature" {
  type    = bool
  default = true
}

resource "terraform_data" "example" {
  input = {
    environment = var.environment
    token       = "expected-token"
  }

  lifecycle {
    precondition {
      condition     = var.enable_feature
      error_message = "enable_feature must be true."
    }
  }
}

With enable_feature = true, plan from success/ still succeeds. Copy to a failure directory and set enable_feature default to false in variables.tf:

bash
cp -r ~/terraform-labs/terraform-validation-checks/success ~/terraform-labs/terraform-validation-checks/precondition-fail

Edit precondition-fail/variables.tf so enable_feature defaults to false, then plan:

bash
cd ~/terraform-labs/terraform-validation-checks/precondition-fail && terraform plan
output
Planning failed. Terraform encountered an error while generating this plan.


Error: Resource precondition failed

  on main.tf line 9, in resource "terraform_data" "example":
   9:       condition     = var.enable_feature
    ├────────────────
    │ var.enable_feature is false

enable_feature must be true.

An output uses the same condition and error_message arguments, nested directly in the block:

hcl
output "message" {
  value = terraform_data.example.output

  precondition {
    condition     = terraform_data.example.output != ""
    error_message = "message output must not be empty."
  }
}

Terraform evaluates preconditions as early as possible — generally during plan. If a value is (known after apply) at plan time, the same precondition may be re-checked during apply and prevent the associated operation from proceeding. A failed precondition is an error. If Terraform can evaluate it during plan, planning fails; if the condition only becomes known during apply, Terraform blocks the associated action rather than continuing with a violated assumption.


Postconditions

A postcondition sits in lifecycle on managed resources, data sources, and ephemeral resources. Terraform checks it after the instance attributes behind self are available. Output blocks do not support postcondition — use a resource or data source postcondition, or gate the output with an output precondition instead.

Use postconditions to assert guarantees about what the provider returned — name format, tag presence, or a computed field in range.

Add a postcondition to success/main.tf:

hcl
lifecycle {
  precondition {
    condition     = var.enable_feature
    error_message = "enable_feature must be true."
  }

  postcondition {
    condition     = self.input.token == "expected-token"
    error_message = "token in input must remain expected-token."
  }
}

When token stays "expected-token", plan and apply succeed.

To see a failure, create postcondition-fail/main.tf with a static input Terraform can evaluate at plan time:

hcl
resource "terraform_data" "example" {
  input = "wrong-value"

  lifecycle {
    postcondition {
      condition     = self.input == "correct-value"
      error_message = "input must be correct-value after apply."
    }
  }
}

Plan from that directory:

bash
cd ~/terraform-labs/terraform-validation-checks/postcondition-fail && terraform plan
output
Terraform planned the following actions, but then encountered a problem:

  # terraform_data.example will be created
  + resource "terraform_data" "example" {
      + id     = (known after apply)
      + input  = "wrong-value"
      + output = (known after apply)
    }

Plan: 1 to add, 0 to change, 0 to destroy.

Error: Resource postcondition failed

  on main.tf line 6, in resource "terraform_data" "example":
   6:       condition     = self.input == "correct-value"
    ├────────────────
    │ self.input is "wrong-value"

input must be correct-value after apply.

Because input is a literal string, Terraform knows self.input at plan time. The postcondition fails during plan before any create runs. When a postcondition depends on values only known after the provider API call, the failure may surface during apply instead.

Downstream resources that depend on a failed instance do not proceed — Terraform stops the graph walk when a precondition or postcondition errors.


Check blocks

check blocks (Terraform 1.5+) hold one or more assert nested blocks. They are suited to ongoing assertions — environment still in an allow-list, naming convention still met — that you want reported without blocking the run.

Add success/checks.tf:

hcl
check "environment_allowed" {
  assert {
    condition     = contains(["dev", "prod"], var.environment)
    error_message = "Check failed: environment must be dev or prod."
  }
}

With a valid environment, plan shows no check warning:

bash
cd ~/terraform-labs/terraform-validation-checks/success && terraform plan
output
Plan: 1 to add, 0 to change, 0 to destroy.

Trigger a check failure

Create check-fail/ with default = "staging" on the environment variable and the same check block. Plan still completes:

bash
cd ~/terraform-labs/terraform-validation-checks/check-fail && terraform plan
output
Plan: 1 to add, 0 to change, 0 to destroy.

Warning: Check block assertion failed

  on checks.tf line 3, in check "environment_in_list":
   3:     condition     = contains(["dev", "prod"], var.environment)
    ├────────────────
    │ var.environment is "staging"

Check failed: environment must be dev or prod.

Plan exits 0 despite the warning. Apply behaves the same way — the resource is still created:

bash
terraform apply -auto-approve
output
terraform_data.example: Creation complete after 0s [id=...]

Warning: Check block assertion failed
...
Apply complete! Resources: 1 added, 0 changed, 0 destroyed.

On Terraform 1.15.8, failed check asserts are warnings, not errors. Use check when you want visibility in plan output without stopping automation. Use validation or precondition when a bad value must halt the run.


Choose the right Terraform condition

Use this table when you are deciding which block to add:

Need Use
Reject a bad variable value at input time validation on the variable
Gate a specific resource, data source, or ephemeral resource before it changes lifecycle precondition
Gate an output value before Terraform exposes it precondition directly in the output block
Verify provider result on a resource or data source lifecycle postcondition
Report a problem without stopping plan or apply check + assert

validation vs precondition

Prefer validation on the variable Prefer precondition on the resource or output
Rule involves only that variable's value Rule needs self, another resource, a data source, or an output value expression
Fail as early as possible on bad .tfvars Fail only when a specific instance or output would change
Same rule applies everywhere the variable is set Rule is about this resource or output in this module context

Example: contains(["dev", "prod"], var.environment) belongs in validation. var.enable_feature gating creation of one terraform_data instance belongs in a lifecycle precondition on that resource.

You may use both — validation on the variable for fast input rejection, plus a precondition when the rule also depends on computed graph values.

precondition vs postcondition

Think of timing relative to the instance operation:

text
before operation  →  precondition  (assumption must hold to proceed)
after attributes  →  postcondition (result must match guarantee)
precondition postcondition
Where lifecycle on resource, data, or ephemeral resource; directly in output lifecycle on resource, data, or ephemeral resource only
Evaluates Before create, update, or destroy on the instance; before output is finalized After planned or applied attributes are known
self references Limited — usually other args, variables, or upstream values Full read of self attributes after evaluation
Fails at Plan, or apply if required values were not known earlier Plan or apply, depending on when attributes become known

Both are errors on Terraform 1.15.8. Choose precondition for inputs and assumptions; choose postcondition for outcomes you want Terraform to verify before dependents proceed.

check vs blocking conditions

check + assert precondition / validation
Attached to Named check block in the root module Variable, resource, data, ephemeral resource, or output
Typical intent Report policy drift or soft assumptions Hard gate before inputs or instance changes
Failure severity Warning Error
Blocks plan (1.15.8) no yes (when evaluated and false)
Blocks apply (1.15.8) no yes (when evaluated and false)

If CI must fail when environment is wrong, put the rule in validation or precondition, not only in a check block.


Custom conditions vs terraform validate

terraform validate checks whether configuration is syntactically valid and consistent with installed provider schemas. It does not evaluate your validation, precondition, postcondition, or check rules against real variable values.

Create validate-vs-custom/ with a valid default and the environment validation block. Run validate:

bash
cd ~/terraform-labs/terraform-validation-checks/validate-vs-custom && terraform validate
output
Success! The configuration is valid.

Validate succeeds even when you are about to pass a bad value at plan time. Supply staging on the command line:

bash
terraform plan -var environment=staging
output
Planning failed. Terraform encountered an error while generating this plan.


Error: Invalid value for variable
...
environment must be dev or prod, not staging.

The configuration is structurally fine — validate passes — but the custom rule fails when Terraform evaluates var.environment during plan. Keep both in CI: terraform validate for schema health, then terraform plan with production-like variable files for business rules.


Reference other values in conditions

Variable validation conditions may reference other variables in the same module. Create variable-validation-cross-var/variables.tf:

hcl
variable "min_port" {
  type    = number
  default = 1024
}

variable "max_port" {
  type    = number
  default = 65535
}

variable "port" {
  type    = number
  default = 8080

  validation {
    condition     = var.port >= var.min_port && var.port <= var.max_port
    error_message = "port must be between min_port and max_port."
  }
}

With defaults, plan succeeds. Create variables-fail.tfvars with min_port = 9000 and port = 8080, then plan:

bash
cd ~/terraform-labs/terraform-validation-checks/variable-validation-cross-var && terraform plan -var-file=variables-fail.tfvars
output
Error: Invalid value for variable
...
    │ var.max_port is 65535
    │ var.min_port is 9000
    │ var.port is 8080

port must be between min_port and max_port.

Terraform shows every referenced variable in the diagnostic. Resource and data source preconditions and postconditions may reference self, other resources, data sources, and variables. Output preconditions may reference the output value expression and its dependencies. check asserts may reference providers and data sources declared in the same module.

Restrictions to remember:

  • validation cannot reference self or managed resource attributes — only other variables and functions
  • Conditions must evaluate to bool; a string or number causes a configuration error
  • Values that are (known after apply) can make a condition fail with "depends on unavailable value" until the graph resolves

Write useful error_message text

Terraform prints your error_message verbatim when condition is false. Vague text wastes the operator's time.

Weak Better
Invalid value. environment must be dev or prod, not staging.
Bad port. port must be at least 1024 (privileged ports not allowed).
Check failed. Check failed: environment must be dev or prod.

Include the allowed set, the expected range, or which variable to fix. You may interpolate ${var.environment} in error_message when showing the actual value helps — as in environment must be dev or prod, not ${var.environment}.


Common condition errors

Symptom Likely cause Fix
condition does not evaluate to bool Expression returns string, number, or null Wrap comparisons so the result is strictly true or false
Invalid reference in validation Condition uses self or a resource attribute Move the rule to precondition or postcondition on that resource
Depends on unavailable value Condition reads an attribute still unknown at evaluation Narrow the rule, use precondition, or split into a check that runs later
Check warns but CI passes Failed assert is a warning on 1.15.8 Duplicate critical rules in validation or precondition if the pipeline must fail
terraform validate passes but plan fails Business rule only exists in validation / lifecycle / check Expected — run plan with real variable values in CI

References


Summary

Terraform gives you four custom condition mechanisms, and they differ in placement, timing, and severity. Variable validation rejects bad input at plan time. lifecycle precondition and postcondition guard resources and data sources before and after their attributes are known; output blocks support precondition directly but not postcondition. check blocks with assert report problems as warnings on Terraform 1.15.8 without blocking apply.

The mistake I see most often is treating check like a hard gate — failed asserts warn but plan and apply still exit zero. Put must-stop rules in validation or precondition. The second mistake is expecting terraform validate to catch business logic; validate only checks syntax and schema, so run plan with realistic variable files for custom rules.

Postconditions can fail during plan when self is already known, which surprises teams who expect them only after apply. Read the diagnostic timing line carefully before you blame the provider.

Next, wire validation into module inputs and outputs and revisit Terraform lifecycle for the other lifecycle meta-arguments that control replacement and destroy behavior.


Frequently Asked Questions

1. What is the difference between Terraform validation and terraform validate?

validation is an HCL block inside a variable that enforces your business rules during plan and apply. terraform validate is a CLI command that checks syntax and schema consistency only. A configuration can pass terraform validate and still fail a validation block when variable values break your rule.

2. Do Terraform check blocks stop apply?

On Terraform 1.15.8, a failed check assert prints a warning during plan and apply but does not block the run. Variable validation, preconditions, and postconditions are errors that stop plan and apply.

3. When should I use a precondition instead of variable validation?

Use variable validation for input rules on a single variable at assignment time. Use a precondition when the rule depends on resource attributes, data sources, or multiple values that are only known after Terraform builds more of the graph.

4. Can a variable validation block reference another variable?

Yes. In Terraform 1.15.8 a validation condition may reference other variables in the same module, such as checking that var.port sits between var.min_port and var.max_port.

5. When does Terraform evaluate a postcondition?

Terraform evaluates postconditions after the resource or data source instance is planned or updated and self attributes are available. Timing depends on when those attributes become known — a failure can appear during plan or during apply. Output blocks do not support postcondition.
Deepak Prasad

R&D Engineer

Founder of GoLinuxCloud with more than 15 years of expertise in Linux, Python, Go, Laravel, DevOps, Kubernetes, Git, Shell scripting, OpenShift, AWS, Networking, and Security. With extensive experience, he excels across development, DevOps, networking, and security, delivering robust and efficient solutions for diverse projects.

  • Go (programming language)
  • Python (programming language)
  • DevOps
  • Computer Security
  • Cloud Computing
  • Kubernetes
  • Linux
  • Ansible (software)