Terraform

Terraform precondition / postcondition: Declarative Assertions

2026-04-19
NicheeLab Editorial Team

precondition / postcondition let you embed declarative "must hold" conditions directly inside resources, data sources, and other blocks. They are not procedural checks — Terraform evaluates them automatically along the plan / apply lifecycle.

On the upper-level Terraform certification, common topics include differences in evaluation timing, how unknown values are handled, and choosing between variable validation, precondition, postcondition, and check. In real codebases they are also a key guardrail for building IaC that does not silently break.

Basics and goals of precondition / postcondition

precondition runs before the operation (during plan and the early phase of apply) and guarantees that the configuration and inputs satisfy the assumed prerequisites. postcondition runs after the operation (after apply, or after a data read) and guarantees that the created, updated, or fetched result matches expectations.

These are declarative assertions with no state-changing side effects. When a condition is violated, Terraform fails the plan or apply and surfaces the explicit error_message to the user.

  • Where you can use them: resource, data, module calls, and output — declared as a nested block inside the corresponding HCL block.
  • The condition expression must return a boolean (use functions like can() and length() to keep it safe).
  • On violation Terraform errors out — you cannot downgrade it to a warning or skip it.
  • The declaration lives right next to the code, doubling as both documentation and a guardrail.

Minimal example (conceptual)

resource "some_resource" "app" {
  # ... argument declarations

  precondition {
    condition     = contains(["dev", "stg", "prod"], var.env)
    error_message = "env must be one of dev / stg / prod"
  }

  postcondition {
    condition     = self.id != null
    error_message = "No ID was returned after creation (check the provider response)"
  }
}

Evaluation timing and unknown values

precondition is evaluated at plan time. The expression must be true (or be unknown so it can be re-evaluated at apply). If it is definitively false, the plan fails. When the value is unknown at plan time, the plan notes that it will be re-checked at apply, and Terraform does so during apply.

postcondition is evaluated immediately after the target's create / update / read. If the final value is already known at plan time and is false, the plan fails; in most cases, though, it is evaluated against the actual value at apply. A violation does not trigger a rollback — you fix the configuration or environment and re-apply.

  • For data blocks, use postcondition to verify what was read; verify input validity with precondition.
  • For resource blocks, use postcondition to verify the actual values observed after create or update.
  • Wrap expressions that may be unknown in can() or try() to keep plan-time evaluation stable.

Where pre / post are evaluated during plan and apply

terraform planevaluate preconditions (true/false/unk)PLAN FAILif any falseterraform applyre-evaluate preconditions (if unk)Provider create/update/readproceed if all trueevaluate postconditions(true/false)APPLY failureif any false (no rollback)Where pre / post are evaluated during plan and apply

Writing for unknown-tolerance with can / try

resource "some_resource" "svc" {
  name = var.name

  precondition {
    # var.labels may be null/unknown. Guard missing-key access with can()
    condition     = can(var.labels["team"]) && length(try(var.labels["team"], "")) > 0
    error_message = "labels.team is required"
  }
}

Syntax and reference rules (self, expressions, error messages)

Nest precondition / postcondition inside the target block and specify condition and error_message. The condition must evaluate to a boolean. The error_message is shown directly to the user when violated, so make it concrete and tell the reader what action to take.

Inside postcondition on a resource or data block, self refers to the final value of that object. Inside precondition, self refers to the value during planning (which can be unknown). Module calls and outputs also support conditions, but what you can reference and when it is evaluated follow the surrounding block.

  • Expressions must be side-effect-free (no external API calls, etc.).
  • regex() / can() / try() are handy for regex and type checks.
  • Break long conditions into named locals to keep them readable.

Declaration examples on resource and output

# resource example (conceptual)
resource "some_resource" "db" {
  name    = var.name
  version = var.version

  precondition {
    condition     = length(var.name) <= 20 && can(regex("^[a-z0-9-]+
NicheeLab を読み込み中…
quot;, var.name)) error_message = "name must be <= 20 chars and only lowercase letters, digits, and hyphens" } postcondition { condition = self.id != null && length(tostring(self.id)) > 0 error_message = "Missing ID after creation — check the provider and permissions" } } # output example (asserts URL shape) output "service_url" { value = var.base_url precondition { condition = can(regex("^https?://", var.base_url)) error_message = "base_url must use an http(s) scheme" } }

Real-world patterns (using them as guardrails)

Enforcing environment policy: catch "production must have encryption and tags enabled" rules early via precondition. Combinations of inputs (e.g. prod requires redundancy) can also be expressed here.

Result verification: use postcondition to verify that the final state from the provider or an external system meets your requirements — for example whether the assigned CIDR is in an allowed range, or whether the returned endpoint matches the expected format.

  • Encapsulate policy-able rules inside the module so callers keep their freedom while safety is guaranteed.
  • Verify attributes that are often unknown in postcondition instead of precondition, so plans stay stable.
  • Cross-resource invariants are best expressed in a check block.

Environment-policy and result-verification example (conceptual)

# Environment policy (prod requires redundancy)
resource "some_resource" "api" {
  replicas = var.replicas
  env      = var.env

  precondition {
    condition     = var.env != "prod" || (var.env == "prod" && var.replicas >= 2)
    error_message = "replicas >= 2 is required in the prod environment"
  }

  # Example: verify the assigned identifier carries the expected prefix
  postcondition {
    condition     = can(regex("^api-", tostring(self.id)))
    error_message = "Created ID violates policy (prefix 'api-' is required)"
  }
}

Pre / post vs. related features (comparison table)

Variable validation and the check block serve similar purposes but differ in scope and evaluation timing. On the exam, separate the options by asking "where, when, and what are we verifying?".

postcondition is unique in that it can verify "what was actually produced." It fits invariants that cannot be guaranteed from inputs alone.

  • Input validity → validation / precondition
  • Final result validity → postcondition
  • Cross-resource consistency → check
FeatureDeclared inEvaluation timingself / other references
variable validationvariable blockAt plan time (when inputs are evaluated)No self (uses var.*)
preconditionresource/data/module/outputAt plan time (re-evaluated at apply if unknown)self is available on resource / data
postconditionresource/data/module/outputRight after apply (or right after read)self is available on resource / data
check blockRoot moduleAt plan time (re-evaluated at apply if unknown)No self (any expression)

check block (cross-cutting assertion example)

check "naming_convention" {
  assert {
    condition     = can(regex("^proj-[a-z0-9-]+
NicheeLab を読み込み中…
quot;, var.project_id)) error_message = "project_id must start with 'proj-'" } }

Pitfalls and exam prep (best practices)

Terraform does not automatically roll back on a postcondition violation. Be aware that state and real resources can end up in a half-applied state, and include the next action (re-apply, fix the config) in the error message — it pays off in practice.

Mind unknown handling: guard plan-time-uncertain expressions with can() / try(), or move the check into postcondition to stabilize the plan diff.

  • Decompose long, complex conditions into locals to make them easier to test.
  • A useful division: module authors declare internal invariants, while callers declare environment preconditions (organizational rules).
  • Include a remediation hint in error_message (e.g. concrete allowed values, links to related docs).

Split conditions into locals for readability

locals {
  is_valid_env     = contains(["dev", "stg", "prod"], var.env)
  is_replica_valid = var.env != "prod" || var.replicas >= 2
}

resource "some_resource" "svc" {
  replicas = var.replicas
  env      = var.env

  precondition {
    condition     = local.is_valid_env && local.is_replica_valid
    error_message = "The env / replicas combination violates policy"
  }
}

Check yourself

Pro

問題 1

A team must verify that a production service resource actually has redundancy enabled after apply. Checking the input values alone is not enough — they want to evaluate the provider's final response. Which Terraform feature should they use, and where should they declare it?

  1. Use a postcondition inside the target resource block and verify redundancy is enabled using self's final value
  2. Use a validation block on a variable to enforce replicas >= 2
  3. Use a check block to evaluate an arbitrary expression across resources
  4. Enable prevent_destroy in the resource's lifecycle settings

正解: A

Verification based on the actual result (the final value returned by the provider) is exactly what postcondition is for. A postcondition inside the resource can evaluate self's observed values. variable validation only sees inputs, check is for cross-cutting assertions, and prevent_destroy only blocks deletion — none of those match the requirement.

Frequently Asked Questions

Which blocks support precondition / postcondition?

You can declare them inside resource, data, module call, and output blocks. The condition expression must return true or false, and on a violation Terraform fails the operation and surfaces the error_message.

What happens when a condition involves a value that is unknown at plan time?

If the expression cannot be evaluated at plan time (it is unknown), the plan notes that the check will be re-evaluated at apply, and Terraform re-checks it during apply. To keep plans stable, guard with can() / try() or move the check to a postcondition that evaluates the final value.

Does Terraform roll back when a postcondition fails?

No, there is no automatic rollback. Apply just exits with an error. You need to fix the configuration or environment and re-apply (and you may need to manually reconcile state on the provider or external system side).

Check what you learned with practice questions

Practice with certification-focused question sets

無料で問題を解いてみる
Author

NicheeLab Editorial Team

NicheeLab editorial team focused on data engineering and cloud certification learning. Content is structured around practical study needs and official exam domains.


Related articles
Terraform

HCL Syntax: Terraform's Configuration Language (2026)

HCL2 fundamentals for Terraform — blocks, attributes, expres...

Terraform

Terraform Authoring & Operations Pro: Complete Guide (2026)

Tactics for the Terraform Pro exam — module authoring, works...

Terraform

Terraform Providers: Plugin Management Fundamentals (2026)

Provider mechanics — required_providers, versions, mirrors, ...

Terraform

Terraform Resource Blocks: Declarative Infra Units (2026)

Resource block fundamentals — addresses, references, common ...

Terraform

Terraform Data Sources: Read-Only External Data (2026)

Data source basics — declaration, refresh behavior, dependen...

Browse all Terraform articles (102)
© 2026 NicheeLab All rights reserved.