How do you version and source modules (registry, git, local path)?
Quick Answer
The `source` argument tells Terraform where to fetch a module: a local relative path (`./modules/vpc`, no versioning — always uses what's on disk), the public/private Terraform Registry (`hashicorp/vpc/aws`, supporting a `version` constraint like `~> 5.0`), or a direct Git URL (`git::https://.../repo.git?ref=v1.2.0`) pinned to a tag/branch/commit. Registry and Git sources should always be pinned to an explicit version/ref in shared/production code — unpinned sources silently pull in breaking changes on the next `terraform init -upgrade`.
Detailed Answer
The source argument on a module block controls where Terraform fetches the module's code from, and each source type has a different (or absent) versioning story.
Local path — no versioning
module "vpc" {
source = "./modules/vpc"
}
Always uses whatever's currently on disk at that relative path. There's no version pinning because there's nothing to pin — the code lives in the same repository/checkout. Good for tightly-coupled, same-repo modules; not suitable for sharing across repositories/teams since there's no way to "bump a version."
Terraform Registry — explicit version constraints
module "vpc" {
source = "terraform-aws-modules/vpc/aws"
version = "~> 5.0"
}
The public registry (registry.terraform.io) or a private registry (Terraform Cloud, or self-hosted) hosts versioned, publishable modules. The version argument uses the same constraint syntax as providers (~> 5.0 = "5.x only," >= 5.1, < 6.0, etc.), and terraform init resolves the highest version matching the constraint.
Git — pinned to a ref
module "vpc" {
source = "git::https://github.com/my-org/terraform-modules.git//vpc?ref=v1.2.0"
}
Directly references a Git repository (optionally a subdirectory via //path), pinned via ?ref= to a tag, branch, or commit SHA. Useful for private/internal modules not published to a registry. Pinning to a tag or commit SHA (not a branch like main) is important for reproducibility — a branch ref can change underneath you between two init runs.
Why pinning matters
Unpinned sources (a bare branch ref, or omitting version entirely for registry modules if that were possible) mean the exact same configuration can resolve to different module code on different days, purely based on when terraform init -upgrade happened to run — a classic source of "it worked yesterday" bugs. In shared/production code, always pin to an explicit version or immutable ref, and bump it deliberately (reviewed in a PR) rather than letting it float.