TerraformXIV · Modules: Reusable Building BlocksProduction Terraform
Module Releases and Upgrades
What you'll learn
- Apply semantic versioning to a module's releases
- Write a CHANGELOG that documents breaking changes
- Roll back a module release without breaking consumers
- Publish a module to the Terraform Registry with the right metadata
Prerequisites
None — start here.
Verified against Terraform CLI 1.9.x · OpenTofu 1.7.x · HCL 2.0 · bpg/proxmox provider 0.66+ · hashicorp/local provider 2.5+ · hashicorp/null provider 3.2+ · hashicorp/random provider 3.6+ · hashicorp/http provider 3.4+ · Ubuntu 24.04 LTS · Debian 12 (Bookworm) · 2026-08-13
A module without a release process is a module with unannounced changes. The consumer wakes up to a plan that proposes to recreate the database. The consumer does not know why. The consumer does not know who changed what. The release process is the discipline that prevents this.
The release process is four things: a versioning scheme (semver), a changelog, a breaking-change discipline, and a rollback procedure. The lesson teaches each.
Semantic versioning
Modules use semantic versioning. The version is
MAJOR.MINOR.PATCH. Each part has a meaning:
- MAJOR — incompatible changes. Removing a variable, renaming an output, changing the type of an output, changing a default that the consumer depends on.
- MINOR — backwards-compatible features. Adding a variable, adding an output, adding a new resource.
- PATCH — backwards-compatible fixes. Bug fixes, documentation, internal refactors.
The discipline is to bump the right part. The wrong bump
is a contract violation. The consumer’s version = ">= 3.0.0, < 4.0.0" constraint expects a major bump
for breaking changes. A minor bump that introduces a
breaking change surprises the consumer.
| Change | Bump | Reason |
|---|---|---|
Renamed variable vpc_name to name | MAJOR | Breaking |
Removed output flow_log_group_arn | MAJOR | Breaking |
Added optional variable tags | MINOR | Non-breaking |
Added new optional output vpc_arn | MINOR | Non-breaking |
| Fixed a tag in the resource metadata | PATCH | Non-breaking |
| Updated documentation | PATCH | Non-breaking |
The discipline is to ask before every release: “Does this change the behaviour a consumer depends on?” If yes, the answer is MAJOR. If no, the answer is MINOR or PATCH.
The CHANGELOG
The CHANGELOG is the human-readable record of what changed. The format is the Keep a Changelog convention:
# Changelog
All notable changes to this module will be documented
in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/),
and this project adheres to [Semantic Versioning](https://semver.org/).
## [4.2.0] - 2026-08-12
### Added
- Optional `tags` variable for additional resource tags.
- Output `vpc_arn` for the VPC ARN.
### Changed
- Default `enable_flow_logs` is now `true` to match the
org-wide audit requirement.
## [4.1.0] - 2026-07-08
### Added
- Optional `database_subnets` variable for custom DB
subnet layout.
## [4.0.0] - 2026-06-01
### Changed
- **BREAKING**: Renamed `vpc_name` to `name`. The
variable is now an unqualified string.
- **BREAKING**: Removed output `flow_log_group_arn`.
Use the module's `monitoring` outputs instead.
### Migration
- Rename `vpc_name = "production"` to `name = "production"`
- Remove references to `flow_log_group_arn` in your
configuration.
The CHANGELOG entry includes a Migration section for breaking changes. The migration tells the consumer what to change. The migration is the upgrade guide.
The CHANGELOG is committed to the repository. The CHANGELOG is regenerated on every release. The CI pipeline enforces the entry.
The release process
The discipline of a release process:
- Branch from
main. The branch isrelease/v4.2.0. The branch contains the CHANGELOG entry and the version bump. - Open a pull request. The PR is reviewed by two engineers. The PR description is the change summary.
- Tag the merge. The merge commit is tagged
v4.2.0. The tag is immutable. The tag is the released version. - Publish the release. The CI pipeline publishes the release to the Registry (or the internal equivalent). The release is now visible to consumers.
- Announce the release. The release is announced in the team’s chat channel. The consumer knows the new version is available.
The discipline is that every change goes through this process. The discipline is that the tag is created by the CI pipeline, not by a human. The discipline is that the tag is signed (GPG or SSH signing).
# Severity: CONFIGURATION
git tag -s v4.2.0 -m "Release v4.2.0"
git push origin v4.2.0
The -s flag signs the tag with GPG. The signature is
the audit trail. The consumer can verify the tag’s
authenticity.
Breaking-change discipline
A breaking change is a change that requires the consumer to modify their configuration. The discipline is to minimise breaking changes. The discipline is to communicate breaking changes clearly.
For a major release:
- Deprecate before removal. A variable that will
be removed in
v5.0.0is deprecated inv4.0.0. The deprecation message is in the variable’sdescription. - Document the migration. The CHANGELOG entry includes a Migration section. The migration tells the consumer what to change.
- Provide a grace period. The major release is not the first release to break the consumer. The deprecation is in the previous minor release. The consumer has at least one minor release to migrate.
- Test the migration. The upgrade is tested in a pilot stack. The migration is confirmed. The release is approved.
# modules/network/variables.tf
variable "vpc_name" {
type = string
description = "DEPRECATED: use `name` instead. Will be removed in v5.0.0."
validation {
condition = var.name != null
error_message = "Set `name` instead of `vpc_name`."
}
}
The deprecation is in the description. The validation forces the consumer to set the new variable. The consumer migrates. The deprecation is removed in the next major.
Publishing to the Terraform Registry
The public Registry requires the module to be published from a public Git repository. The repository must follow the naming convention:
terraform-<PROVIDER>-<NAME>
For example, terraform-aws-network is published as
terraform-aws-modules/network/aws. The terraform-aws-
prefix is required for the public Registry.
The Registry requires:
README.mdwith the inputs, outputs, and examples in the standard format.LICENSEwith an approved licence (Apache 2.0, MPL 2.0, BSD-3-Clause).examples/directory with at least one working example.- Tagged releases with semver tags (
v1.0.0,v1.1.0, etc.). versions.tfwithrequired_versionandrequired_providers.
The publish is triggered by pushing a tag. The Registry
detects the tag, builds the module, and publishes the
release. The release is visible at
registry.terraform.io/<namespace>/<name>/<provider>.
For the internal Registry, the publish is triggered by the CI pipeline. The internal Registry uses the same metadata requirements. The internal Registry stores the module in the organisation’s infrastructure.
The rollback procedure
A release is broken. The consumer’s plan shows unexpected replacements. The team has to roll back.
The rollback procedure:
- Identify the bad release. The
git logshows the tag. The CI pipeline logs show the test results. - Identify the previous good release. The previous semver tag is the rollback target. The previous tag is a known-good release.
- Do not delete the bad tag. The tag is immutable. The tag is the audit trail. The fix is to publish a new release that reverts the bad change.
- Publish a new release. The new release is
v4.2.1that reverts the breaking change. The new release is a PATCH or MINOR bump, depending on the change. - Communicate the rollback. The consumer is informed that the bad release is not safe. The consumer is told to upgrade to the rollback release.
# Severity: CONFIGURATION
# BAD_COMMIT is the commit that shipped v4.2.0.
BAD_COMMIT=REPLACE_WITH_COMMIT_SHA
git revert "$BAD_COMMIT"
git tag -s v4.2.1 -m "Release v4.2.1 (reverts v4.2.0)"
git push origin v4.2.1
The git revert creates a new commit that undoes the bad
change. The new commit is tagged. The new tag is
published. The consumer’s version = ">= 4.0.0, < 5.0.0" constraint accepts the new tag. The consumer can
upgrade.
The tag is never deleted. The tag is the record of the bad release. The tag is the audit trail.
The upgrade procedure for consumers
The consumer’s upgrade procedure:
- Read the CHANGELOG. The CHANGELOG entry describes the change. The CHANGELOG’s Migration section describes the upgrade.
- Read the diff. The
git diffbetween the old and new tags shows the source change. The diff is the contract change. - Plan in a pilot. The pilot stack is the first upgrade. The plan is reviewed. The apply is approved.
- Migrate the rest. The remaining stacks migrate one at a time. The migration is tracked in a spreadsheet or a project board.
The discipline is to not batch the upgrade across all stacks. The discipline is to upgrade one stack at a time. The discipline is to have a rollback plan for each upgrade.
Inspection commands
The reader validates a release:
# Severity: READ-ONLY
git tag --list "v*" | sort -V
v4.0.0
v4.1.0
v4.1.1
v4.2.0
v4.2.1
The tags are listed in semver order. The history is clear.
# Severity: READ-ONLY
git log --oneline v4.1.0..v4.2.0
a1b2c3d Add optional tags variable
b2c3d4e Default enable_flow_logs to true
c3d4e5f Add vpc_arn output
The commits are listed. The changes are documented.
# Severity: READ-ONLY
git tag -v v4.2.0
object 1234567890...
type commit
tag v4.2.0
tagger Ed Brandi <ed@example.com> 1691846400 +0000
Release v4.2.0
gpg: Signature made ...
gpg: Good signature from "Ed Brandi <ed@example.com>"
The tag is signed. The signature is verified. The tag is authentic.
Production failure modes
-
Patch bump for a breaking change. The author bumps
v4.1.0tov4.1.1for a breaking change. The consumer’sversion = ">= 4.1.0, < 5.0.0"constraint accepts the upgrade. The consumer’s plan shows unexpected replacements. The fix is to revert the release and to bump the major version. -
No CHANGELOG. The release is published without a CHANGELOG entry. The consumer does not know what changed. The fix is to require a CHANGELOG entry in the CI pipeline.
-
Unsigned tags. The tag is created without signing. The signature is not verifiable. The audit trail is incomplete. The fix is to require signed tags in the CI pipeline.
-
Delete the bad tag. The team deletes the bad tag to “hide” the mistake. The tag is part of the Git history. The fix is to publish a new release that reverts the bad change.
-
Batch upgrade across all stacks. The consumer upgrades all stacks at once. One stack fails. The rollback is global. The fix is to upgrade one stack at a time.
-
No pilot stack. The consumer upgrades without a pilot. The first sign of trouble is in production. The fix is to always have a pilot stack.
Security implications
- The signed tag is the audit trail. A consumer who needs to verify the source can verify the tag’s signature.
- The CHANGELOG is the change history. The consumer uses the CHANGELOG to plan the upgrade.
- The release process is the contract. The process prevents the author from publishing an unsigned, untested release.
- The Registry’s visibility controls determine who can see the module. The public Registry is public. The internal Registry is private. The module’s sensitivity determines the Registry.
Performance implications
- The release is a Git operation. The performance is negligible.
- The Registry publication is a build that takes seconds. The CI pipeline runs the build.
- The consumer’s
terraform initis a network round trip. The cost is paid by the consumer.
What comes next
The release process completes the module lifecycle. The next module covers the operational discipline of running Terraform at scale: the state, the workspaces, and the CI/CD pipeline.
Verification
- Every release is tagged with a semver tag.
- Every tag is signed (GPG or SSH).
- The CHANGELOG is updated on every release.
- The CHANGELOG’s breaking changes have a Migration section.
- The
tests/directory is run on every release. - The release is rolled back by publishing a new release, not by deleting the bad tag.
Knowledge check · 6 questions
Q1. A module author renames a variable. Which version bump is correct?
Q2. What is the right way to roll back a broken module release?
Q3. A module release with a CHANGELOG entry is sufficient for the consumer to plan the upgrade.
Q4. A module adds a new optional variable. Which version bump is correct?
Q5. Which of the following are required to publish a module to the public Terraform Registry? (Select all that apply.)
Q6. A team releases v4.2.0 of a module. The consumer's plan shows unexpected replacements. The team wants to roll back. What is the first step?
Passing score: 75%. Answers are checked in this browser.