Stage 4 · Provision
Terraform at Scale
Module Versioning
Semantic versions, private registries, dependency constraints, and safe upgrade paths — managing module lifecycle at scale.
Versioning Strategy
Module versioning determines which changes are automatically accepted and which require manual intervention. Without a versioning strategy, a module update can break every environment that consumes it.
The goal of versioning is to communicate the impact of changes. Patch versions should be safe upgrades. Minor versions should add features without breaking changes. Major versions should signal breaking changes.
Semantic Versioning
Semantic versioning uses MAJOR.MINOR.PATCH to communicate change impact. A module following semver makes predictable upgrades possible. Breaking changes increment MAJOR. New features increment MINOR. Bug fixes increment PATCH.
| Version Change | Impact | Constraint Behavior |
|---|---|---|
| 1.2.3 to 1.2.4 | Bug fix — safe to upgrade | ~> 1.2.3 allows this |
| 1.2.3 to 1.3.0 | New feature — backward compatible | ~> 1.2 allows this |
| 1.2.3 to 2.0.0 | Breaking change — requires code update | ~> 1.2 blocks this |
module "vpc" {
source = "registry.example.com/org/vpc/aws"
version = "~> 1.2.3" # Allows 1.2.x, blocks 1.3.0
}
module "eks" {
source = "registry.example.com/org/eks/aws"
version = "~> 1.2" # Allows 1.x.x >= 1.2, blocks 2.0.0
}
module "rds" {
source = "registry.example.com/org/rds/aws"
version = ">= 1.0.0, < 2.0.0" # Explicit range
}The ~> operator is the most common constraint. On a three-part version (1.2.3), it allows patch updates. On a two-part version (1.2), it allows minor and patch updates. This gives you safe auto-updates within a range.
Constraint Syntax
# Exact version — no updates allowed
version = "1.2.3"
# Patch updates only
version = "~> 1.2.3" # Allows 1.2.4, blocks 1.2.5 and 1.3.0
# Minor updates only
version = "~> 1.2" # Allows 1.3.0, blocks 2.0.0
# Range with floor and ceiling
version = ">= 1.0.0, < 2.0.0"
# Exclude specific versions
version = ">= 1.0.0, != 1.3.0, < 2.0.0"
# Pre-release versions
version = "~> 1.2.0-beta.1"The constraint syntax supports ranges, exclusions, and pre-release versions. The comma operator ANDs constraints together. Use >= with < for explicit ranges when ~> is too permissive.
Private Registries
Private module registries host internal modules. Terraform Cloud, GitLab, GitHub Packages with Terraform, and custom registries following the Terraform Registry protocol all serve private modules.
# Terraform Cloud registry
module "vpc" {
source = "app.terraform.io/my-org/vpc/aws"
version = "~> 2.0"
}
# GitLab registry
module "vpc" {
source = "gitlab.example.com/my-group/vpc"
version = "~> 2.0"
}
# Self-hosted registry
module "vpc" {
source = "registry.example.com/my-org/vpc/aws"
version = "~> 2.0"
}The source format matches the registry protocol. Each registry type has its own authentication mechanism. Terraform Cloud uses tokens. GitLab uses deploy tokens. Self-hosted registries use HTTP basic auth.
Safe Upgrade Paths
$ terraform init -upgrade # Update lock file to latest allowed
$ terraform plan -out=plan.bin # Preview changes
# Review plan for unexpected changes
$ terraform apply plan.bin # Apply the upgrade
# Or pin to a specific version
$ terraform get -update # Update modules to latest
$ terraform init -upgradeThe -upgrade flag updates all modules and providers to the latest versions allowed by constraints. Always review the plan after upgrading. Unexpected changes indicate a module breaking change.
# .terraform.lock.hcl — committed to version control
provider "registry.terraform.io/hashicorp/aws" {
version = "5.31.0"
hashes = [
"h1:abc123...",
"zh:0123456789abcdef...",
]
}
module "vpc" {
source = "registry.example.com/org/vpc/aws"
version = "2.3.1"
}The lock file pins exact provider and module versions. Commit it to Git to ensure consistent versions across the team. The hashes verify binary integrity. Update it intentionally with terraform init -upgrade.
Dependency Management
Modules can depend on other modules. When a dependency updates, dependent modules may need updates too. Manage these cross-module dependencies carefully to avoid cascading failures.
# Module A outputs a value used by Module B
module "networking" {
source = "registry.example.com/org/networking/aws"
version = "~> 2.0"
cidr = "10.0.0.0/16"
}
module "compute" {
source = "registry.example.com/org/compute/aws"
version = "~> 1.0" # May need update if networking changes
vpc_id = module.networking.vpc_id
}
# If networking v3.0 changes the vpc_id output type,
# compute module must be updated to matchThe compute module depends on the networking module's output. A breaking change in networking (like changing vpc_id from string to object) breaks compute. Semantic versioning communicates these breaking changes.
Before upgrading a module in production, test it in an isolated environment. Create a temporary Terraform configuration that uses the new module version. Run plan and apply to verify the upgrade is safe.
If a module changes its output types, removes outputs, or changes required variable types, it must increment the major version. Failing to do so breaks consumers who use ~> constraints.
Mark this lesson complete to store local progress and unlock a cleaner resume path the next time you visit.