Study guide

9.1 Custom Conditions: validation, precondition, postcondition, check

HashiCorp Certified: Terraform Associate 004

Track your progress as you read

Finished sections get a check mark.

1 Introduction & Exam Overview

1.1 Current Exam Facts 1.2 Format, Delivery, Retakes, and Recertification 1.3 004 Changes, Terraform 1.12, and Study Plan

2 Infrastructure as Code with Terraform

2.1 What Infrastructure as Code Is 2.2 Advantages of IaC Patterns 2.3 Multi-Cloud, Hybrid, and Service-Agnostic Workflows

3 Terraform Fundamentals: Providers & State

3.1 Install and Version Terraform Providers 3.2 How Terraform Uses Providers 3.3 Configuration with Multiple Providers 3.4 How Terraform Uses and Manages State

4 Core Terraform Workflow: Init & Validate

4.1 The Terraform Write–Plan–Apply Workflow 4.2 Initialize a Working Directory 4.3 Validate a Configuration

5 Plan, Apply, Destroy, and Formatting

5.1 Generate and Review an Execution Plan 5.2 Apply Changes to Infrastructure 5.3 Destroy Terraform-Managed Infrastructure 5.4 Formatting and Style Adjustments

6 Resources, Data Sources, and References

6.1 Resource Blocks vs Data Blocks 6.2 Resource Attributes and Cross-Resource References 6.3 Resource Addressing and Interpolation Patterns

7 Variables, Outputs, and Complex Types

7.1 Input Variables, tfvars, and Precedence 7.2 Output Values 7.3 Complex Types: list, map, object, tuple, set

8 Dynamic Configuration and Dependencies

8.1 Expressions, count, for_each, and dynamic Blocks 8.2 Built-in Functions 8.3 Dependencies, depends_on, and Lifecycle Rules

9 Conditions, Secrets, and Sensitive Data

9.1 Custom Conditions: validation, precondition, postcondition, check 9.2 Sensitive Data Handling 9.3 Vault, Ephemeral Values, and Write-Only Arguments

10 Terraform Modules

10.1 How Terraform Sources Modules 10.2 Variable Scope within Modules 10.3 Using Modules in Configuration 10.4 Managing Module Versions

11 Terraform State Management

11.1 The Local Backend 11.2 State Locking 11.3 Remote State and the backend Block 11.4 Resource Drift and State Operations

12 Maintain Infrastructure with Terraform

12.1 Import Existing Infrastructure 12.2 Inspect State with the CLI 12.3 Verbose Logging and Diagnostics

13 HCP Terraform

13.1 Create Infrastructure with HCP Terraform 13.2 Collaboration and Governance Features 13.3 Workspaces and Projects 13.4 HCP Terraform Integrations

9.1 Custom Conditions: validation, precondition, postcondition, check

Key Takeaways Explain This Topic AI

Every custom condition is a boolean expression plus a required error_message; the check passes only when the condition is true

variable validation runs immediately, before Terraform generates a plan, and a failure is a hard error that stops the run

precondition lives on resources, data sources, outputs, and ephemeral resources; postcondition lives on resources, data sources, and ephemeral resources; both fail the run

A check block assert failure is a warning only — Terraform continues the plan or apply, which is the official distinction from the other three

Order on Terraform 1.12: variable validation, then preconditions, then postconditions, then checks at the end of plan and apply

Last updated: August 2026

9.1 Custom Conditions: validation, precondition, postcondition, check

Quick Answer: A custom condition is a boolean expression plus a required error_message . validation on a variable, and precondition / postcondition on a resource, data source, or output, error and stop the run. A check block assert warns and continues . Variable validation runs first, before the plan. Preconditions run after the plan is built and before Terraform creates the object. Postconditions run after planning and applying (or after a data-source read). Checks run last on both plan and apply.

Objective 4g on Terraform Associate (004) is new: validate configuration using custom conditions. The product version on the exam is Terraform 1.12 . HashiCorp's current language pages are Validate your configuration and the check block . The older bookmark Custom conditions now redirects into that validate page.

Ship dates you should be able to place if a question mentions them: variable validation since 0.13 , precondition / postcondition since 1.2 , check blocks since 1.5 . All four are current on 1.12.

The rule that never changes: boolean + error_message

Every custom condition has the same two required arguments:

Argument What it must be

condition An expression that evaluates to a boolean . The check passes only when the result is true . A string, a number, a list, or null is not a condition.

error_message A string Terraform prints when the condition is false . Required on variable validation , precondition , postcondition , and check assert .

Write the condition so a future maintainer can read the intent . Prefer var.environment == "prod" over a pile of && that hides the rule. The error_message can be a literal, a heredoc, or a template ( "${var.image_id} is not an AMI id" ). HashiCorp's validate page says Terraform includes that text when the condition is unmet.

If the condition is unknown during plan — it depends on a value that is (known after apply) — Terraform does not invent true or false . It defers that evaluation until apply, when the value exists.

Four tools, two failure modes

  • Tool Where you write it When Terraform 1.12 evaluates it Failure
  • validation Inside variable Immediately, before generating a plan Error; stops the operation

precondition lifecycle on a resource or data source; directly on an output After generating a plan, before creating the resource, data source, or output Error; stops the operation

postcondition lifecycle on a resource or data source After planning and applying changes, or after reading a data source Error; stops the operation

check / assert Standalone check block Last step of plan or apply, after Terraform has planned or provisioned Warning ; the run continues

That last row is the 4g discriminator. Official check language: it is the only validation that does not block operations. A failed assert is not a failed apply.

Variable validation

Use validation to reject a bad input before Terraform spends time planning. Official jobs for it: format checks, acceptable ranges, and stopping the run when a variable is misconfigured. You can declare more than one validation block on the same variable. variable "image_id" {

  • type = string
  • description = "AMI id for the server."
  • validation {
  • condition = length(var.image_id) > 4 && substr(var.image_id, 0, 4) == "ami-"
  • error_message = "The image_id value must be a valid AMI id, starting with \"ami-\"."
  • }
  • }
  • variable "environment" {
  • type = string
  • validation {
  • condition = contains(["dev", "staging", "prod"], var.environment)
  • error_message = "environment must be dev, staging, or prod."
  • }
  • }

If image_id is "latest" , the condition is false . Terraform prints the error_message and does not generate a plan . That is earlier than a provider API error, and it is the point: fail in your words, not in AWS's.

On Terraform 1.12 a validation condition may also refer to other objects in the same module (that expansion shipped in 1.9). The exam still treats validation as input-shaped : it is how you police var.* , not how you wait for a just-created instance to become healthy. Health after apply belongs on a postcondition or a check .

precondition : assumptions before create

A precondition is an assumption you need to be true before Terraform creates the enclosing object. HashiCorp's wording: use it to verify configuration assumptions for resources, data sources, and outputs before Terraform creates them. Preconditions take precedence over argument errors the provider would have raised on a misconfigured object. resource "aws_instance" "web" {

  • instance_type = "t3.micro"
  • ami = data.aws_ami.example.id
  • lifecycle {
  • precondition {
  • condition = data.aws_ami.example.architecture == "x86_64"
  • error_message = "The selected AMI must be for the x86_64 architecture."
  • }
  • }
  • }

Terraform evaluates that while it builds the plan. If the AMI is arm64 , the run errors with your message and does not create the instance. An output can carry a precondition too, so a bad value never becomes an exported output or a state write: output "instance_public_ip" {

value = aws_instance.web.public_ip

precondition {

condition = length([for rule in aws_security_group.web.ingress : rule if rule.to_port == 80 || rule.to_port == 443]) > 0

  • error_message = "Security group must allow HTTP (port 80) or HTTPS (port 443) traffic."
  • }
  • }

Official 1.12 order text: preconditions run after generating a plan but before creating the resource, data source, or output. During apply that means: plan first, then refuse to create if the assumption is now false.

postcondition : guarantees after apply (or after a read)

A postcondition is a guarantee about the object Terraform just produced or just read. Official 1.12 timing: after planning and applying changes to a resource, or after reading from a data source. Use self inside a postcondition to mean this instance — the same self you already saw in provisioner and connection blocks. data "aws_ami" "example" {

id = var.aws_ami_id

  • lifecycle {
  • postcondition {
  • condition = self.tags["Component"] == "nomad-server"
  • error_message = "tags[\"Component\"] must be \"nomad-server\"."
  • }
  • }
  • }

If that tag is missing, the postcondition fails, Terraform stops, and downstream resources that would have consumed the AMI never cascade. HashiCorp calls that out: postconditions can prevent cascading changes to dependent resources.

Choose pre versus post by asking whether the rule is an assumption you must hold before create, or a guarantee you can only prove after create or read. Same fact can be written either way: a postcondition on the producer, or a precondition on each consumer. If one resource has many dependents, one postcondition on the producer is usually clearer than five preconditions. If the producer and consumer live in different modules , HashiCorp says it can be useful to keep both, so each module keeps verifying the other as they evolve.

Postconditions are static guardrails on a resource or data block. For a live, external, changing signal — "is the website returning 200 right now?" — official docs send you to check blocks, which run after postconditions.

check blocks: continuous assertions that do not fail apply the same way

A check block sits outside the resource lifecycle. Terraform runs it as the last step of plan or apply, after it has planned or provisioned infrastructure. A failed assert is a warning . The apply that just created the load balancer still succeeds . That is the official distinction you must not blur on 4g. 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"
  • }
  • }

Rules from the 1.12 check reference:

At least one assert is required. Multiple assert blocks are allowed; every condition must be true for the check to pass.

A nested data block is optional and is scoped to that check . You cannot reference it from the rest of the module.

Nested data sources are fetched as the final step of plan or apply, so they can see infrastructure this run just built.

If the nested data source's provider errors, Terraform masks those errors as warnings and still does not fail the run.

Use depends_on on the nested data source when you must wait for a managed resource this run is creating. Until then, plan may print known after apply instead of a false warning.

Use checks to validate whole-system behavior and to feed HCP Terraform continuous validation . If a workspace has health assessments enabled, HCP Terraform re-runs check blocks, preconditions, and postconditions on a schedule. HashiCorp still recommends check blocks for that post-apply monitoring, because a failed check alerts you without turning a later plan into a hard error the way a failed postcondition does.

Do not treat check as terraform test . Tests live in .tftest.hcl and judge configuration logic. A check judges deployed infrastructure and, on a normal apply, refuses to fail the apply.

004 traps for objective 4g

A check that prints a warning is not a failed apply. A failed precondition or postcondition is .

condition must be boolean true / false . "Non-empty string" is not a pass.

error_message is required on all four tools.

Variable validation runs before the plan exists. It is not an after-apply hook.

self belongs in postconditions (and provisioners). A precondition usually names other objects ( data.aws_ami.example.architecture ), not self , because the object does not exist yet.

Nested data inside check is not a module-wide data source.

Continuous validation in HCP Terraform can re-run checks, preconditions, and postconditions. It does not change the CLI rule that only check failures are warnings.

Loading diagram... Custom-condition evaluation order on Terraform 1.12