Terraform

Practical Terraform Module Versioning: Operational Guidelines for source and version

2026-04-19
NicheeLab Editorial Team

ハブ記事: Complete Guide to Terraform Modules

Hub article covering the full picture of Terraform modules — design, distribution, and operations

Terraform modules specify their origin with source, and version constraints can only be applied when the source is a registry. For URL sources such as Git, version is invalid — you pin tags or commits via ref instead.

This article walks through, end to end, the minimal principles for achieving safe upgrades in practice and the points most frequently tested on certification exams.

Basics: source is required, version is registry-only

source is required in a module block. Only registry-backed modules can use the version argument for version constraints. For non-registry sources such as Git or local paths, version is not allowed and will error during initialization.

For URL-based sources like Git, you pin by specifying a tag name, branch, or commit SHA via the ref query. If reproducibility is the priority, pinning by tag or commit SHA is the standard.

  • Registry source: source is a registry address; version constraints are allowed
  • URL source (Git/HTTP, etc.): version is not allowed; pin with ref
  • Local module: No version control needed — changes are reflected immediately
  • The version under required_providers and a module's version are different things

Module resolution flow

module "x" {
  source  +  version?        terraform init
    |            |                 |
    |            |       Dependency resolution engine
    |            |                 v
    +--> Source address ----> Origin detection
           |                         |
           |                         +--> Registry: resolve version constraint and pick latest match
           |                         |
           |                         +--> Git/HTTP etc.: if ref is set, fetch that tag/commit
           v                                  (otherwise default branch)
    Expanded into .terraform/modules

Basic example via the registry

terraform {
  required_version = ">= 1.3"
}

module "consul" {
  source  = "hashicorp/consul/aws"   # Registry address
  version = "~> 0.10"                 # Pin to 0.10.x (excludes future 0.11.x)
}

Pinning methods and pitfalls by source type

The difference between registry and Git specifications is commonly confused both on exams and in practice. version can only be used with the registry. For Git, you specify a tag or commit via ref=. Local paths and HTTP archives also do not support version.

Reproducibility is the top priority. In production, avoid pinning to branches (main or master) — pin to a tag or commit SHA instead.

  • Registry: version supports SemVer range constraints
  • Git: Pin with ref=<tag name> or ref=<commit SHA>. Range constraints are not supported
  • HTTP/S3/GCS archive: Prefer URLs that point to immutable, hashed objects
  • Local: Not suitable for team sharing — reproducibility depends on VCS management
Source typeHow to specify the versionExampleCaveats
Registry (public/private)Constraint expression on versionsource = "app.terraform.io/acme/network/aws" version = "~> 2.4"init -upgrade resolves the latest allowed version. Semantic Versioning
Git (tag)ref=<tag> on sourcesource = "git::https://github.com/org/terraform-modules.git//vpc?ref=v1.2.3"Range constraints are not supported. Disciplined tag management is required
Git (commit SHA)ref=<SHA> on sourcesource = "git::ssh://[email protected]/org/mods.git//eks?ref=9f2c0ab"Full pinning gives high reproducibility but is hard for humans to read
Local pathNonesource = "../modules/alb"Local changes apply immediately. Not recommended for CI/CD or multi-environment use
HTTP archiveNonesource = "https://example.com/mods/vpc-1.2.3.zip"Manage checksums to guard against tampering

Example of Git tag pinning with subdirectory specification

module "vpc" {
  source = "git::https://github.com/org/terraform-modules.git//network/vpc?ref=v1.2.3"
}

module "eks" {
  source = "git::ssh://[email protected]/org/iac.git//modules/eks?ref=9f2c0abd1"
}

Operating the Terraform Cloud/Enterprise Private Registry

In the Terraform Cloud/Enterprise private module registry, use app.terraform.io/<org>/<name>/<provider> as the source and constrain via version. Register SemVer release tags from your VCS, and consumers receive them through version constraints.

Authenticate the first time with terraform login; modules are then resolved at init. To upgrade, keep the version constraint in code and run terraform init -upgrade to move to the latest version within the allowed range.

  • Create SemVer tags (e.g., v2.4.1) in VCS and publish them to the registry
  • Consumers allow patch updates via range constraints like version = "~> 2.4"
  • Breaking changes go in a major bump; backward-compatible changes go in minor/patch
  • Authentication is configured via CLI config or terraform login

Example: Using a private registry

module "network" {
  source  = "app.terraform.io/acme/network/aws"
  version = "~> 2.4"   # Allow 2.4.x
}
# First-time authentication: terraform login

Constraint Design Patterns: Balancing Stability and Easy Upgrades

For registry sources, use SemVer-compliant constraints. The ~> operator follows the convention of "pin the two leftmost components", so ~> 1.2 means >= 1.2.0 and < 1.3.0. You can design per-environment policies — e.g., production allows patches only, while staging allows minor updates.

Git sources do not support ranges; you upgrade explicitly by changing the tag or commit. If you want automatic upgrades, choose registry publication and version constraints.

  • Patches only automatic: version = "~> 2.4"
  • Up to minor automatic: version = "~> 2"
  • Fully pinned: version = "= 2.4.3" or Git ref=<tag/commit>
  • Pre-releases compare via SemVer rules. As a rule, avoid them in production

Concrete constraint examples

module "web" {
  source  = "app.terraform.io/acme/web/aws"
  version = "~> 1.8"     # 1.8.x
}

module "batch" {
  source  = "app.terraform.io/acme/batch/aws"
  version = ">= 2.3, < 3.0"  # Compound condition
}

# Git does not support ranges. Pin with a tag/commit.
module "ops" {
  source = "git::https://github.com/acme/ops-mods.git//alerts?ref=v0.9.5"
}

Upgrade Procedures and CI Integration

For registry-backed modules, init reuses the previously downloaded version if it exists. To move to a newer version within the allowed range, run terraform init -upgrade. Git sources are not updated unless you change the ref in code.

The dependency lock file (.terraform.lock.hcl) is for providers only and does not apply to modules. Reproducibility for modules is ensured through version constraints (or tag/commit pinning for Git) and by recreating the .terraform directory.

  • Registry: An init with no changes does not re-resolve — use -upgrade to re-resolve
  • Git: Immutable unless the ref is updated. CI will not silently move to the latest
  • .terraform/ is a cache. To reproduce, delete it and re-run init
  • Explicitly run init -upgrade as a job step before plan

Example CI job

# Bash example
set -euo pipefail
rm -rf .terraform/          # Clean reproduction
terraform init -upgrade
terraform validate
terraform plan -out=tfplan

Anti-Patterns and Exam Pitfalls

Specifying version on a Git source is a common mistake that frequently appears on exams. version is registry-only; Git uses ref. Also avoid pinning to mutable branches like main in production.

Don't confuse provider version pinning with module version pinning. required_providers is for providers only; module version is for registry modules only.

  • Mistake: Adding version to a Git source → error
  • Mistake: Pinning to a branch (ref=main) in production → unexpected diffs
  • Mistake: Expecting modules to upgrade on init alone → -upgrade is required
  • Correct: Use version for registry, ref for Git; local needs no pinning at all

Bad and good examples

# Bad: version on a Git source
module "bad" {
  source  = "git::https://github.com/org/mods.git//vpc"
  version = "~> 1.2"   # ← Error: version is not valid here
}

# Good: Pin Git by tag
module "good_git" {
  source = "git::https://github.com/org/mods.git//vpc?ref=v1.2.3"
}

# Good: Range constraint on the registry
module "good_registry" {
  source  = "app.terraform.io/acme/vpc/aws"
  version = "~> 1.2"
}

Check with a Practice Question

Associate / Pro

問題 1

You want to use a module hosted on GitHub and ensure reproducibility in production. Which specification is recommended?

  1. Specify version = "~> 1.2" on the module
  2. Set source to git::...//path?ref=v1.2.3
  3. Set source to a local relative path (../modules/xxx)
  4. Pin ref=main and always run terraform init -upgrade to get the latest

正解: B

version is for registry modules only and is not valid on Git sources. To ensure reproducibility, pin a tag (or commit SHA) via ref. Local paths are poor for sharing and reproducibility, and pinning to main is too volatile for production.

Frequently Asked Questions

Is there a lock file for modules like the one used for providers?

No. .terraform.lock.hcl is a lock file for providers only. For modules, reproducibility is ensured via version constraints (registry) or ref pinning (Git).

Is it OK to prefix tags with v in the registry?

The common SemVer v-prefix (v1.2.3) is supported. SemVer-compliant tags are also recommended for the Terraform Cloud/Enterprise private registry.

Does init alone automatically upgrade to the latest allowed version?

No. If a module is already downloaded, the same version is reused. To upgrade to the latest version within the allowed range, run terraform init -upgrade. Git sources are not updated unless you rewrite the ref.

Check what you learned with practice questions

Practice with certification-focused question sets

無料で問題を解いてみる
Author

NicheeLab Editorial Team

NicheeLab editorial team focused on data engineering and cloud certification learning. Content is structured around practical study needs and official exam domains.


Related articles
Terraform

HCL Syntax: Terraform's Configuration Language (2026)

HCL2 fundamentals for Terraform — blocks, attributes, expres...

Terraform

Terraform Authoring & Operations Pro: Complete Guide (2026)

Tactics for the Terraform Pro exam — module authoring, works...

Terraform

Terraform Providers: Plugin Management Fundamentals (2026)

Provider mechanics — required_providers, versions, mirrors, ...

Terraform

Terraform Resource Blocks: Declarative Infra Units (2026)

Resource block fundamentals — addresses, references, common ...

Terraform

Terraform Data Sources: Read-Only External Data (2026)

Data source basics — declaration, refresh behavior, dependen...

Browse all Terraform articles (102)
© 2026 NicheeLab All rights reserved.