Domain 5 of 8 · Chapter 1 of 4

How Terraform sources modules

The source argument: one address, several fetch methods

A module's source argument is an address, and Terraform reads its shape to decide how to install it. The same module block header works for a directory on disk, a published registry module, or a Git repository; only the source string changes:

module "network" {
  source = "./modules/network"       # local path
}

module "consul" {
  source  = "hashicorp/consul/aws"   # registry address
  version = "~> 0.11"
}

module "vpc" {
  source = "git::https://example.com/network.git//modules/vpc?ref=v1.2.0"  # Git package
}

Terraform sorts every source into one of three kinds: a local path, a registry address, or a remote package[1], the last being a repository or archive that Terraform downloads whole. The kind decides how Terraform fetches the code, whether the version argument applies, and how you point at a sub-directory. Terraform resolves the source when you run terraform init, and on Terraform v1.12 the value must be a literal string[2]: it does not support variables, template sequences, or arbitrary expressions, so source = "./modules/${var.name}" is a configuration error.

Local paths are read in place

A local path source begins with ./ or ../, the prefixes that tell Terraform to read files on disk instead of resolving a registry address. Terraform loads a relative local module in place, as part of the same package[1] as the configuration that calls it, rather than copying it into a cache. A change to the module's files therefore takes effect on the next plan or apply with no re-download.

The leading ./ is not decoration. A bare relative name such as source = "modules/network" is read as a registry address, not a directory, and terraform init fails trying to resolve it against a registry. Only paths beginning with / or a drive letter count as absolute, and those Terraform does copy into the local module cache, which is why absolute filesystem paths are discouraged: they couple the configuration to one machine's layout.

init installs every module

terraform init walks the configuration, installs every module source it finds, and records the result so later commands can read the code. You must re-run init after changing a source[3] so Terraform can update the installed copy. In outline, init reads each source, classifies it by shape, fetches it by the matching method, then installs the module for plan and apply. The three sections that follow take the local, registry, and remote-package kinds in turn.

terraform initRead the source string,classify by its shapeLocal path./ or ../read in placeRegistry addressNAMESPACE/NAME/PROVIDERregistry protocolRemote packagegit:: s3:: gcs:: URLdownload and extractModule installedfor plan and apply
How terraform init resolves a module source: read the string, classify it by shape, fetch it by the matching method, then install the module.

Registry addresses: public and private

Publishing a module to a registry is the primary way to share it across configurations, and a registry address is what a caller writes to pull it in. A public Terraform Registry[1] module uses a three-part address:

module "consul" {
  source  = "hashicorp/consul/aws"   # <NAMESPACE>/<NAME>/<PROVIDER>
  version = "~> 0.11"
}

The form is <NAMESPACE>/<NAME>/<PROVIDER>. The final <PROVIDER> segment names the provider the module targets (here aws) and is required: an address of only <NAMESPACE>/<NAME> does not resolve. With no hostname, Terraform resolves the address against the public registry at registry.terraform.io.

A private registry prepends a hostname

A private registry uses the same three-part address with a hostname prepended[1]:

Registry Address form Example
Public <NAMESPACE>/<NAME>/<PROVIDER> hashicorp/consul/aws
HCP Terraform app.terraform.io/<NAMESPACE>/<NAME>/<PROVIDER> app.terraform.io/example-corp/vpc/aws
Terraform Enterprise <HOSTNAME>/<NAMESPACE>/<NAME>/<PROVIDER> tfe.example.com/example-corp/vpc/aws

HCP Terraform uses the fixed hostname app.terraform.io, while a self-hosted Terraform Enterprise install uses its own deployment hostname. On either platform you can also use the generic localterraform.com hostname to request modules from the instance running the operation.

Why a registry rather than a Git URL

The registry protocol has full support for versioning[1], so registry addresses are the one source kind you pair with the version argument (constraint syntax is covered on the Managing module versions page). A private module registry in HCP Terraform[4] adds two things a plain Git URL cannot: it keeps modules confidential within an organization, and it presents a browsable catalog of published modules with their versions. A Git URL shares code, but it offers neither the version-constraint protocol nor the catalog.

Registry module addressHOSTNAMEapp.terraform.iopublic registry omits this/NAMESPACEexample-corp/NAMEvpc/PROVIDERaws
A registry module address is NAMESPACE/NAME/PROVIDER; a private registry such as HCP Terraform prepends a hostname like app.terraform.io.

Remote packages: Git, archives, and object storage

Everything that is not a local path or a registry address is a remote package (the Terraform docs call it a module package): a whole repository or archive that Terraform downloads and unpacks. Terraform detects the package type from a prefix or a well-known host, fetches the entire package, then reads the module from it.

Git and GitHub

Terraform runs git clone for Git-based sources. Three spellings are common:

module "a" { source = "github.com/org/repo" }              # GitHub over HTTPS
module "b" { source = "git@github.com:org/repo" }          # GitHub over SSH
module "c" { source = "git::https://example.com/vpc.git" } # any Git URL

The github.com/ shorthand[1] needs no git:: prefix; Terraform recognizes the host. The generic git:: prefix[1] works with any valid Git URL over SSH, HTTPS, or the Git protocol, and Bitbucket is detected from the bitbucket.org/ host. By default Terraform clones the branch referenced by HEAD; add ?ref= to select a branch, tag, or commit SHA, which is the version-pinning mechanism for Git sources.

Archives and object storage

Object-storage and archive sources fetch a compressed file instead of cloning:

  • s3::[1] followed by an S3 object URL, for example s3::https://s3-eu-west-1.amazonaws.com/example-bucket/vpc.zip. The object must be an archive (.zip, .tar.gz, and similar), which Terraform extracts.
  • gcs::[1] followed by a Google Cloud Storage object URL.
  • A plain HTTP or HTTPS URL[1]: if it ends in an archive extension, Terraform downloads and extracts it directly; otherwise Terraform treats it as a vanity URL, issues a GET with terraform-get=1, and expects the service to return the real source in an X-Terraform-Get header or a terraform-get meta tag.

These package sources take no version argument. A Git ?ref= is the only in-address version control; for an S3 or GCS archive, the version is whatever object the URL points at.

// selects a sub-directory

When the module you want sits below the root of a package, add // and the sub-directory path[1]:

module "vpc" {
  source = "git::https://example.com/network.git//modules/vpc?ref=v1.4.0"
}

Everything before // identifies the package that Terraform downloads; everything after is the path to the module inside it. Any query parameters, such as ?ref=, come after the sub-directory segment. Terraform extracts the whole package but reads only the named sub-directory.

git::https://example.com/network.git//modules/vpc?ref=v1.4.0Packagegit::https://example.com/network.gitdownloaded and extracted wholeSub-directory//modules/vpcread from the packageRevision?ref=v1.4.0Git ref pins branch/tag/SHA
A remote package source decomposes into the package Terraform downloads, a // sub-directory read from it, and a Git ?ref revision selector.

How this appears on the exam

Questions on this objective rarely ask for a definition; they show a source string and ask what Terraform does with it, or which of several addresses is valid. Each trap below is a restatement of a rule from the sections above.

  • A bare relative path is not local. source = "modules/vpc" looks like a folder but resolves as a registry address[1]; only ./ or ../ marks a local path. The fix is ./modules/vpc.
  • The provider segment is required. A registry address is <NAMESPACE>/<NAME>/<PROVIDER>, three parts. hashicorp/consul (two parts) is the distractor; hashicorp/consul/aws is correct.
  • version is registry-only. A stem that puts version = "1.2.0" on a git:: or local source is wrong. Pin a Git source with ?ref=v1.2.0; a local path cannot be pinned at all.
  • source is a literal string. On Terraform v1.12, source = "./modules/${var.env}" does not work; the value cannot interpolate a variable.
  • // is the sub-directory selector. To read modules/vpc from a repository, the answer is network.git//modules/vpc, not a single /. Everything before // is the downloaded package.
  • Local modules are read in place. After editing a ./ module you re-download nothing; the next plan sees the change. "Run terraform get again to fetch the new copy" is the wrong answer for a local source.

Module source categories

AspectLocal pathRegistry addressRemote package
Address form`./` or `../` prefix`[HOST/]NAMESPACE/NAME/PROVIDER``git::`, `github.com/`, `s3::`, `gcs::`, archive URL
How Terraform retrieves itReads files in place on diskDownloads via the registry protocolDownloads and extracts the whole package
`version` argumentNot supportedSupported (full versioning)Not supported; pin with Git `?ref=`
Sub-directory via `//`Not applicableNot applicable`//<subdir>` after the package

Decision tree

Module files on local disk?Local path./ or ../YesNoPublished to a registry?Registry addressNAMESPACE/NAME/PROVIDERYesNoIn a Git repository?git:: or github.com/?ref= pins a revisionYesNos3:: gcs:: archive URLdownload an archive

Sharp facts the exam loves — give these one last read before exam day.

Cheat sheet

Sharp facts the exam loves — scan these before test day.

Local paths must begin with ./ or ../

A local path module source must begin with either ./ or ../ so Terraform recognizes it as a filesystem path rather than a registry address; for example source = "./modules/vpc". A bare relative name like modules/vpc is interpreted as a registry address, not a local path.

Trap Believing a bare relative path such as modules/vpc (without a leading ./) loads a local directory.

5 questions test this
Local modules are loaded in place, not downloaded

Terraform treats a local path module as part of the same package as the calling configuration and loads it directly from disk instead of fetching a cached copy. Edits to a local module take effect on the next plan or apply without re-running terraform get.

Trap Assuming local modules are copied into .terraform/modules and must be re-downloaded after every edit.

4 questions test this
The source value must be a literal string

The source argument must be a literal string and cannot contain variables, expressions, or interpolation, because module sources are resolved during terraform init before input variables are evaluated. This applies to every source type, local or remote.

Trap Trying to build a source dynamically, e.g. source = "./modules/${var.name}".

3 questions test this
Public registry address is //

A public Terraform Registry module is addressed with the three-part form <NAMESPACE>/<NAME>/<PROVIDER>, for example source = "hashicorp/consul/aws". The final PROVIDER segment is required, not optional.

Trap Thinking a registry address is only / and omitting the trailing provider segment.

5 questions test this
Private registry addresses add a host prefix

A module in a private registry is addressed by prepending the registry hostname to the standard address: <HOST>/<NAMESPACE>/<NAME>/<PROVIDER>, for example source = "app.terraform.io/example-corp/vpc/aws". Public registry modules omit the host.

Trap Using a git:: URL for a private-registry module instead of the host/namespace/name/provider address.

7 questions test this
HCP Terraform private module registry keeps modules confidential and versioned

A private module registry in HCP Terraform shares modules confidentially within an organization while still supporting Terraform's semantic version constraints and providing a browsable directory of published modules. A plain Git URL provides sharing but not the browsable versioned catalog.

Trap Assuming the public Terraform Registry can host organization-confidential modules.

5 questions test this
Git and GitHub module sources

Terraform installs modules from generic Git repositories with the git:: prefix (over HTTPS or SSH), and from GitHub with the shorthand source = "github.com/org/repo". Bitbucket URLs are also detected automatically.

Trap Believing every remote source needs the git:: prefix, even the github.com/ shorthand.

6 questions test this
S3, GCS, and HTTP archive sources

Terraform can source modules from object storage using the s3:: prefix (e.g. s3::https://s3-eu-west-1.amazonaws.com/bucket/vpc.zip) or the gcs:: prefix, and from plain HTTP URLs that point to a .zip archive. These sources fetch a compressed archive rather than cloning a repository.

Trap Thinking S3/GCS sources support a version argument like registry modules do.

7 questions test this
Double-slash selects a sub-directory within a package

For sources that fetch a whole package (Git, archive, object storage), the // double-slash selects a module in a sub-directory, for example git::https://example.com/network.git//modules/vpc. Everything before // identifies the package; everything after is the path inside it.

References

  1. Module block reference: source argument and source types
  2. Module block reference (Terraform v1.12): literal source value
  3. Use modules in your configuration
  4. HCP Terraform private registry