Terraform Plan JSON Viewer

Read a plan in JSON: what is destroyed, what is replaced and which attribute forces it. Read it here rather than pasting it anywhere else, because this document does not redact sensitive values.

Paste below, or drop a file anywhere on this panel

Or drop a file anywhere on this panel. Nothing is uploaded: the analysis runs in this tab.

The answer appears here

Paste on the left and press Read. Nothing leaves this tab.

Examples

Real input you can load into the tool above. Each one shows a different thing going wrong, because that is what the tool is for.

A forced replacement

replace_paths names the attribute the provider cannot change in place

{
  "resource_changes": [
    {
      "address": "aws_instance.web",
      "change": {
        "actions": ["delete", "create"],
        "replace_paths": [["instance_type"]]
      }
    }
  ]
}

A sensitive value in the JSON

The readable plan would print (sensitive value); this document carries it

{
  "resource_changes": [
    {
      "address": "aws_db_instance.main",
      "change": {
        "actions": ["update"],
        "after": { "password": "REPLACE_ME" },
        "after_sensitive": { "password": true }
      }
    }
  ]
}

A destroy

Not reversible by re-running apply, and prevent_destroy is the only guard

{
  "resource_changes": [
    {
      "address": "aws_db_instance.main",
      "change": { "actions": ["delete"] }
    }
  ]
}

Common mistakes

These are the ones that fail silently. The config is accepted, nothing raises an error, and the consequence arrives later.

  1. Archiving plan JSON as a build artifact

    The JSON is not redacted. Every sensitive value the plan touched is in it in plaintext, and build artifacts are usually readable by everyone with repository access.

    Instead:Archive the human-readable plan output, which is redacted, or nothing at all. Treat any plan JSON already stored as leaked and rotate what it contained.

  2. Reading a replacement as an update

    delete and create together is a replacement. The object is destroyed and a new one created with a new id, so dependents change and any data on it goes.

    Instead:Check replace_paths for the attribute forcing it, and decide whether that change is worth the replacement.

  3. Ignoring the resource_drift section

    Drift is reported separately from planned changes, so a plan can show no changes and still be about to overwrite something changed outside Terraform.

    Instead:Read drift as well as changes. Bring deliberate console changes into the configuration, and use ignore_changes where another controller owns the field.

The readable plan hides sensitive values and the JSON does not

This catches people out because the human-readable plan is so careful about it. The JSON carries the real value and records the fact that it was meant to be hidden as a separate boolean alongside.

A plan JSON is a credential

Where the terminal prints (sensitive value), the JSON has the value under before or after and a matching entry under before_sensitive or after_sensitive saying it is sensitive. So a plan JSON attached to a ticket, pasted into a chat, or archived as a CI artifact leaks everything the plan touched. Archiving plan JSON for auditing is a common and expensive habit.

replace_paths names the attribute forcing a replacement

This is the question people actually have. A replacement shows as delete and create together, and replace_paths lists the attributes the provider cannot change in place. When it is absent, the replacement came from outside the diff: a -replace flag, or a tainted instance in state.

A replacement is a destroy, whatever the ordering says

create,delete is create-before-destroy and delete,create is the other way round. Either way the object goes and a new one arrives with a new id, so anything referring to that id changes too, and any data on it is gone unless it was on a separate volume.

resource_drift is a separate section people miss

It records what refresh found different from state, and it is not part of the planned changes. A plan can say no changes and still list drift, which is then quietly corrected on the next apply. If a controller owns a field, ignore_changes stops it appearing forever.

after_unknown is why a plan can be wrong about what happens next

Anything listed there is decided by the provider at apply time. Where such a value feeds another resource's argument, that argument cannot be planned either, which is how a plan promising an in-place update ends up replacing something downstream.

What this cannot see

Whether the plan is correct, or what the values are: it reports paths and actions only, and attribute values are deliberately never shown. Everything runs in your browser and nothing is uploaded.