
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
As Terraform's built-in successor to the hashicorp/null provider's null_resource, terraform_data (v1.4+) offers key advantages: it requires no external provider, its triggers_replace argument supports all value types (unlike null_resource's string-only triggers), and it provides explicit input/output attributes for data handling. It is the recommended choice for new configurations.
terraform_data uses key arguments: input (optional) stores arbitrary data, causing an update if its value changes, and triggers_replace (optional) forces resource replacement if its value changes. Read-only attributes include output (reflecting input's value) and id (unique resource identifier). The choice between input and triggers_replace dictates 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 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.The triggers_replace argument is central for advanced scenarios, forcing resource replacement when its computed value (any data type: string, list, map, filemd5(), timestamp()) changes from the previous state. This allows precise triggering based on diverse conditions. A common challenge involves boolean flags: triggers_replace reacts to a boolean's change (e.g., false to true or true to false), not its static true state. The idiomatic solution is often conditional resource creation using for_each (e.g., for_each = var.enable_feature ? toset(["active"]) : toset()).
terraform_data often hosts create and destroy provisioners when no other logical resource fits. Within provisioners, the self object provides access to input, output, and triggers_replace values, crucial for context-aware actions, especially stateful cleanup in destroy provisioners. However, provisioners are a last resort. Prefer native Terraform resources, data sources, cloud-init, or dedicated configuration management tools. Using provisioners means the user is responsible for script idempotency and any state managed outside Terraform.
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.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_resourceMigrating from null_resource to terraform_data (recommended for Terraform v1.4+) is streamlined using a moved block. This allows changing the resource type and renaming triggers to triggers_replace while preserving state history. After defining the terraform_data resource and the moved block, terraform plan should show no net changes, confirming a successful state migration path. The moved block can be removed after applying the changes.
terraform_data is a handy built-in for holding arbitrary data, triggering actions, and standing in for null_resource. It pulls common patterns into Terraform itself, so you lean on fewer external pieces. Use it with some restraint. Reach for native resources and data sources first, keep your provisioner scripts idempotent and free of hardcoded secrets, and don't let one terraform_data block do too many jobs at once. It covers a lot of ground today, and later Terraform releases may smooth out the rougher edges around complex triggering logic.

