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.

More terraform tools

Terraform Validator & Formatter Does it parse, and is it formatted? Terraform dynamic Block Generator It is ingress.value, not each.value Terraform check and precondition Generator A failed check does not stop the apply Terraform variables.tf Generator From the var. references you already wrote Terraform Unused Declaration Linter Dead variables, locals and data sources Terraform Atlantis YAML Generator when_modified misses your modules Terraform Function Tester and HCL Console terraform console, in a tab Terraform for Expression Tester Where do the keys collide? Terraform Interpolation Tester null becomes an empty string Terraform templatefile Tester Render it before you apply it Terraform Resource Count Calculator How many objects, not how many blocks Terraform count to for_each Converter With the moved blocks it needs Terraform moved Block Generator Rename without destroying Terraform state mv Command Builder No plan, no undo Terraform target Flag Builder And why to stop using it Terraform outputs.tf Generator Outputs are not sensitive by default Terraform Remote State Data Source Generator It grants read of the whole state Terraform Provider Block Generator docker is not hashicorp/docker Terraform .gitignore Generator and Checker .terraform* eats the lock file Terraform to OpenTofu Migration Checker The state is the part to watch Terraform tfvars to JSON Converter The JSON file wins Terraform Version Constraint Parser What ~> actually allows Terraform Resource Address Parser Quote it before the shell eats it Terraform cidrsubnet Calculator newbits is added, not the target Terraform Import Block Generator The id is not the name Terraform Lock File Viewer Why CI fails and your laptop does not Terraform Variables Checker A missing value hangs CI Terraform tfvars to Env Vars Converter Lists and maps have to be JSON Terraform Deprecated Syntax Checker What a current Terraform rejects Terraform State File Viewer Read it without uploading it Terraform HCL to JSON Converter And what the conversion loses Terraform Module Source Validator version only works for the registry Terraform Backend Config Decoder Is state actually locked? Terraform Naming Convention Validator A hyphen makes a resource unreferenceable

Elsewhere on the site