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
9.1 Custom Conditions: validation, precondition, postcondition, check
Quick Answer: A custom condition is a boolean expression plus a required
error_message.validationon a variable, andprecondition/postconditionon a resource, data source, or output, error and stop the run. Acheckblockassertwarns 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
assertis required. Multipleassertblocks are allowed; everyconditionmust betruefor the check to pass. - A nested
datablock 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_onon the nested data source when you must wait for a managed resource this run is creating. Until then, plan may printknown after applyinstead 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
checkthat prints a warning is not a failed apply. A failedpreconditionorpostconditionis. conditionmust be booleantrue/false. "Non-empty string" is not a pass.error_messageis required on all four tools.- Variable
validationruns before the plan exists. It is not an after-apply hook. selfbelongs in postconditions (and provisioners). Apreconditionusually names other objects (data.aws_ami.example.architecture), notself, because the object does not exist yet.- Nested
datainsidecheckis 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
checkfailures are warnings.
On Terraform 1.12, what must a custom condition expression evaluate to for Terraform to treat the check as passing?
A check block assert evaluates to false at the end of terraform apply. What does Terraform 1.12 do?
When does Terraform 1.12 evaluate input variable validation blocks?