Share a Terraform plan
A plan is the single most consequential artifact in an infrastructure change, and it is shared worse than anything else in the pipeline.
The default is a pull request comment. Which means:
- It gets truncated. GitHub caps a comment at 65,536 characters. A
plan touching thirty resources clears that, so the bot collapses it
behind
<details>and cuts the middle out — often the part with thedestroyin it. - It's unreadable. Hundreds of lines of
~ tags = { ... }noise wrapped in a monospace block, with the three lines that matter buried somewhere in the middle. - You can't point at anything. "The RDS one, further down" is not a
code review. Line-anchored comments work on the diff of your
.tffiles, not on the plan the diff produces. - It's gone next week. The plan that was approved lives in a comment thread on a merged PR, if it lives anywhere.
And the people who most need to read it — a security reviewer, the DBA,
whoever owns the account you're about to modify — often can't run
terraform plan themselves. They have no state access and no
credentials, which is the correct security posture and a terrible
review workflow.
The pipeline
Plan, serialize, render, publish. The middle step is a script you own —
30 lines over resource_changes is enough:
terraform plan -out=tfplan
terraform show -json tfplan > plan.json
# plan.json → plan.html (group by action: create / update / replace / destroy)
python3 render_plan.py plan.json > plan.html
curl -fsS -X PATCH "https://commareports.com/api/v1/reports/$REPORT_ID" \
-H "Authorization: Bearer $COMMA_API_TOKEN" \
-H "Content-Type: application/json" \
-d "$(jq -n --rawfile html plan.html \
--arg title "prod-network — plan $(git rev-parse --short HEAD)" \
'{title: $title, html: $html}')"
A plan report that reviews well leads with the counts (4 to add, 2 to change, 1 to destroy), puts destroys and replacements first, and
keeps the per-resource attribute diffs collapsed underneath. Comma
renders report HTML verbatim with scripts enabled inside a sandboxed
iframe, so <details> blocks, a filter toggle, or a small
sort-by-action control all work — see interactive HTML
reports.
Then the PR comment becomes one line with a link, which is what a PR comment should have been all along.
One URL per stack
Create the report once per stack or workspace, store its id as a CI
variable, and PATCH it on every run. Each run appends a revision at the
same URL:
- The link in the change ticket never goes stale.
- Two revisions can be diffed — "this plan added a destroy that wasn't in the one we approved on Tuesday" is visible instead of remembered.
- Open comment threads carry across revisions, so an unresolved objection follows the plan into the next run rather than disappearing with it.
For a plan-per-PR workflow, create one report per PR instead and keep
the stack-level report for the mainline. Both patterns are one curl.
Comments land on the destroy
This is what a PR comment thread can't do. A reviewer opens the plan,
highlights the row for
aws_db_instance.analytics → destroy/create (engine_version), and pins a
thread to it: "this forces a replacement — we need the snapshot restore
runbook before this merges." The thread is attached to that resource,
survives the next plan, and is answerable in place. That's the anchored
comment model.
Two things follow from that:
- Approval has an artifact. The plan that was reviewed, the thread where the objection was raised, and the reply that resolved it are one URL with a revision history — the thing an auditor asks for six months later.
- An agent can close the loop. A Claude Code agent attached through
Comma's MCP server can read the threads with
list_comments, adjust the module, re-plan, andupdate_report— same URL, new revision, reply on the thread.
Handle it as sensitive, because it is
Terraform redacts attributes it knows are sensitive in show -json
output, but that is not the whole risk surface. Resource names, CIDR
blocks, account ids, ARNs, instance types and tag values are a decent map
of your infrastructure.
- Keep plan reports at private or team visibility. Never public. See the sharing model.
- Mint the CI token with
reports:writeonly and nothing else — scopes and instant revocation are covered in API tokens. - Filter in your renderer. If a module emits values you don't want
rendered at all, drop them in the script rather than relying on
Terraform's
sensitivemarking to cover you.
Try it
Comma is free — unlimited reports, unlimited commenters, unlimited revision history. Take the next plan your bot was going to truncate, publish it, and let the PR comment be a link.
Related
- Publish from CI — the one-curl pipeline pattern
- Share an interactive HTML report — what runs inside the sandbox
- Scoped tokens for AI agents — least-privilege publishing
- Comment on an HTML report — how anchored threads work