kb/modules ── Modules ── intermediate

Module versioning: semver and version constraints

A module version is a semver tag (`v1.2.3`) on a git repository or in the Registry. `version = "~> 5.7"` means "5.7 or newer within the 5.x range". Without a version pin, Terraform takes the latest, which is a hazard in CI. MAJOR (1->2) breaks the contract, MINOR (5.7->5.8) adds features compatibly, PATCH (5.7.0->5.7.1) fixes bugs. Upgrade with `init -upgrade` and review the plan before running apply.

view as markdownaka: terraform-module-version, terraform-module-versioning

Why version modules

Without a version pin: you call module "vpc" { source = "..." }. Today it downloads code in one shape. Tomorrow the module author releases a breaking change, renames an input. Your next terraform init pulls the new code, the plan shows a destroy on the VPC. In production.

Versioning fixes this: you pin a specific version, and updates happen only when you decide, after reviewing the plan.

Semantic versioning

The semver standard: MAJOR.MINOR.PATCH.

ComponentChanges whenExample
PATCHBug fixes, no contract change1.2.3 -> 1.2.4
MINORNew inputs/outputs or resources, backward-compatible1.2.3 -> 1.3.0
MAJORBreaking: removed or renamed inputs, changed resource type1.2.3 -> 2.0.0

The Terraform Registry and most module communities follow semver. It is not enforced, and theoretically someone could introduce a breaking change in a minor release. In practice, terraform-aws-modules/* follows semver strictly.

Version constraints

In a module block:

hcl
module "vpc" {
  source  = "terraform-aws-modules/vpc/aws"
  version = "5.7.0"   # exactly this version
  # version = "~> 5.7"  # 5.7.x, but not 5.8 and not 6.0
  # version = ">= 5.0, < 6.0"  # any 5.x
  # version = "~> 5.7.0"  # 5.7.x (excludes 5.8)
}

Operators

OperatorMeaning
5.7.0Exactly this version
>= 5.7.0This version or newer
~> 5.7.05.7.x (last component is free)
~> 5.75.x where x >= 7 (second-to-last component is free)
>= 5.0, < 6.0A range, explicit major boundary
!= 5.8.3Any version except this one (for example, a known-bad release)

Pessimistic constraint ~>

The most common choice in production. It means "the last component may grow; everything above it is fixed".

~> 5.7.0 is equivalent to >= 5.7.0, < 5.8.0: picks up patches, ignores minor bumps.

~> 5.7 is equivalent to >= 5.7, < 6.0: picks up minor releases, blocks major bumps.

The tradeoff: get bug fixes automatically, stay protected against breaking changes.

When to pin exactly

  • Production: use an exact version. version = "5.7.0". Upgrade deliberately, through a PR with code review.
  • Services with independent release cycles: use ~> 5.7.0. Patches come in automatically; minor bumps go through a PR.
  • Experimental environments or playgrounds: ~> 5.0 is fine. Pinning tightly adds no value here.

Where version constraints apply

The version constraint works only for:

  • Modules from the Terraform Registry (terraform-aws-modules/vpc/aws).
  • Providers through required_providers.version. See tf-version-constraints.

For git, HTTP, and S3 modules, the version argument does not work. Use ?ref=v5.7.0 in the URL instead:

hcl
module "billing" {
  source = "git::https://github.com/myorg/modules.git//billing?ref=v5.7.0"
}

The same constraint applies as for any git source: ref takes a static value, not an expression.

How to upgrade versions

Step 1: update the HCL

diff
module "vpc" {
  source  = "terraform-aws-modules/vpc/aws"
- version = "5.7.0"
+ version = "5.8.0"

Step 2: terraform init -upgrade

Without -upgrade, Terraform checks the cache and concludes it already has what it needs.

bash
terraform init -upgrade

Step 3: terraform plan

Read the plan carefully:

  • Any destroy in a plan coming from a module is a red flag. It likely means a breaking change.
  • Changes to default tags, ARNs, and IDs may be fine (in-place replace) or problematic (destroy + create). Context determines which.
  • When the plan is hard to interpret, open the module's CHANGELOG. Good modules have one.

Step 4: apply

Apply in a test environment first. Never upgrade a module directly in production.

Lockfile and modules

.terraform.lock.hcl records only provider versions. Module versions are not stored in the lockfile. The single source of truth for a module version is the version value in the HCL itself.

This means two developers with identical HCL and the same version = "~> 5.7" can end up with different minor versions of a module if a new release came out between their init runs. The fix is an exact version pin in production.

Pitfalls

  • ~> 5.0 is not the same as ~> 5.0.0. ~> 5.0 allows anything up to 5.99.99. ~> 5.0.0 allows only 5.0.x. Know the difference or you will pick up minor updates you did not expect.

  • No version specified means "give me the latest". A module "x" { source = "..." } block without a version in the Registry downloads the newest release. Fine on a brand-new project. On an existing one, you get the classic "it was working and I changed nothing."

  • Semver does not mean no plan. Even a minor upgrade like 5.7.0 -> 5.8.0 (new resources, backward-compatible) adds resources to state. That is a plan, just one without any destroys. Read it.

  • CHANGELOG and UPGRADE_GUIDE are required on large modules. Before a major upgrade of terraform-aws-modules/vpc/aws from 4 to 5, open the UPGRADE.md in the module repository. It lists which inputs were renamed. Skipping that step means debugging afterward.

  • Pre-release versions (5.8.0-rc.1) match ~> 5.8. This can be surprising. Terraform treats a pre-release as greater than 5.8.0. To exclude pre-releases, add < 5.9.0-0 to the constraint.

  • You removed a module block, but the module is still downloaded. That is harmless. terraform init -upgrade will clean it up. The leftover code in .terraform/modules/ is clutter, not a bug.

§ commands

bash
terraform init -upgrade

Re-read version constraints and download the newest versions within them. Safe: updates only within the existing constraint bounds.

bash
terraform providers

Prints the module tree together with providers. Shows which module versions were actually resolved.

bash
cat .terraform/modules/modules.json | jq '.Modules[] | {Source, Version}'

Lists the source and version of every downloaded module. Use this to verify what is currently installed.

bash
terraform plan -out=plan.bin && terraform show plan.bin

Visually inspect the plan after upgrading a module, paying attention to any destroy operations.

§ see also