Fatskills
Practice. Master. Repeat.
Study Guide: Terraform Module Versioning - A Hyper-Practical Study Guide
Source: https://www.fatskills.com/cloud-application-developer/chapter/tech-terraform-module-versioning-a-hyper-practical-study-guide

Terraform Module Versioning - A Hyper-Practical Study Guide

By Fatskills Exam Guides Team — the exam nerds behind 28,500+ quizzes and 2.1M practice questions across 500+ global exams.

⏱️ ~7 min read

Terraform Module Versioning: A Hyper-Practical Study Guide

(Zero fluff, 100% actionable for real projects & certs)


1. What This Is & Why It Matters

What it is: Terraform modules let you package reusable infrastructure (e.g., a VPC, an ECS cluster, or a secure S3 bucket) into a single unit. Versioning those modules ensures you can: - Upgrade safely (e.g., "v1.2 adds encryption, but v1.1 doesn’t"). - Roll back if a new version breaks production. - Share modules across teams without breaking existing deployments.

Why it matters in production: Imagine you’re a cloud engineer at a fintech startup. Your team deploys 50+ AWS accounts using the same Terraform module for a secure VPC. One day, a junior engineer updates the module to add a new subnet—but accidentally removes the enable_dns_support flag. Without versioning, every single account deploys the broken change instantly. With versioning, you can: - Pin accounts to v1.0 (stable). - Test v1.1 in staging. - Roll out only after validation.

Real-world scenario: You inherit a legacy Terraform codebase where all modules are referenced via git::https://github.com/company/terraform-aws-vpc.git. Every terraform apply pulls the latest commit—meaning a single git push can break production. Your mission: Lock down versions so deployments are predictable.


2. Core Concepts & Components

Term Definition Production Insight
Module Source The location of the module (e.g., GitHub, Terraform Registry, S3). Never use ref=main in production—it’s a ticking time bomb.
Version Constraint A rule like version = "~> 1.2" that restricts which module versions Terraform can use. Use ~> (pessimistic) for minor updates, = for exact versions.
Terraform Registry Public/private repository for sharing modules (like Docker Hub for IaC). Private registries (e.g., Terraform Cloud) are critical for enterprise secrets.
Semantic Versioning (SemVer) MAJOR.MINOR.PATCH (e.g., 1.2.3). Breaking changes = MAJOR bump. New features = MINOR. Bug fixes = PATCH.
Git Source Modules stored in Git (e.g., source = "git::https://github.com/org/repo.git?ref=v1.2.0"). Always pin to a tag (ref=v1.2.0) or commit hash (ref=abc123).
Local Source Modules stored on disk (e.g., source = "./modules/vpc"). Avoid in production—hard to version and share.
Module Outputs Values exposed by a module (e.g., vpc_id, subnet_ids). Document outputs in README.md—future you will thank you.
Module Inputs Variables passed to a module (e.g., cidr_block = "10.0.0.0/16"). Validate inputs with validation blocks to catch misconfigurations early.
Terraform Lock File (terraform.lock.hcl) Tracks exact provider and module versions. Commit this file to Git—it ensures reproducibility.
Module Calls How you reference a module in code (e.g., module "vpc" { source = "..." }). Use descriptive names (e.g., module "prod_vpc" instead of module "vpc").

3. Step-by-Step: Versioning a Module (Hands-On)

Prerequisites

  • Terraform installed (>= 1.0.0).
  • GitHub account (or GitLab/Bitbucket).
  • AWS account (for testing a real module).

Goal:

Convert a legacy module from "latest commit" to versioned releases.


Step 1: Create a Module Repository

  1. Create a new GitHub repo (e.g., terraform-aws-vpc).
  2. Add a basic module (example structure): terraform-aws-vpc/ ? main.tf # VPC, subnets, etc. ? variables.tf # Input variables ? outputs.tf # Exposed values ? README.md # Documentation
  3. Example main.tf (simplified VPC): ```hcl resource "aws_vpc" "this" { cidr_block = var.cidr_block enable_dns_support = true tags = var.tags }

variable "cidr_block" { type = string }

variable "tags" { type = map(string) default = {} }

output "vpc_id" { value = aws_vpc.this.id } ```


Step 2: Tag a Release (SemVer)

  1. Commit your changes: bash git add . git commit -m "Initial VPC module"
  2. Tag the first version (follow SemVer): bash git tag -a v1.0.0 -m "Initial release" git push origin v1.0.0

Step 3: Reference the Module with Versioning

  1. In your root Terraform config (main.tf): hcl module "vpc" { source = "git::https://github.com/your-org/terraform-aws-vpc.git?ref=v1.0.0" cidr_block = "10.0.0.0/16" tags = { Environment = "prod" } }
  2. Initialize Terraform: bash terraform init
  3. Terraform will download the module at v1.0.0 and cache it in .terraform/modules.

Step 4: Upgrade the Module (Safely)

  1. Make a breaking change (e.g., remove enable_dns_support): hcl # main.tf (in the module repo) resource "aws_vpc" "this" { cidr_block = var.cidr_block # enable_dns_support removed (BREAKING CHANGE) tags = var.tags }
  2. Tag a new major version: bash git add . git commit -m "Remove DNS support (breaking change)" git tag -a v2.0.0 -m "Breaking: Remove DNS support" git push origin v2.0.0
  3. Update the root module to use v2.0.0: hcl module "vpc" { source = "git::https://github.com/your-org/terraform-aws-vpc.git?ref=v2.0.0" cidr_block = "10.0.0.0/16" tags = { Environment = "prod" } }
  4. Apply the change: bash terraform init -upgrade # Forces re-download of the module terraform plan # Verify the change terraform apply

Step 5: Use Version Constraints (Pessimistic Operator)

Instead of pinning to v2.0.0, allow non-breaking updates (e.g., bug fixes):

module "vpc" {
  source  = "git::https://github.com/your-org/terraform-aws-vpc.git?ref=v2.0.0"
  version = "~> 2.0"  # Allows 2.0.1, 2.1.0, but not 3.0.0
  cidr_block = "10.0.0.0/16"
}
  • ~> 2.0: Allows 2.x.x but not 3.0.0.
  • ~> 2.0.0: Allows 2.0.x but not 2.1.0.

Step 6: Verify the Lock File

  1. Check terraform.lock.hcl: ```hcl # This file is maintained automatically by "terraform init". # Manual edits may be lost in future updates.

provider "registry.terraform.io/hashicorp/aws" { version = "5.23.0" constraints = "~> 5.0" hashes = [ "h1:...", ] }

module "vpc" { source = "git::https://github.com/your-org/terraform-aws-vpc.git?ref=v2.0.0" version = "2.0.0" dir = ".terraform/modules/vpc" } 2. Commit the lock file to Git:bash git add terraform.lock.hcl git commit -m "Add module lock file" ```


4.-Production-Ready Best Practices

Security

  • Private modules: Use Terraform Cloud or GitHub private repos for sensitive modules (e.g., IAM policies).
  • Sign module releases: Use GPG-signed tags to prevent tampering.
  • Scan for secrets: Run git-secrets or trivy on module repos to catch hardcoded credentials.

Cost Optimization

  • Avoid "latest": Always pin versions to prevent unexpected cost spikes (e.g., a module update enabling expensive logging).
  • Tag resources: Use tags = var.tags in modules to track costs per environment.

Reliability & Maintainability

  • SemVer for modules: Follow MAJOR.MINOR.PATCH strictly.
  • MAJOR: Breaking changes (e.g., removing a variable).
  • MINOR: Backward-compatible features (e.g., adding a new subnet).
  • PATCH: Bug fixes (e.g., fixing a typo in a tag).
  • Changelog: Maintain a CHANGELOG.md in the module repo.
  • Deprecation warnings: Use terraform validate and check blocks to warn about deprecated variables.

Observability

  • Module outputs: Expose critical values (e.g., vpc_id, security_group_id) for debugging.
  • Logging: Use terraform plan -out=tfplan and store plans in S3 for auditing.

5. Common Mistakes & Traps

Mistake Symptom Fix/Prevention
Using ref=main Every terraform apply pulls the latest commit, breaking production. Always pin to a tag (ref=v1.2.0) or commit hash.
No version constraints Terraform silently upgrades to the latest module version. Use version = "~> 1.2" in module calls.
Not committing terraform.lock.hcl Different team members get different module versions. Commit the lock file to Git.
Breaking changes in PATCH releases A "bug fix" update breaks existing deployments. Follow SemVer: PATCH = bug fixes only.
Hardcoding values in modules Module can’t be reused (e.g., hardcoded cidr_block). Use variables for all configurable values.

6.-Exam/Certification Focus

Typical Question Patterns

  1. "Which version constraint allows 1.2.3 and 1.3.0 but not 2.0.0?
  2. ? ~> 1.2
  3. ? >= 1.2.0, < 2.0.0 (correct but verbose)
  4. = 1.2.3 (too restrictive)

  5. "You need to update a module but avoid breaking changes. Which version should you use?"

  6. ? ~> 1.2 (allows 1.2.x and 1.3.x but not 2.0.0)
  7. >= 1.0.0 (allows 2.0.0)

  8. "Where does Terraform store downloaded modules?"

  9. ? .terraform/modules/
  10. ? .terraform/providers/ (stores providers, not modules)

Key Trap Distinctions

  • ref=main vs ref=v1.2.0:
  • ref=main: Always pulls the latest commit (dangerous).
  • ref=v1.2.0: Pins to a specific version (safe).
  • version = "1.2.0" vs version = "~> 1.2":
  • = 1.2.0: Exact version (no updates).
  • ~> 1.2: Allows 1.2.x and 1.3.x (non-breaking updates).

7.-Hands-On Challenge

Challenge: You have a module referenced like this:

module "s3_bucket" {
  source = "git::https://github.com/company/terraform-aws-s3.git"
}

Problem: Every terraform apply pulls the latest commit, which is risky.

Task:
1. Tag the module repo with v1.0.0.
2. Update the module call to use v1.0.0 with a version constraint.
3. Verify the lock file is updated.

Solution:
1. Tag the module: bash git tag -a v1.0.0 -m "Initial release" git push origin v1.0.0
2. Update the module call: hcl module "s3_bucket" { source = "git::https://github.com/company/terraform-aws-s3.git?ref=v1.0.0" version = "~> 1.0" }
3. Run terraform init -upgrade and check terraform.lock.hcl.

Why it works: - ref=v1.0.0 pins the exact version. - version = "~> 1.0" allows future 1.x.x updates but not 2.0.0.


8.-Rapid-Reference Crib Sheet

Command/Concept Example Notes
Git module source source = "git::https://github.com/org/repo.git?ref=v1.2.0" Always pin to a tag or commit hash.
Terraform Registry source source = "terraform-aws-modules/vpc/aws" Use version = "~> 3.0" to constrain.
Local module source source = "./modules/vpc" Avoid in production.
Version constraint version = "~> 1.2" Allows 1.2.x and 1.3.x.
Exact version version = "= 1.2.0" No updates allowed.
Pessimistic operator ~> 1.2 Allows 1.2.x but not 2.0.0.
Lock file location .terraform.lock.hcl Commit this to Git!
Module cache .terraform/modules/ Stores downloaded modules.
SemVer MAJOR.MINOR.PATCH Breaking changes = MAJOR bump.
Tag a release git tag -a v1.0.0 -m "Initial release" Use annotated tags for changelogs.
List module versions terraform providers mirror ./mirror Shows all versions available.
Default behavior ref=main Never use in production!

9.-Where to Go Next

  1. Terraform Module Registry – Browse official modules.
  2. Terraform Module Sources – Official docs on module sources.
  3. Semantic Versioning (SemVer) – Rules for versioning.
  4. Terraform Cloud Private Registry – For enterprise module management.