Tag each release of a shared Terraform module with a semantic version such as v1.4.0, bump the major number for any change that forces callers to edit their code or recreate resources, and let callers pin with a constraint. Semver only helps if everyone agrees what counts as breaking.
What Terraform expects
For the public registry, HashiCorp states that release tags must be semantic versions, with an optional v prefix such as v1.0.4 or 0.9.2, and that at least one valid version tag must exist before you publish. Tags that are not version numbers are ignored.
Callers then choose a constraint. HashiCorp documents these operators: an exact version, !=, >, >=, <, <=, and ~>, which lets only the rightmost component increment. For example, ~> 1.0.4 permits 1.0.5 and 1.0.10 but not 1.1.0.
HashiCorp also advises using version ranges for modules your own organization maintains, if it follows semantic versioning consistently. That condition is the whole point of this post.
Define "breaking" for your modules
Semver numbers are only promises. Write down what you promise. A practical rule set:
| Change | Bump |
|---|---|
| Remove or rename an input variable | Major |
| Remove or rename an output | Major |
| Change a resource address without a moved block | Major |
| Tighten a validation rule so old values fail | Major |
| Raise the minimum provider version | Minor or major, state which in your policy |
| Add an optional input with a safe default | Minor |
| Fix a bug, no interface change | Patch |
The moved block matters here. HashiCorp notes that refactoring blocks record how resource names and module structure changed between versions, so Terraform can map existing objects to new addresses during planning. Without one, a rename can plan a destroy and create.
Release steps
- Merge the change to the default branch through review.
- Choose the next version using the table above.
- Create an annotated tag on the reviewed commit.
- Write release notes that name each breaking change and the edit callers must make.
- Announce major bumps to caller teams before they bump their pins.
Protect the tags once released. See rulesets for release tags.
Pre-releases
A pre-release has a dash suffix, such as 1.2.0-beta. Terraform does not match pre-release versions with >, >=, <, <= or ~>. A caller must name the exact version. That makes pre-releases safe for trials, because nobody receives one by accident through a range.
Caller side
Callers using a range will get new minor and patch versions the next time they run terraform init -upgrade. Without that flag, the installed version stays. Changing a registry version or a source also requires rerunning terraform init. Review the plan after any upgrade, since a clean semver history does not guarantee your own configuration plans cleanly.
For third party modules, HashiCorp's advice is to pin specific versions so updates happen when convenient.
Where Safeguard fits
Safeguard's SBOM and dependency scanning can inventory dependencies across repositories, and policies can gate a pull request that changes a pin. The versioning rules above are still a human agreement.
Quick check
Does a v prefix on the tag matter?
HashiCorp says the v prefix is optional for registry tags. Pick one style and keep it.
Will a tilde arrow range pick up a pre-release?
No. Terraform does not match pre-release versions on the tilde arrow operator, so callers must name the exact pre-release version.
Sources
- Terraform: publish modules
- Terraform: version constraints
- Terraform: module blocks
- Terraform: develop modules
Last reviewed: 19 October 2026.