Module Source
The source argument in a Terraform module block tells Terraform where to find the module code — a local relative path, a Terraform Registry address, a GitHub URL, or a private registry. The source determines how Terraform downloads and versions the module.
What Is a Terraform Module Source?
The source argument is the address inside a module block that tells Terraform exactly where to go and fetch the module's code from. It is the only required argument in any module call — without it, Terraform has no idea what you are trying to use.
Think of source like the address line on a delivery — it can point to a folder right next to your code, a published package in a public catalog, or a specific commit in a private company repository. Each format has its own rules for versioning.
At Zerodha, the security team maintains a private Git repository of approved modules — terraform-security-baseline. Every team's root module must source its IAM role module from that one repository and pin it to a tag. This is how the security team enforces that nobody accidentally creates an IAM role with * permissions — the approved module simply does not allow it.
Local Path Source
Used when the module lives inside the same repository as the code calling it. Always starts with ./ or ../:
module "vpc" { source = "./modules/vpc" # relative to the calling .tf file's directory} module "shared_tags" { source = "../../shared-modules/tags" # going up two directories first}Local sources do not support a version argument — there is nothing to version. Whatever is in that folder right now is what gets used.
Terraform Registry Source
The public registry uses a short three-part address: <NAMESPACE>/<NAME>/<PROVIDER>. This is the format you see for community modules like terraform-aws-modules:
module "vpc" { source = "terraform-aws-modules/vpc/aws" # namespace / name / provider version = "~> 5.0" # registry modules SHOULD always pin a version}GitHub / Git Source
For modules hosted directly on GitHub or any Git server, prefix with git:: and the full clone URL. Use ?ref= to pin to a tag, branch, or commit SHA:
module "ecs_service" { source = "git::https://github.com/swiggy-platform/tf-modules.git?ref=v3.1.0" # ^^^^^^^^^^^ # tag, branch, or full commit SHA} # Pulling a module from a SUBDIRECTORY inside a larger repo — use a double slashmodule "vpc" { source = "git::https://github.com/swiggy-platform/tf-modules.git//networking/vpc?ref=v3.1.0" # ^^ subdirectory path after //} # Pinning to a specific commit SHA instead of a tag — useful for maximum reproducibilitymodule "rds" { source = "git::https://github.com/swiggy-platform/tf-modules.git//database?ref=a1b2c3d4"}Tag vs Branch vs Commit — Which Should You Pin To?
NoteTip from a senior engineer: never, ever point
ref=atmainormasterin a production root module. A teammate can merge a breaking change to that branch at 2pm and yourterraform init -upgradeat 3pm silently pulls it in.
| Pin Type | Stability | When to Use |
|---|---|---|
Git tag (ref=v2.1.0) |
Highest — tags do not move | Production root modules — always |
Commit SHA (ref=a1b2c3d) |
Highest — exact snapshot | When you need byte-for-byte reproducibility |
Branch (ref=main) |
Lowest — moves underneath you | Only inside the module repo's own CI testing |
Private Registry Source
Terraform Cloud and Terraform Enterprise host private registries with a four-part address that includes your organisation's hostname:
module "payments_vpc" { source = "app.terraform.io/phonepay-org/vpc/aws" # ^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^ # private registry organisation namespace version = "1.4.2"}SSH Source for Private Git Repos
If your company's Git server requires SSH authentication instead of HTTPS tokens, use the git:: prefix with an SSH URL:
module "internal_module" { source = "git::ssh://git@github.com/cred-platform/tf-modules.git//iam-role?ref=v1.0.0"}This requires your CI runner or laptop to already have an SSH key registered with access to that repository.
Quick Reference
| Source Format | Example | Supports version? |
|---|---|---|
| Local path | ./modules/vpc |
No |
| Public registry | terraform-aws-modules/vpc/aws |
Yes |
| Git (HTTPS) | git::https://github.com/org/repo.git?ref=v1.0.0 |
No — use ref instead |
| Git (SSH) | git::ssh://git@github.com/org/repo.git?ref=v1.0.0 |
No — use ref instead |
| Private registry | app.terraform.io/org/module/provider |
Yes |
| Error | Root Cause | Fix |
|---|---|---|
Error downloading module |
Git URL typo or no network access | Double-check the exact clone URL and your VPN/runner network access |
Module source has changed |
Source string was edited | Run terraform init again — Terraform refuses to proceed silently |
git exited with 128 |
SSH key not authorized for the repo | Add your deploy key or personal key to the Git server |
Could not find module in registry |
Wrong namespace/name/provider order | Re-check the exact three-part address on registry.terraform.io |
Frequently Asked Questions
What source formats does Terraform's module block actually support?
Terraform accepts several source types: a local path (`./modules/vpc`), a Terraform Registry address (`hashicorp/consul/aws`), a Git URL (with optional `ref` for a tag, branch, or commit), an HTTP URL to an archive, or an S3/GCS bucket path. The format determines both how Terraform fetches the code during `terraform init` and how versioning works — registry sources use a separate `version` argument, while Git sources pin a version through the `ref` query parameter in the URL itself.
What's a common mistake with module source versioning that causes drift between environments?
Pointing a module source at a Git branch (like `?ref=main`) instead of a tagged release means every `terraform init` can pull in different code without any version bump showing up in state, so dev and prod can silently diverge even though both reference 'the same' module. Pinning to an immutable tag or commit SHA, and bumping it deliberately with a visible diff, is the safer pattern for anything beyond a personal sandbox.