Terraform 1.4 gives ordinary values a resource lifecycle
The new terraform_data resource turns value changes into planned actions and separates ordinary updates from replacement triggers.
A variable can change without having a resource lifecycle of its own. That becomes a problem when another resource needs replacement in response to the change. Terraform's replace_triggered_by argument examines planned resource actions, so a plain variable does not provide the information it expects. Terraform 1.4, released on March 8, adds terraform_data to represent such values as managed resources.
The new type also supplies a place for provisioners when there is no suitable remote resource to contain them. Both uses follow the same design. Terraform tracks an instance with ordinary create, update, and destroy behavior, while the resource itself performs no external API operation. Its useful state is the value supplied by configuration and the lifecycle decisions attached to it.
I like Terraform's ability to express reusable infrastructure behavior above different APIs. A resource that stores data may initially sound unrelated to that purpose. In practice, it gives configuration authors a way to express a dependency in terms the planner already understands, without inventing a remote object to carry it.
Built in without a provider installation
The release-specific resource documentation identifies terraform.io/builtin/terraform as the provider source. No provider configuration or installation is required for this resource. The resource still uses Terraform's managed-resource model, including an instance ID and computed output.
A basic configuration supplies an input value and exports the recorded result. The following example keeps the revision numeric throughout.
variable "revision" {
type = number
default = 1
}
resource "terraform_data" "revision" {
input = var.revision
}
output "recorded_revision" {
value = terraform_data.revision.output
}
After apply, output reflects input. The input can have any supported value type. This matters when the value represents structured configuration, since a caller does not have to reduce it to a string simply to put it under lifecycle management. The documentation also specifies that an unknown output during planning retains the input's type.
The implementation declares input and triggers_replace with dynamic types. It declares output as computed and id as a computed string. Configuration validation rejects attempts to set either computed attribute directly. During refresh, the resource returns its prior state, which is consistent with the absence of a remote system to query.
That refresh behavior defines an important limit. This resource records a value within Terraform's state. It does not independently verify an external fact represented by that value. Recording a revision number is useful for lifecycle decisions, but the existence of the stored number does not establish that software with that revision is running anywhere.
Input updates and replacement are distinct
Changing input causes a planned update and recomputes output. Changing triggers_replace forces replacement of the resource instance. These are separate arguments because changing stored data and creating a new lifecycle instance are different operations. Configuration can use either behavior deliberately.
For replacement triggered by a plain variable, the resource documentation shows input on a terraform_data resource and a reference to that resource in another resource's replace_triggered_by. The downstream argument can react to the planned update. The data resource itself does not have to undergo replacement for the downstream resource to be replaced.
The downstream resource refers to the managed instance in its lifecycle block. The following fragment attaches replacement behavior to the recorded revision.
lifecycle {
replace_triggered_by = [terraform_data.revision]
}
Here terraform_data.revision refers to the resource in the earlier example. The fragment belongs inside a real managed resource whose remaining arguments depend on its provider. A change to the revision creates a planned action that the lifecycle argument can inspect. A direct reference to var.revision in that argument would lack a managed resource's planned action.
Using triggers_replace addresses a different requirement. If the data resource contains a creation-time provisioner, merely updating its input does not rerun that provisioner. The provisioner documentation for this release specifies that creation provisioners run during creation. Replacement supplies a new creation operation, so a value in triggers_replace can control when that operation happens.
Provisioners still have their existing limits
The resource documentation includes a bootstrap example whose replacement triggers are the IDs of two instances. When either changes, Terraform replaces the data resource and runs its local bootstrap provisioner for the new lifecycle. This gives an action that depends on several resources its own address and explicit replacement criteria.
That address does not give Terraform knowledge of arbitrary script behavior. The provisioner documentation explains that Terraform cannot model a script's actions in its plan. It also recommends using provider-supported resource operations or machine initialization mechanisms where possible. Moving a provisioner into terraform_data changes where the lifecycle belongs, while retaining those limits.
A creation-time provisioner failure normally marks its containing resource as tainted and fails the apply. A later apply can then plan recreation. Since terraform_data does not itself manage an external object, its recreation is a lifecycle event in state. Any external effects left by the failed script still depend on that script's behavior. The resource cannot infer how much of an arbitrary operation completed before failure.
I prefer a dedicated provider resource when an external object has an API and an ongoing identity. Writing providers is part of what makes that integration model appealing to me. terraform_data is useful where the missing element is a planned action for a value. It does not supply the read and update semantics that a proper provider resource can give an external service.
The lifecycle record has an implementation
The planner's branches make the distinction precise. Creating a data instance produces an unknown ID and an output derived from the input's type. A changed replacement trigger requests replacement and a new ID. A changed input alone leaves the instance identity in place and marks output for recomputation. Apply copies the input to output when needed and generates an ID for a new instance.
Those rules explain how to read a plan containing this resource. An update can represent changed recorded data, even though no external API operation belongs to the resource itself. Replacement can represent an intentional new execution point for an attached provisioner. Downstream lifecycle expressions can react to either planned action according to their references. Terraform 1.4 supplies those decisions directly in its built-in resource implementation.