Skip to main content

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 ../:

HCL
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:

HCL
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:

HCL
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 slash
module "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 reproducibility
module "rds" {
source = "git::https://github.com/swiggy-platform/tf-modules.git//database?ref=a1b2c3d4"
}

Tag vs Branch vs Commit — Which Should You Pin To?

Note

Tip from a senior engineer: never, ever point ref= at main or master in a production root module. A teammate can merge a breaking change to that branch at 2pm and your terraform init -upgrade at 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:

HCL
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:

HCL
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.