9.1 Custom Conditions: validation, precondition, postcondition, check

Key Takeaways

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

ArgumentWhat it must be
conditionAn 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_messageA 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

ToolWhere you write itWhen Terraform 1.12 evaluates itFailure
validationInside variableImmediately, before generating a planError; stops the operation
preconditionlifecycle on a resource or data source; directly on an outputAfter generating a plan, before creating the resource, data source, or outputError; stops the operation
postconditionlifecycle on a resource or data sourceAfter planning and applying changes, or after reading a data sourceError; stops the operation
check / assertStandalone check blockLast step of plan or apply, after Terraform has planned or provisionedWarning; 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
Test Your Knowledge

On Terraform 1.12, what must a custom condition expression evaluate to for Terraform to treat the check as passing?

A
B
C
D
Test Your Knowledge

A check block assert evaluates to false at the end of terraform apply. What does Terraform 1.12 do?

A
B
C
D
Test Your Knowledge

When does Terraform 1.12 evaluate input variable validation blocks?

A
B
C
D