skip to content

When would you reach for a `precondition` or `postcondition` in Terraform instead of a variable `validation` block?

level: seniorimportance: should knowfreq 44%

answer

  1. input, assumption, promise
  2. validation lives on the variable
  3. lifecycle block — except on outputs
  4. self is postcondition-only
  5. a tripwire, not a rollback

basics

~20 s

A validation block checks the module's own input. Preconditions and postconditions check assumptions about everything else: a precondition guards an object before it is created, and a postcondition asserts a guarantee about what was actually produced.

solid answer

~60 s

Variable `validation` answers a question about the argument list: is this input acceptable. `precondition` and `postcondition` answer questions about the world the module is operating in. A `precondition` sits in a `lifecycle` block on a resource or data block — or directly in an `output` block — and is a guard: it must hold before Terraform plans that object, so "the AMI I was handed must be for the architecture this instance type needs" stops the run before anything is built. A `postcondition` is a guarantee about the result, checked after the object is resolved, and it can use `self` to reference the object's own attributes. Both were added in Terraform 1.2, and both can reference anything in the module, which is the other reason they get used — before Terraform 1.9 a validation condition could only see its own variable, so any cross-object rule had to live in a precondition. The mental split I use is: validation guards the door, preconditions guard the assumption, postconditions guard the promise.

code

hcl · 25 lines
hcl
data "aws_ami" "app" {
  most_recent = true
  owners      = ["self"]

  filter {
    name   = "name"
    values = ["app-*"]
  }

  lifecycle {
    postcondition {
      condition     = self.architecture == "x86_64"
      error_message = "The most recent app-* AMI is not an x86_64 image."
    }
  }
}

output "instance_public_ip" {
  value = aws_instance.app.public_ip

  precondition {
    condition     = aws_instance.app.associate_public_ip_address
    error_message = "The instance must have a public IP address for this output to be usable."
  }
}

go deeper

for a junior

Know that the family exists and that validation is the one you write on a variable. Recognise a precondition inside a lifecycle block as a guard the module author added, and do not delete it to make a plan succeed.

for a middle

Explain the placement rules precisely — lifecycle on resources and data sources, directly inside an output block, self only in postconditions — and give one concrete example of an assumption a variable validation cannot express.

for a senior

Show the operational consequence: a postcondition on unknown values fires during apply, after the object exists, so it surfaces the problem without undoing it. Argue for placing each check where its diagnostic will name the right resource.

for a principal

Frame these as the executable form of the assumptions a platform team holds about its estate, and set the standard for which assumptions are worth failing a production apply over versus reporting non-blockingly.

## Three checks, three different subjects Terraform's custom-condition family looks like one feature with three spellings. It is not — the three differ in *what they make a claim about*. | Construct | Lives in | Claims something about | |---|---|---| | `validation` | a `variable` block | the input this module was given | | `precondition` | a `lifecycle` block on a resource or data block, or directly in an `output` block | an assumption that must hold before this object is handled | | `postcondition` | a `lifecycle` block on a resource or data block | a guarantee about the object that resulted | `precondition` and `postcondition` arrived in Terraform 1.2. Both take the same two arguments as a validation: `condition` and `error_message`. ## Precondition: the assumption A precondition is a guard evaluated before Terraform plans or applies the object it is attached to. The classic case is an assumption that spans two things the module did not choose together: ```hcl resource "aws_instance" "app" { ami = var.ami_id instance_type = var.instance_type lifecycle { precondition { condition = data.aws_ami.selected.architecture == "x86_64" error_message = "The supplied AMI must be an x86_64 image for this instance type." } } } ``` No variable validation could express that, because the fact being checked is not in the input at all — it comes from a data source that had to be read first. Preconditions may reference anything in the module: other resources, data sources, locals, several variables at once. An `output` block takes a `precondition` directly, not nested in a `lifecycle` block, and that placement is worth remembering because it is the odd one out. It is how a module refuses to export a value it does not trust. ## Postcondition: the promise A postcondition is checked after the object is resolved, and it is the only one of the three that can use `self` to refer to the object's own attributes: ```hcl data "aws_ami" "app" { most_recent = true owners = ["self"] filter { name = "name" values = ["app-*"] } lifecycle { postcondition { condition = self.architecture == "x86_64" error_message = "The most recent app-* AMI is not an x86_64 image." } } } ``` That is a guarantee about a result the module computed rather than received: a `most_recent` filter will happily pick up whatever was published last night, and the postcondition is how the module notices. Timing has a consequence people miss. When the values a postcondition needs are known at plan time, it is checked during plan and stops the run cheaply. When they are only known after the object exists — a generated address, a computed ARN — the check happens during apply, *after* that object has been created. The apply fails, but the object is real and is recorded in state. A postcondition is a tripwire, not a rollback; Terraform has no transaction to undo. ## Choosing between them Ask what the claim is about. - About a value the caller passed in → `validation`, because it fails earliest, needs no data sources, and the error points at the argument the caller can actually change. - About a relationship between things, or a fact fetched from the provider → `precondition`, attached to the object that depends on the assumption so the error names the right thing. - About something the run produced → `postcondition`. There is a version wrinkle worth stating explicitly in an interview. Before Terraform 1.9, a validation condition could reference only its own variable, so every cross-field rule — "backup_bucket must be set when backups_enabled is true" — had to be written as a precondition. From 1.9 those rules can move back to the variable where they are more discoverable. If a codebase is full of preconditions doing input validation, that is usually the history, not a design choice. ## And the placement question Put the check where the failure will make sense. A precondition attached to the resource that relies on the assumption produces an error naming that resource; the same condition hoisted into an unrelated place produces an error somebody has to go and decode. All three constructs are documentation that executes, and the address in the diagnostic is part of the message.

  • A postcondition fails during apply, after the resource was created. What is the state of the world?
    The resource exists in the cloud and is recorded in state; the apply reports an error and stops. There is no rollback — Terraform has no transaction to undo, so a postcondition is a tripwire that surfaces the problem, not a guard that prevents it. If the check can be made at plan time on known values, it fires there instead and costs nothing.
  • Where exactly do these blocks go — is it always inside `lifecycle`?
    Almost. `precondition` and `postcondition` are nested in a `lifecycle` block on a `resource` or `data` block. The exception is an `output` block, which takes a `precondition` directly with no `lifecycle` wrapper and supports only preconditions. That asymmetry is a common interview trip-up.
  • Why is `self` available in a postcondition but not in a precondition?
    Because a precondition runs before the object it belongs to has been planned or created, so there is no instance to refer to — referencing itself would be circular. A postcondition runs once the object is resolved, so `self.attribute` is meaningful and is how you assert facts about what was actually produced.

saying these in an interview costs you the question

  • Says preconditions and postconditions are just validation with another name
  • Expects a failed postcondition to roll back the created resource
  • Puts a precondition inside a lifecycle block on an output
  • Tries to use self in a precondition
  • Uses postconditions for input checks that belong on the variable

context