Infrastructure-as-code changelogs: Terraform modules, Helm charts, and changes that touch running systems
Most release notes are read by someone deciding whether to update an app. Infrastructure-as-code
release notes are read by someone deciding whether to run terraform apply against
production. The difference matters more than any other fact about this genre. When a library
upgrade goes wrong, an app crashes and you roll back. When a Terraform module or Helm chart
upgrade goes wrong, the tool does exactly what it was told — and what it was told might be
“destroy this database and create a new, empty one with a different name.” Like the
CLI-and-library reader, your reader upgrades on
purpose, often in bulk, often prompted by a Renovate PR. Unlike that reader, their worst case
isn’t a failed build. It’s a plan that applies cleanly and takes production with it. Write for
that reader, and every rule in this guide follows.
“Breaking” is measured in blast radius, not interface shape
Semver instinct says: renamed an input, that’s breaking; changed an internal default, that’s a patch. For IaC that instinct is exactly backwards, because the contract isn’t just the variables file — it’s the infrastructure the module produces. Classify changes by what happens to running systems when a pinned consumer bumps the version:
- Input renames and removals (Terraform variables, Helm values) are the loud, cheap kind of breaking: the plan errors, the chart fails to render, nobody gets hurt. Still breaking — still a major — but the failure announces itself.
- Changed defaults that alter resources are the dangerous kind. No interface changed, every existing configuration still validates, and the plan quietly wants to modify — or replace — things. A default instance size, a turned-on encryption setting that forces replacement, a changed deletion policy. These are majors wearing a patch costume. If the same inputs now produce different infrastructure, that’s a breaking change no matter how small the diff.
- Resource addressing changes — renaming resources, converting
counttofor_each, restructuring internal modules — are the catastrophic class. Without shippedmovedblocks, Terraform sees “old address gone, new address appeared” and plans a destroy-and-create. For a stateless security group that’s a blip; for a database or a bucket it’s data loss with a green checkmark. - Constraint bumps — required provider versions, minimum Terraform core, minimum Kubernetes version, a chart’s API-version floor — are breaking for every consumer who can’t move yet, the same way dropping a runtime is. Say the new floor in the entry, not just in the diff.
- Helm’s special cases: changes to immutable fields (a Deployment’s selector,
a StatefulSet’s
volumeClaimTemplates) makehelm upgradefail or force recreation; values-schema restructures break every consumer’s pinned values file; and CRD changes don’t apply at all — more on that below.
Version by blast radius, write by blast radius. The first line of the entry should carry the worst thing in it, per the breaking-changes playbook: “Breaking: upgrading replaces the Redis instance” beats three paragraphs of context.
The expected-diff line: one sentence that prevents 3 a.m. surprises
The single most valuable sentence an IaC changelog can contain is a statement of what the plan should look like after upgrading:
“After bumping to v4.0.0 and running the moved blocks, terraform plan should
show 2 to change, 0 to destroy. Any planned destroy means stop — you’ve hit the migration notes
below.”
This line converts your release notes from prose into a checkable contract. The operator
doesn’t have to interpret your intentions; they compare your sentence against their terminal.
Plans that match get applied with confidence. Plans that don’t get investigated instead of
YOLO-approved. The Helm equivalent describes the helm diff upgrade output shape:
which objects change, whether anything gets recreated, whether pods restart. If you can’t
write the expected-diff line because the answer is “it depends on your inputs,” say which
inputs it depends on — that sentence is almost as protective.
Migration homework is yours, not the reader’s
Every consumer of your module repeats whatever migration work you didn’t do, once per root that pins it. The multiplication argument from schema-change notes applies with interest:
- Ship
movedblocks inside the module when you rename or restructure resources — Terraform has supported refactoring declarations for exactly this. If a move can’t be expressed that way, print the exactterraform state mvcommands, in order, copy-pasteable. - Map old values to new values when a Helm values schema changes — a two-column table beats a paragraph. Every renamed key without a mapping line becomes one support thread per consumer.
- State the order when order matters: apply CRDs first, then upgrade; bump the provider before the module; run the state moves before the plan.
- Estimate honestly. “Most consumers: bump and re-plan, ~5 minutes. If you
set
create_database = true: read the replacement notes, budget an hour” — an honest estimate is a trust signal, and it triages your readers for you.
The CRD gap: your changelog is the upgrade mechanism
Helm installs CRDs on first install and then
never touches
them again — helm upgrade skips them by design. Which means for any chart
that ships CRDs, there is no automated path from “new CRD version exists” to “cluster has it.”
The release notes are not a courtesy here; they are the delivery mechanism. Every entry for a
CRD-shipping chart should carry a CRD line even when it’s boring: “CRD changes: none.” When
it’s not boring, it’s the top of the entry: which CRDs changed, the kubectl apply
command to update them, and what breaks if you skip it (usually: new fields silently ignored,
or webhook failures that look like cluster gremlins). An operator who has been bitten once will
scan every future entry for that line — reward the habit.
Where the notes must surface (machines are the couriers)
IaC upgrades arrive as bump PRs: Renovate and Dependabot embed your release notes directly in the pull request, where a platform engineer spends about thirty seconds deciding to merge. That embedded text is your notes’ most-read venue, so the verdict belongs in the first line — “Safe to bump: no plan changes expected” or “Breaking: see migration notes.” Beyond the PR embed:
- Registries render what you publish. The Terraform Registry shows your
GitHub release notes; Artifact Hub reads a structured
changes annotation from
Chart.yaml— a genuinely machine-readable changelog field. Fill them; an empty release body on a module touching production reads as negligence. - CHANGELOG.md travels in the repo and survives registry outages and platform moves.
- A canonical hosted page with permalinks earns its keep in incident reviews: “the replacement was announced here, with migration steps” is a sentence you want a link for. Permalinks also give runbooks something stable to cite.
A release entry that respects the plan
## v4.0.0 — 2026-07-29 Breaking: subnet resources are now indexed by availability zone (count → for_each). Moved blocks are included — state moves run automatically on plan. Expected diff after upgrade: 3 to change, 0 to destroy. A planned destroy of aws_db_instance means you set `database_identifier` — see migration notes before applying. - New: `enable_flow_logs` (default false; enabling adds 2 resources) - Changed: default `instance_class` t3.medium → t3.large (in-place modify, brief performance blip, no replacement) - Floors: Terraform ≥ 1.6, AWS provider ≥ 5.30 Migration notes: <permalink> Estimated effort: bump + re-plan ~5 min; flow-logs adopters ~20 min.
Skimmable verdict first, expected diff second, mechanics after — the template discipline, tuned for a reader with a terminal open.
Anti-patterns
- The Renovate wall. Your own changelog is forty “chore(deps): bump…” entries. Mechanical bumps that don’t change blast radius live in the diff, not the notes — curate like generated changelogs demand.
- The breaking default as a patch. “Just a default change” that replaces resources is the most common way IaC modules burn trust. Same inputs, different infrastructure: major.
- The surprise destroy. Renames shipped without moved blocks, state-move commands, or a warning. The operator finds out from the plan — or worse, from the apply.
- “See the diff.” A commit link is not release notes for code that manages production state. The diff shows what changed in HCL; the notes must say what changes in the world.
- The unmapped rename. Values restructured, no old→new table. Every consumer independently reverse-engineers your refactor.
- Notes only in the tag. Consumers live on the registry, Artifact Hub, and bump PRs. A tag message none of those surfaces render is a changelog nobody sees.
Running this on Wakelog
Wakelog fits the module-maintainer loop without pretending to be a plan parser. Post the
entry from the same CI job that tags the release — the
one-curl pattern works from any pipeline — and
tag it breaking or security (see
how to publish a security fix) so the
public page and feeds carry the flag structurally. Permalinks give runbooks and incident docs
a stable citation; RSS and JSON feeds let platform teams watch the internal modules they
depend on, and unlisted projects keep an internal platform team’s stream off the public
directory. The honest caveat: nothing here reads your plan output. The expected-diff line —
the most valuable sentence in the entry — is a writing job, and it stays one.
Give your modules a changelog operators trust Next: schema changes and migrations →
Related guides
- Data and schema changelogs: announcing changes to tables other people query
Every column name is an API. Why data breaks silently at query time instead of loudly at deploy time, what counts as breaking (grain, semantics, enums), backfill restatement notes, and a template. - Platform engineering changelogs: release notes for your internal developer platform
A platform team ships a real product to the strangest user base in software: engineers who never chose it and can’t leave. Every release lands inside someone else’s repo, breaks builds on commits that touched nothing, and gets enforced by deadline. What a paved-road changelog announces, why entries should be findable by error message, and the template that keeps internal customers on the road. - Container image release notes: tags, digests, and CVE rebuilds
People deploy your image into their infrastructure sight unseen — often automatically. Tags are mutable pointers, a CVE rebuild changes the artifact without touching your code, and no registry gives you a changelog surface. What to announce, where to put it, and what breaks consumers that Dockerfile diffs never show.
Last updated 2026-07-29 · All guides