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.
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.
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 examples3::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
GETwithterraform-get=1, and expects the service to return the real source in anX-Terraform-Getheader or aterraform-getmeta 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.
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/awsis correct. versionis registry-only. A stem that putsversion = "1.2.0"on agit::or local source is wrong. Pin a Git source with?ref=v1.2.0; a local path cannot be pinned at all.sourceis 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 readmodules/vpcfrom a repository, the answer isnetwork.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 nextplansees the change. "Runterraform getagain to fetch the new copy" is the wrong answer for a local source.
Module source categories
| Aspect | Local path | Registry address | Remote package |
|---|---|---|---|
| Address form | `./` or `../` prefix | `[HOST/]NAMESPACE/NAME/PROVIDER` | `git::`, `github.com/`, `s3::`, `gcs::`, archive URL |
| How Terraform retrieves it | Reads files in place on disk | Downloads via the registry protocol | Downloads and extracts the whole package |
| `version` argument | Not supported | Supported (full versioning) | Not supported; pin with Git `?ref=` |
| Sub-directory via `//` | Not applicable | Not applicable | `//<subdir>` after the package |
Decision tree
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 examplesource = "./modules/vpc". A bare relative name likemodules/vpcis 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
- A shared `modules/common` directory sits one level ABOVE a service's root configuration on disk. From that root module, an engineer wants the `source` argument to reference the shared directory as a l
- An engineer adds a `module "network"` block whose child code lives in a `modules/network` subdirectory of the same configuration, and writes `source = "modules/network"` with no leading `./`. After ru
- An engineer is reviewing four `module` blocks in one configuration and wants to identify the single block that Terraform will load as a LOCAL directory on disk rather than fetch from a remote location
- A platform team keeps a child module in a `./modules/app` directory committed to the same repository as the root configuration that calls it. A new engineer, used to modules pulled from the public reg
- After writing `source = "network"` for a module whose code lives in a `network` subfolder beside the root configuration, an engineer sees `terraform init` fail while trying to find the module in a reg
- 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
- A platform engineer edits a resource inside a local child module referenced by `source = "./modules/vpc"`, then runs `terraform plan` from the root module without running `terraform get` again. A team
- A teammate wants to pin `source = "./modules/logging"` to a specific release by adding a `version` argument to the module block, exactly as they do for modules pulled from a registry. According to Ter
- A root configuration calls two child modules: one with `source = "./modules/db"` and another with `source = "terraform-aws-modules/vpc/aws"` from the public registry. After `terraform init` finishes s
- A platform team keeps a child module in a `./modules/app` directory committed to the same repository as the root configuration that calls it. A new engineer, used to modules pulled from the public reg
- The source value must be a literal string
The
sourceargument must be a literal string and cannot contain variables, expressions, or interpolation, because module sources are resolved duringterraform initbefore 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
- To keep module blocks DRY, an engineer defines a local value `local.module_root = "./modules"` and then writes `source = "${local.module_root}/network"`, expecting Terraform to assemble the path from
- To reuse a single module block across several environments, a developer writes `source = "./modules/${var.env}"`, expecting Terraform to assemble the local path from an input variable when the plan ru
- During a code review, a colleague asks why Terraform refuses to let the `source` argument reference an input variable even though other arguments in the same module block accept variables freely. Whic
- Public registry address is //
A public Terraform Registry module is addressed with the three-part form
<NAMESPACE>/<NAME>/<PROVIDER>, for examplesource = "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
- During a code review, a teammate claims that the public registry address in the `source` value `terraform-aws-modules/vpc/aws` only needs the namespace and module name, and that the final `aws` segmen
- A platform engineer is adding a module block that pulls HashiCorp's official Consul module for Amazon Web Services directly from the public Terraform Registry, with no hostname prefix. Which value for
- An engineer copies a module block but accidentally leaves the `source` value as `terraform-aws-modules/vpc` (only two segments, with the provider part missing), and then runs `terraform init` to insta
- An engineer who has only ever consumed modules from the public Terraform Registry now needs to reference a module stored in the company's HCP Terraform private module registry. Compared with a public
- While reviewing module blocks, an engineer wants to identify the entry that sources a module from the PUBLIC Terraform Registry, which uses an address with no hostname prefix. Which of the following `
- 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 examplesource = "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
- Your organization already stores every Terraform module in Git repositories that teammates reference by URL. A lead asks what the HCP Terraform private module registry would add on top of simply shari
- An organization publishes several internal modules to its HCP Terraform private module registry. A newly onboarded engineer asks how teammates find out which modules and versions exist and whether any
- While reviewing two module blocks, an engineer sees one with `source = "hashicorp/vault/aws"` and another with `source = "app.terraform.io/acme-corp/vault/aws"`. The modules are otherwise equivalent.
- A team has published a `vpc` module to their organization `example-corp` in the HCP Terraform private module registry, reached at the SaaS host `app.terraform.io`, and the module targets AWS. In a new
- An engineer who has only ever consumed modules from the public Terraform Registry now needs to reference a module stored in the company's HCP Terraform private module registry. Compared with a public
- While reviewing module blocks, an engineer wants to identify the entry that sources a module from the PUBLIC Terraform Registry, which uses an address with no hostname prefix. Which of the following `
- A `vpc` module is published in the organization's HCP Terraform private registry as `app.terraform.io/example-corp/vpc/aws`. Instead of that address, an engineer sets the `source` value to `git::https
- 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
- Your organization already stores every Terraform module in Git repositories that teammates reference by URL. A lead asks what the HCP Terraform private module registry would add on top of simply shari
- An organization publishes several internal modules to its HCP Terraform private module registry. A newly onboarded engineer asks how teammates find out which modules and versions exist and whether any
- An engineer proposes pushing the team's internal, company-only Terraform modules to the public Terraform Registry, reasoning that as long as the module names are not publicized, nobody outside the com
- A platform team must share a curated set of approved infrastructure modules across their whole organization. The modules must stay confidential to the company, honor Terraform's semantic version const
- A `vpc` module is published in the organization's HCP Terraform private registry as `app.terraform.io/example-corp/vpc/aws`. Instead of that address, an engineer sets the `source` value to `git::https
- 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 shorthandsource = "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
- A configuration must have Terraform clone a private module repository over SSH, using key-based authentication, from a generic Git server at example.com under the path /storage.git. Which source value
- Your team keeps a Terraform module on a self-hosted GitLab server, reachable at the URL https://git.internal.example.com/infra/network.git, and you need Terraform to clone that generic HTTPS Git URL w
- A platform engineer packages a reusable networking module as a compressed .zip archive and uploads it to an S3 bucket, then sets source = "s3::https://s3.amazonaws.com/acme-tf-modules/networking.zip"
- A platform engineer is adding a networking module that lives in a public GitHub repository at github.com/acme/terraform-aws-vpc to a root configuration. She wants Terraform to install it using the bui
- An engineer sources a module from a generic Git repository at git::https://example.com/network.git and must have Terraform check out the release tagged v2.4.0 instead of the default branch. Which chan
- Your organization keeps a Terraform module in a public Bitbucket repository at bitbucket.org/acme/terraform-consul. A teammate claims you must place a git:: prefix in front of the URL before Terraform
- 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 thegcs::prefix, and from plain HTTP URLs that point to a.ziparchive. 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
- An engineer sources a module with source = "s3::https://s3.amazonaws.com/corp-modules/network.zip" and wants to pin it to release 3.1.0, so they add version = "3.1.0" to the same module block. What ha
- A module is published as a downloadable zip archive at the plain URL https://modules.example.com/vpc-module.zip, served over standard HTTPS with no S3 or Git backing. If a module block sets source = "
- A team publishes their VPC module as a zip archive to a private S3 bucket, retrievable at https://s3-eu-west-1.amazonaws.com/examplecorp-terraform-modules/vpc.zip only with the team's AWS credentials.
- A single archive stored in S3 at corp-modules/platform.zip bundles several modules in separate folders, and you need Terraform to install only the module located in the networking/vpc subdirectory ins
- A platform engineer packages a reusable networking module as a compressed .zip archive and uploads it to an S3 bucket, then sets source = "s3::https://s3.amazonaws.com/acme-tf-modules/networking.zip"
- Your platform team stores a compressed Terraform module archive in a Google Cloud Storage bucket and wants Terraform to fetch it with the dedicated object-storage fetcher for Google Cloud rather than
- An engineer sources a module from a generic Git repository at git::https://example.com/network.git and must have Terraform check out the release tagged v2.4.0 instead of the default branch. Which chan
- 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 examplegit::https://example.com/network.git//modules/vpc. Everything before//identifies the package; everything after is the path inside it.