Terraform 1.2 puts resource assumptions in configuration
Preconditions and postconditions give modules explicit contracts around resource behavior, while replace_triggered_by connects replacement to upstream changes.
A correctly typed input can still describe infrastructure that a module cannot use. An image identifier can be a valid string and identify an image for the wrong CPU architecture. Terraform 1.2, released on May 18, adds preconditions and postconditions so module authors can express those requirements alongside the resources that depend on them. The change extends validation beyond an individual input variable.
This matters because a module often relies on properties that emerge only after reading another object. The custom-condition documentation included in 1.2.0 distinguishes assumptions about a resource's inputs from guarantees about its results. Terraform can check both, report an author-supplied error, and stop further execution when the condition fails.
I value Terraform's ability to make infrastructure definitions reusable without hiding the APIs underneath them. Resource conditions improve that arrangement because a module can explain the constraints that its types do not capture. The relevant question becomes which object owns the requirement and when Terraform can obtain the information needed to check it.
The limits of input validation
Input variable validation remains useful for requirements about the input itself. In this version, a variable validation expression can refer only to that variable. It can check a string's format or whether members of a collection meet a rule, but it cannot inspect an independently read resource to establish a relationship between them.
A resource precondition has a wider context. Its expression can refer to other objects in the same module, provided those references do not create a dependency cycle. Terraform evaluates resource preconditions after expanding count or for_each, so conditions can use the corresponding instance information. They run before evaluation of the resource's configuration arguments and can take precedence over argument-evaluation errors.
For an existing image data source, the essential condition can be written inside the consuming resource's lifecycle block. This fragment assumes that the surrounding module already declares data.aws_ami.example and configures the resource that will use it.
lifecycle {
precondition {
condition = data.aws_ami.example.architecture == "x86_64"
error_message = "The selected image must use the x86_64 architecture."
}
}
The distinction between format and meaning is concrete here. A variable rule could recognize a syntactically acceptable identifier. Reading the image supplies its architecture, and the precondition expresses the consuming resource's requirement. The error belongs near the configuration whose operation depends on that architecture.
Putting the condition at that location also documents why the choice matters. A maintainer who changes the resource can see its assumption without searching a separate operational procedure. This is especially useful when a module accepts identifiers for objects that another configuration owns. The caller can select the object while the module retains responsibility for stating the properties it requires.
Guarantees belong with their producer
A postcondition checks a result after Terraform evaluates the associated object. Resources and data sources can include both kinds of condition inside lifecycle. A resource postcondition can use self to refer to the instance under evaluation, so it does not have to refer to that instance through its full resource address.
The documentation uses a private DNS name as one example of a guarantee that downstream resources may require. The relevant lifecycle fragment is short.
lifecycle {
postcondition {
condition = self.private_dns != ""
error_message = "The instance must have a private DNS hostname."
}
}
This is a guarantee about the produced object. A failed postcondition stops changes to dependent resources. If several consumers all require the same property, one postcondition on the producer can avoid duplicating the requirement throughout the configuration.
The placement still involves judgement. A precondition near a consumer can explain a requirement specific to that consumer. A postcondition near a producer can state a property that all its callers should be able to rely on. The versioned documentation recommends choosing according to that distinction and also considering where the resulting error message will be most helpful.
I prefer requirements that state the module's operational meaning directly. A CPU architecture or an allocated hostname is more informative than a generic assertion that provisioning succeeded. Conditions let the configuration distinguish an object that exists from an object that satisfies the assumptions of the rest of the graph.
Planning cannot check values it does not know
The new conditions do not make every failure detectable during planning. Terraform evaluates them as early as it can, but a condition that depends on an unknown value must wait until apply. A condition based on an existing image's known architecture can be checked earlier than one based on an identifier that a remote service has not assigned yet.
This timing is part of the feature's semantics. During apply, a failed precondition prevents the associated planned action. A failed postcondition occurs after the associated action has happened. It blocks dependent actions but does not undo the action already completed. Treating postconditions as transactional rollback would therefore give the wrong account of what Terraform guarantees.
The distinction can even change across runs. The documentation notes that initial creation often provides less information than subsequent changes. A check that waits until apply during the first run may be evaluable during planning later, when the relevant object already exists. The expression can remain identical while the availability of its inputs changes.
Data-source checks need attention to ordering as well. The documentation discourages declaring a managed resource and a data source for the same object without considering how their results relate. When a data source checks a result that the managed resource does not export directly, its check should be a postcondition of the data source. That placement tells Terraform the read is checking an object defined elsewhere and lets it order operations accordingly.
Output values support preconditions too. An output precondition can prevent Terraform from saving an invalid new output in state and, where applicable, preserve a valid previous value. Although the syntax says precondition, its role at the module boundary is to check the guarantee the module makes to consumers before finalizing the output.
Replacement can depend on an upstream change
Terraform 1.2 also adds replace_triggered_by. The versioned lifecycle reference describes a list of references to managed resources, resource instances, or their attributes. Changes to those references can require replacement of the containing resource.
The granularity of the reference matters. Referring to a resource with multiple instances means an update or replacement of any instance can trigger replacement. A reference to one instance restricts that replacement relationship to the selected instance alone. Referring to a particular attribute makes a change in that attribute the trigger. In a repeated containing resource, count.index or each.key can select the corresponding upstream instance.
The documentation illustrates the attribute form with an autoscaling target tied to an ECS service identifier. This is a lifecycle fragment for that relationship, assuming the service is declared elsewhere in the module.
lifecycle {
replace_triggered_by = [
aws_ecs_service.svc.id
]
}
The allowed references are managed resources. This rule connects replacement to the changes Terraform tracks for managed objects. A dependency that establishes ordering and a replacement rule express different requirements, so replacement behavior deserves its own declaration when the relationship needs it.
Conditions and replacement triggers solve related configuration problems without doing the same job. A condition rejects an assumption or result that is false. A replacement trigger specifies an action when an upstream managed object changes. The former supplies an explanatory failure, while the latter changes the proposed lifecycle action.
Error messages are part of the contract
Conditions require an error_message expression that evaluates to a string. The release also allows expression-based messages for custom variable validations, so relevant values can appear in the diagnostic. The documentation recommends complete sentences and says Terraform presents the message with the object location and values involved in the expression.
That gives module authors a precise place to explain the failed requirement. A message about an image architecture can tell the caller which property is unacceptable without requiring them to decipher a provider error from a later operation. The condition and message together explain what the module expects and why it stopped at that point.
These checks remain bounded by the data Terraform can observe. A postcondition about a returned attribute establishes that attribute's value at evaluation time. It does not establish an unrelated property of the running application. The practical improvement is that assumptions already necessary for a resource or module output can now be expressed in the configuration and evaluated in the dependency order that makes their inputs available.