Skip to content

Terraform Plan Summarizer · IaC

Terraform Plan Summarizer — read a plan before you apply it

Paste terraform plan output — ANSI colour and CRLF from a CI log included — or terraform show -json tfplan. Adds, changes, destroys and replacements are counted as four separate figures, every forces replacement attribute is named on the resource it belongs to, and anything destructive that touches a database, a NAT gateway or a cluster control plane is pulled to the top. Then the totals are checked against Terraform’s own summary line — because a plan reader that quietly drifts is worse than no plan reader at all.

Runs in your browser — nothing you paste leaves this page. How we prove that

Runs in your browser Counts cross-checked No upload, no signup Updated Aug 3, 2026

Terraform plan summarizer playground

Examples

Both formats work and the format is detected for you: the human-readable terraform plan transcript (ANSI colour codes and CRLF from a CI log are fine) or terraform show -json tfplan. Up to 2 MiB — roughly a 50,000-line plan. It stays in this tab: there is no server here to send it to, which matters, because plan output is full of account IDs, ARNs, CIDRs and policy JSON.

Results update as you type — press Enter to run now.

Grouping

By action puts replacements and destroys first, because those are the ones that can take something down. By module follows the code instead, which is what you want when one module call is the thing under review.

Result

Paste a plan to see adds, changes, destroys and replacements counted separately, the replacement-forcing attributes named, and the totals cross-checked against Terraform’s own summary line.

The Gap

The one -/+ that matters is on line 310.

A real plan is thousands of lines of attribute diff, and the part that can take production down is three characters wide. Reviewing one in a pull request means scrolling a folded CI log looking for a -/+ among hundreds of ~ tag updates, then scrolling further to find the # forces replacement comment that says which attribute caused it. The summary at the bottom does not help: 2 to add reads like two new things, when one of them is your database being destroyed and rebuilt.

So people paste the plan into a chatbot. That is the worst available option, twice over. First, exfiltration: plan output carries account IDs, ARNs, subnet and CIDR ranges, IAM policy documents, security-group rules and internal DNS names — a shape-of-the-network document you would never attach to a ticket, handed to a third party and, on consumer tiers, into training data. Second, accuracy: a language model reading a plan is pattern-matching text. It attributes a forces replacement comment to the wrong resource, silently drops resources when the paste is truncated, and will confidently total up numbers it never actually counted.

This page is the ground-truth version of that same question, and it is deliberate about the one thing an AI answer cannot give you: a statement of how much it does not know. Terraform’s human-readable output is not a stable interface, so the parsed per-resource list is summed and compared against Terraform’s own Plan: line every time. When they agree, it says so. When they do not — which is what a truncated CI paste looks like — it names both figures and tells you to trust Terraform’s. The sixth example chip is exactly that case: a GitHub Actions log whose middle the viewer folded away, where Terraform’s line reports 4 to add, 3 to change and 2 to destroy while only 2, 1 and 1 survived the copy.

Reviewing the pipeline that runs the plan rather than the plan itself? The GitHub Actions Validator and GitLab CI Validator check the workflow, and the .env.example Checker catches the variables it forgot to pass.

The Pipeline

How it works.

Five deterministic steps, all inside your browser tab — two parsers behind one auto-detecting entry point, and a cross-check that turns an unstable input format into an honest answer.

  1. Detect the format, and say so when it is the wrong file.

    JSON or text is decided from the first character and the keys present. A state dump, a `terraform validate -json` result, or JSON truncated mid-array each get named for what they are, with the command that produces a real plan — never a bare "invalid input".

  2. Strip ANSI, normalise line endings.

    A plan copied out of GitHub Actions, GitLab or Jenkins arrives wrapped in colour escapes and CRLF endings. Both are removed before anything is parsed, so a CI paste and a terminal paste produce identical output. Input is capped at 2,097,152 characters — 2 MiB, roughly a 50,000-line plan.

  3. Read the resources, not the diff.

    Each `# <address> …` header line carries the module path and the index, so that is the source of truth for the address; the symbol line underneath contributes the replacement order. Inside each block only three things are looked for: `# forces replacement`, `(sensitive value)`, and the `(because …)` reason Terraform gives, which is kept verbatim.

  4. Rank by what breaks, not by what changed.

    A table of 17 resource-type patterns in four classes — data store, egress path, control plane, encryption key — flags destructive actions only, with a sentence naming what specifically goes: the data, the public egress IP, the kubeconfigs. A type that is not in the table is unclassified, and the page says so rather than implying it is safe.

  5. Cross-check the total, and refuse to be confidently wrong.

    The parsed list is summed Terraform's way — each replacement counting once as an add and once as a destroy — and compared with the `Plan:` line. Agreement is stated. Disagreement is a warning that names both numbers and says to trust Terraform. Nothing here silently guesses.

Reference

Every symbol a plan can print.

Nine glyphs and annotations carry the whole meaning of a plan. Six are actions, three are notes on a value — and two of the six differ only in the order of two operations, which is the difference between a rolling change and an outage.

Symbol What it means
+ create A new object. Nothing exists yet, so most of its attributes read (known after apply).
~ update in-place The existing object is modified. The id survives, and so does anything stored on it.
- destroy The object is deleted and not recreated. Look for the (because …) line: it names why.
-/+ destroy then create A replacement, old object first. There is a window with nothing there — the default, and the one to read carefully.
+/- create then destroy A replacement with create_before_destroy = true. The new object exists before the old one goes.
<= read during apply A data source that cannot be resolved at plan time. Never part of add, change or destroy.
# forces replacement attribute annotation This attribute cannot change in place, so the whole object is replaced. The one comment worth grepping for.
(known after apply) value annotation The provider assigns it on create, so it genuinely does not exist yet. Not an error.
(sensitive value) value annotation Marked sensitive, so the plan hides it. Note that it is still in the state file in cleartext.

Why a replacement is counted twice

Terraform counts operations. This page counts resources, and prints Terraform’s figures alongside so neither can be mistaken for the other.

replace-math.txt
Plan: 2 to add, 0 to change, 1 to destroy.

  what Terraform counted            what actually happens
  ────────────────────────          ─────────────────────
  + aws_iam_role.app                one NEW role
  -/+ aws_db_instance.primary       one database replaced
        counted as +1 add
        counted as +1 destroy

  this page shows them apart:
    + add      1   created outright
    ~ change   0   updated in place
    − destroy  0   destroyed outright
    ± replace  1   destroyed and recreated

Getting the right file

show -json against a saved plan is the dependable input; the text transcript is the one you already have. Two near-misses are recognised and named.

get-a-plan.sh
# The reliable path: save the plan, then read the machine format
terraform plan -out=tfplan
terraform show -json tfplan > plan.json     # paste plan.json above

# The path you already have: the text a pipeline printed
terraform plan -no-color                    # -no-color if you can; ANSI is fine either way

# What NOT to paste — both are told apart from a plan and named
terraform show -json                        # this is STATE, it has no resource_changes
terraform validate -json                    # this is a config check, not a plan

Next Step

The plan is fine. Now check the pipeline that runs it.

A plan is only as trustworthy as the workflow that produced it: the GitHub Actions Validator and GitLab CI Validator catch the pipeline mistakes that make a plan run against the wrong state or the wrong credentials, and the .env.example Checker finds the variable your job never passed. All of them, like this page, run entirely in your browser.

plan-summary.txt
+ add 1   ~ change 1   − destroy 0   ± replace 1

read this first — 1 high blast radius
  module.data.aws_db_instance.primary      data store
    forces replacement: engine_version
    destroy then create — the database does not exist in between

cross-check: reconciles
  Terraform printed: Plan: 2 to add, 1 to change, 1 to destroy.
  (the replacement is one of those adds AND the destroy)

FAQ

Questions, answered.

Tap a question to expand the answer.

Both, and it detects which one you pasted. The first is the human-readable transcript Terraform prints — the one with +, ~, - and -/+ down the left margin — including the ANSI colour codes and CRLF line endings you get from copying a CI log. The second is terraform show -json tfplan, the machine format, which carries resource_changes, replace_paths, action_reason and output_changes and is a documented, versioned interface. The JSON is more reliable and is what to use if you can save a plan file; the text is what you actually have once a pipeline has already run. What it does NOT accept is terraform show -json of your state, or terraform validate -json output — both are common mix-ups, and both get told what they are, plus the command that produces the right thing instead.

The order of the two operations, and it decides whether you get an outage. -/+ means destroy then create: the old object is gone before the new one exists, so anything depending on it is broken for the length of that gap — seconds for a security group rule, twenty minutes for an RDS instance. +/- means create then destroy, which is what lifecycle { create_before_destroy = true } buys you: the replacement is built and only then is the old object removed. Terraform picks destroy-first by default, so +/- only appears where somebody asked for it. This page spells both orders out in words on every replacement row rather than making you decode a three-character glyph.

It marks the specific attribute whose change cannot be applied in place, so the provider has to delete the object and build a new one. It is per-attribute, and that is the useful part: engine_version on an aws_db_instance is a real upgrade you meant to do, while availability_zone or name changing by accident — from a renamed variable, a reformatted tag, a provider default that moved — is the classic surprise replacement. This page lifts every one of those attribute names out of the block it belongs to and shows them as chips on that resource row, so you never have to scroll a 400-line diff hunting for the comment.

Because Terraform counts operations, not resources, and a replacement is two operations. So a plan with one plain create and one replacement prints "Plan: 2 to add, 0 to change, 1 to destroy" — the replacement contributes to both figures. That is correct, and it is also the single most misread line in Terraform output, because "2 to add" sounds like two new things. This page shows four disjoint tiles instead: add is created outright, replace is destroyed and recreated, and nothing is ever counted twice. Terraform own accounting is then printed underneath, verbatim, so the two can be compared rather than confused.

Yes, and that is the reason this page exists in this form. There is no server behind it: the parser is JavaScript that loads into your tab and runs there, so the plan never leaves your machine — you can confirm it in your browser network tab, or by loading the page and then going offline. That matters more for plan output than for almost anything else you would paste into a web tool: a plan is full of account IDs, ARNs, subnet and CIDR ranges, IAM policy documents, security-group rules and internal DNS names. Pasting one into a chatbot hands all of that to a third party and, on the consumer tiers of most assistants, into training data.

It means the resources this page could read do not sum to the totals on Terraform own summary line, and nine times out of ten the paste is incomplete: a CI log viewer folded the middle away, a scrollback buffer cut it, or the copy started halfway down. The warning names both sets of numbers and tells you to trust Terraform. The reason it exists at all is that Terraform human-readable output is explicitly not a stable interface, so a text parser will eventually drift on a format change — and a summarizer that drifted silently would print a confident wrong total, which is worse than printing nothing. That cross-check is what makes parsing an unstable format defensible in the first place.

It means the value does not exist yet. Attributes like an ARN, an id, a generated DNS name or an IP address are assigned by the provider when the object is created, so at plan time Terraform genuinely does not know them and refuses to guess. It is not an error and not a warning — a plan for anything new is full of them. It does have one practical consequence: a resource whose input depends on an unknown value cannot be fully evaluated either, which is why you sometimes see a data source marked "read during apply" instead of resolved, and why an apply can turn out to do more than the plan showed.

Yes. OpenTofu forked from Terraform 1.5.x and kept both output formats: the text transcript uses the same symbols and the same "Plan: N to add, N to change, N to destroy" line, and tofu show -json emits the same format_version family with the same resource_changes shape. Everything on this page applies unchanged, and the version banner is read as whichever product printed it. The same holds for the wrappers that pass Terraform output straight through — Terragrunt, Atlantis, Spacelift — as long as the plan text itself is intact.

The JSON path is written against format_version 0.1 through 1.x, which spans Terraform 0.12 to 1.9 and current OpenTofu; a document with an unknown major version is accepted but carries a warning, because a renamed field would otherwise silently produce zeros. The text path is tested against the 1.5-and-later shapes, which is where "N to import" joined the summary line and where moved and import blocks appear; older output mostly parses, and anything that does not is caught by the count cross-check rather than reported as fact. The one thing this page will never do is estimate cost — prices go stale, and a stale figure printed with confidence is exactly the class of answer this site exists to replace.

More free, private DevOps tools.

The Terraform Plan Summarizer is one tool in OpsCanopy — a growing canopy of browser-based validators, converters and testers that never touch a server.

39 free tools, every one offline-capable — opscanopy.com works with no signup and nothing uploaded.

Related infrastructure tools: the docker run → Compose Converter, the .env.example Checker for the variables a deploy forgot, and the GitHub Actions and GitLab CI validators for the pipeline that runs your plan — or browse the full tools directory.

Provided as-is for convenience. Not affiliated with HashiCorp. This page summarises what a plan says; it does not and will not estimate cost — pricing goes stale, and a stale figure printed with confidence is the one answer a ground-truth tool cannot give. A resource type absent from the blast-radius table is unclassified, not proven safe. OpsCanopy is free and open.