skip to content

In Terraform, what is terraform_data for, and why did it supersede null_resource as the place to hang a provisioner?

level: middleimportance: nice to knowfreq 34%

answer

  1. a resource that creates nothing
  2. somewhere to hang a local-exec
  3. built in, so no extra provider
  4. triggers_replace accepts any value
  5. input echoed to output on apply

basics

~20 s

terraform_data is a built-in resource, added in Terraform 1.4, that does nothing itself but can hold provisioners or store values. It replaces null_resource because it needs no third-party provider, and its triggers_replace argument forces replacement when a value changes.

solid answer

~50 s

Sometimes you need a resource-shaped hook that is not attached to any real infrastructure — somewhere to hang a `local-exec`, or a way to force something to be replaced when an arbitrary value changes. The traditional answer was `null_resource` from the `hashicorp/null` provider, with a `triggers` map of strings. Terraform 1.4 added `terraform_data`, which does the same job as a **built-in** resource type: no extra provider to declare in `required_providers`, no entry in `.terraform.lock.hcl`, no extra plugin download in CI. Its arguments are `triggers_replace` — any value, and changing it forces the resource to be replaced — plus `input`, whose value is echoed to the `output` attribute after apply, which is handy for pinning a value through a dependency edge. `null_resource` still works and is not removed, but new code should use `terraform_data`. Both remain escape hatches: hanging a provisioner off one is still an imperative step outside the plan.

code

hcl · 12 lines
hcl
resource "terraform_data" "config_reload" {
  triggers_replace = [
    aws_instance.web.id,
    filemd5("${path.module}/app.conf"),
  ]

  input = filemd5("${path.module}/app.conf")

  provisioner "local-exec" {
    command = "./reload.sh ${aws_instance.web.public_ip}"
  }
}

go deeper

for a junior

Recognise terraform_data as a do-nothing resource used to hold a provisioner or a value, and know it is the modern replacement for null_resource.

for a middle

Contrast triggers_replace with the string-map triggers it replaces, explain the input/output pair, and say why dropping the hashicorp/null provider simplifies init and the lock file.

for a senior

Point out that removing the provider dependency does not fix the provisioner: effects stay outside state and the plan, so the script must tolerate arbitrary re-runs on replacement.

for a principal

Own where imperative glue is permitted at all, and treat a growing set of terraform_data hooks as a signal that something should be modelled by a real provider resource or moved out of the apply path.

## The problem both resources solve A provisioner has to live inside a `resource` block. But plenty of imperative glue is not tied to any single piece of infrastructure — you want to run a command once, or once whenever some value changes, without pretending it belongs to a particular instance. You also occasionally want a node in the dependency graph that carries a value but creates nothing. ## null_resource, the historical answer `null_resource` comes from the `hashicorp/null` provider. It creates nothing; its only meaningful argument is `triggers`, a **map of strings**. Because the values must be strings, non-string data has to be converted — the familiar idiom is `jsonencode(...)` or `md5(...)` around the real value. When any value in the map changes, the resource is replaced, and replacement re-runs the provisioners attached to it. ```hcl resource "null_resource" "reload" { triggers = { config_hash = md5(file("${path.module}/app.conf")) } provisioner "local-exec" { command = "./reload.sh" } } ``` The cost is a dependency: `hashicorp/null` must be resolvable, it appears in `required_providers` and in `.terraform.lock.hcl`, and every `terraform init` — including on air-gapped or restricted CI runners — has to fetch another plugin binary to do nothing. ## terraform_data, added in Terraform 1.4 `terraform_data` is a **built-in** managed resource type. There is no provider to declare and nothing to download; it is part of Terraform itself. It exposes: - `triggers_replace` (optional, any type) — a value stored in state; when it changes, the resource is replaced. Unlike `triggers`, it accepts any value, so a list, a map, or a resource ID works directly without string conversion. - `input` (optional, any type) — a value to store. - `output` — the value of `input` from the most recent apply. - `id` — a generated string identifier. ```hcl resource "terraform_data" "reload" { triggers_replace = [ aws_instance.web.id, filemd5("${path.module}/app.conf"), ] provisioner "local-exec" { command = "./reload.sh ${self.id}" } } ``` ## What input and output are actually for The `input`/`output` pair is the less obvious half. `output` takes the value `input` had at the last successful apply, which lets you pin a value across applies and hand it to something else through a normal dependency reference. A typical use is capturing a value that would otherwise be recomputed every plan, or building an explicit graph edge between two things that have no natural attribute reference. Because `output` is only updated on apply, referencing it gives you last-applied semantics rather than current-configuration semantics. ## Migrating `null_resource` is not deprecated in the sense of being removed — existing code keeps working, and the null provider is still published. But new code has no reason to reach for it. The mechanical differences to watch when converting: `triggers` (map of strings) becomes `triggers_replace` (any value), the resource address changes, and because the address changes the object would be destroyed and recreated. If the resource merely holds a hash and re-running the provisioner is harmless, let it replace; if not, a `moved` block will not help across different resource *types*, so plan the recreation deliberately. ## The honest caveat Swapping `null_resource` for `terraform_data` removes a provider dependency. It does not make the provisioner any better. Everything true of provisioners generally is still true here: the command's effects are not in state, nothing appears in the plan beyond "this resource will be replaced", the script must be idempotent because you cannot control how often replacement happens, and a failure taints the object. A `terraform_data` with a `local-exec` is a perfectly reasonable piece of build glue and a poor substitute for a provider resource that actually models what you are doing. ## Version note `terraform_data` requires Terraform 1.4 or newer. On older versions `null_resource` is the only option, which is worth stating out loud if the interview touches a codebase pinned by `required_version`.

  • What is the practical difference between triggers on null_resource and triggers_replace on terraform_data?
    `triggers` is a map of strings, so anything non-string has to be run through `jsonencode` or a hash function first. `triggers_replace` accepts any value — a list, a map, a resource ID — directly. Both have the same effect: a change to the stored value forces the resource to be replaced, which re-runs any attached provisioners.
  • Why does removing the hashicorp/null dependency matter in practice?
    Every provider is a binary that `terraform init` must resolve, download and record in `.terraform.lock.hcl`, with the platform-specific hashes that go with it. On restricted or air-gapped runners that is a mirror entry and a review burden for a plugin that creates nothing. `terraform_data` ships inside Terraform, so the dependency disappears entirely.
  • What does the output attribute of terraform_data give you that input does not?
    `output` holds the value `input` had at the last successful apply, so it carries last-applied semantics rather than current-configuration semantics. That lets you pin a value across applies and reference it from elsewhere as a normal dependency edge, which is useful when two things need ordering but share no natural attribute reference.

saying these in an interview costs you the question

  • Thinking terraform_data provisions real infrastructure
  • Assuming triggers_replace only accepts strings
  • Believing null_resource was removed from Terraform
  • Expecting terraform_data to work before Terraform 1.4

context