
The terraform_data resource (Terraform v1.4+) gives you a place to keep arbitrary data, trigger actions, and run provisioners without standing up any real infrastructure. It ships with Terraform, so there's no provider to install. People reach for it to store lifecycle-managed values, to host provisioners when nothing else fits, and as the modern stand-in for null_resource. This guide walks through what it does, how to use it sensibly, and how to migrate off null_resource.
terraform_data vs. null_resource
terraform_data (v1.4+) is Terraform's built-in replacement for the hashicorp/null provider's null_resource. It needs no external provider, its triggers_replace argument takes any value type (where null_resource's triggers only took strings), and it gives you explicit input and output attributes for handling data. For new configurations, it's the one to use.
terraform_data Have?terraform_data has two main arguments. input (optional) stores arbitrary data, and changing its value plans an in-place update. triggers_replace (optional) forces a replacement when its value changes. There are also two read-only attributes: output, which mirrors input's value, and id, the unique resource identifier. Whether you reach for input or triggers_replace decides whether downstream actions see an update or a full replacement.
| Name | Type | Description | Behavior on Change |
|---|---|---|---|
input |
Argument | (Optional) Arbitrary data stored in state, reflected in output. |
Plans an in-place update for terraform_data. |
triggers_replace |
Argument | (Optional) Arbitrary data whose change forces resource replacement. | Forces replacement of terraform_data. |
output |
Attribute | Value of the input argument. |
Changes if input changes. |
id |
Attribute | Unique resource identifier. | N/A (assigned by Terraform) |
terraform_data?terraform_data is useful for:
lifecycle.replace_triggered_by (e.g., redeploying services based on a version change).for_each for dynamic data handling or actions. While versatile for decoupling logic, avoid over-reliance to maintain clarity.triggers_replace Handle Complex Triggering and Dependencies?triggers_replace does the heavy lifting in trickier setups. It forces a replacement when its computed value changes from the previous state, and that value can be any type: a string, list, map, filemd5(), timestamp(), and so on. That lets you trigger on all sorts of conditions. Boolean flags are where people get tripped up. triggers_replace reacts to a boolean changing (false to true, or true to false), not to its sitting at true. The usual fix is to create the resource conditionally with for_each (e.g., for_each = var.enable_feature ? toset(["active"]) : toset()).
terraform_data?terraform_data often hosts create and destroy provisioners when no other resource is a sensible home for them. Inside a provisioner, the self object gives you the input, output, and triggers_replace values, which you need for context-aware actions and especially for stateful cleanup in destroy provisioners. That said, treat provisioners as a last resort. Reach for native Terraform resources, data sources, cloud-init, or a real configuration management tool first. Once you use a provisioner, keeping the script idempotent and tracking any state outside Terraform is on you.
terraform_data?Effective use of terraform_data involves:
terraform_data instance.terraform_data is a utility for ancillary tasks, and that management of any state altered by its provisioners falls to the user.terraform_data?Be aware of:
local-exec provisioners create dependencies on the execution environment's tools.triggers_replace behavior with boolean flags (reacts to change, not absolute value).input values can bloat the state file.null_resource to terraform_data?A moved block makes migrating from null_resource to terraform_data (recommended on Terraform v1.4+) easy. It lets you change the resource type and rename triggers to triggers_replace while keeping the state history. Once you've defined the terraform_data resource and the moved block, terraform plan should show no net changes, which tells you the state migrated cleanly. You can drop the moved block after you apply.
terraform_data?terraform_data is a built-in for holding arbitrary data, triggering actions, and standing in for null_resource. Because it pulls these patterns into Terraform itself, you depend on fewer external pieces. Reach for native resources and data sources first. When you do use it, keep your provisioner scripts idempotent and free of hardcoded secrets, and don't let one terraform_data block do too many jobs at once. Later Terraform releases may smooth out the rougher edges around complex triggering logic.

