Terraform Module Versioning
Module versioning controls which version of a module a configuration uses. Pinning versions ensures that a configuration that works today still works in three months — when the module author releases a breaking change, your infrastructure isn’t affected until you consciously choose to upgrade.
Version Constraints
Version constraints in Terraform use the same operators as provider versioning:
# Exact versionmodule "vpc" { source = "terraform-aws-modules/vpc/aws" version = "5.1.2" # Exactly this version}
# Minimum versionmodule "vpc" { source = "terraform-aws-modules/vpc/aws" version = ">= 5.0.0" # 5.0.0 or newer}
# Version rangemodule "vpc" { source = "terraform-aws-modules/vpc/aws" version = ">= 5.0, < 6.0" # Any 5.x version}
# Pessimistic constraint (most common)module "vpc" { source = "terraform-aws-modules/vpc/aws" version = "~> 5.1" # 5.1.0 to <6.0.0 (same minor, any patch)}
module "vpc" { source = "terraform-aws-modules/vpc/aws" version = "~> 5.1.2" # 5.1.2 to <5.2.0 (same minor.patch, any patch)}Semantic Versioning for Modules
Module versions follow semver (MAJOR.MINOR.PATCH):
| Version bump | Meaning | Safe to auto-upgrade? |
|---|---|---|
Patch (5.1.1 → 5.1.2) | Bug fix, no API change | Yes |
Minor (5.1.x → 5.2.0) | New features, backwards compatible | Usually yes |
Major (5.x.x → 6.0.0) | Breaking changes to variables/outputs | No — review first |
The ~> constraint lets you auto-upgrade patches (or patches+minors) while blocking major version changes:
# Auto-upgrade within 5.x.x, never upgrade to 6.x.xversion = "~> 5.1"
# Auto-upgrade within 5.1.x, never upgrade to 5.2.0version = "~> 5.1.2"Module Lock Files
When you run terraform init, Terraform resolves the exact version that satisfies your constraint and records it in .terraform.lock.hcl:
# .terraform.lock.hcl (commit this to git!)provider "registry.terraform.io/hashicorp/aws" { version = "5.31.0" constraints = "~> 5.0" hashes = [ "h1:abc123...", "zh:def456...", ]}Note: The lock file tracks provider versions. Module versions from the Registry are recorded in .terraform/modules/modules.json but this file is NOT committed (it’s in .gitignore). The version constraint in your .tf file is what’s “locked” for modules.
Registry Modules: Versioning in Practice
# Reference popular modules with pinned versionsmodule "vpc" { source = "terraform-aws-modules/vpc/aws" version = "~> 5.1"
name = "production-vpc" cidr = "10.0.0.0/16" azs = ["us-east-1a", "us-east-1b", "us-east-1c"]
private_subnets = ["10.0.1.0/24", "10.0.2.0/24", "10.0.3.0/24"] public_subnets = ["10.0.101.0/24", "10.0.102.0/24", "10.0.103.0/24"]}
module "eks" { source = "terraform-aws-modules/eks/aws" version = "~> 20.0"
cluster_name = "production" cluster_version = "1.31" vpc_id = module.vpc.vpc_id subnet_ids = module.vpc.private_subnets}
module "rds" { source = "terraform-aws-modules/rds/aws" version = "~> 6.0"
identifier = "production-db" engine = "postgres" vpc_id = module.vpc.vpc_id subnet_ids = module.vpc.private_subnets}Git-Based Module Versioning
For modules hosted in git, use tags as versions:
# Specific git tag (most stable)module "networking" { source = "git::https://github.com/mycompany/terraform-modules.git//networking?ref=v2.1.0"}
# Specific commit SHA (most reproducible)module "networking" { source = "git::https://github.com/mycompany/terraform-modules.git//networking?ref=abc1234567890"}
# Branch reference (least stable — changes on every push)module "networking" { source = "git::https://github.com/mycompany/terraform-modules.git//networking?ref=main" # Avoid in production — the branch moves}Git-based modules don’t support the version argument — the ref parameter in the source URL is the version pin.
Upgrading Module Versions
# See what modules are currently installedcat .terraform/modules/modules.json | python3 -m json.tool
# To upgrade to a newer compatible version, just run init again# (version constraint controls what's selected)terraform init -upgrade
# After upgrading, always run plan to see what changesterraform plan
# If a major version upgrade is required, update the constraint first:# version = "~> 5.1" → version = "~> 6.0"# Then check the changelog for breaking changes# Then run init -upgrade + planPrivate Registry Module Versioning (HCP Terraform)
# Private registry modules use the same version constraintsmodule "ecs_service" { source = "app.terraform.io/mycompany/ecs-service/aws" version = "~> 3.0"
name = "payment-service" image = var.image cluster_id = module.cluster.id}Publishing a new version to the private registry:
# Module naming convention: terraform-<provider>-<name># e.g., repo name: terraform-aws-ecs-service
# Tag the release in gitgit tag v3.1.0git push origin v3.1.0
# HCP Terraform picks up the new tag and publishes it as version 3.1.0Version Pinning Strategy
# PRODUCTION: Pin to specific minor version, allow patch upgradesmodule "vpc" { source = "terraform-aws-modules/vpc/aws" version = "~> 5.1" # 5.1.x — patches only}
# STAGING: Allow minor upgrades to test before productionmodule "vpc" { source = "terraform-aws-modules/vpc/aws" version = "~> 5.0" # Any 5.x.x}
# DEVELOPMENT: Looser — test new major versionsmodule "vpc" { source = "terraform-aws-modules/vpc/aws" version = ">= 5.0"}Upgrade workflow:
- Update version constraint in staging first
- Run
terraform init -upgrade && terraform plan - Review plan for unexpected changes
- Apply to staging and test
- Update production constraint
- Apply to production