What a module is (in short)
A module is a reusable folder of HCL. A module has its own input variables, its own outputs, and its own resources inside. In the root HCL you wire a module in like this:
module "vpc" {source = "terraform-aws-modules/vpc/aws"
version = "5.8.1"
name = "demo-vpc"
cidr = "10.0.0.0/16"
}
This course does not write its own modules in the beginner track; that is intermediate material. But it helps to know what init does when someone else's modules are present: sooner or later you will run into someone else's repo.
Module sources in the source field
The source field tells terraform where to get the source from. There
are several formats.
Terraform Registry (public or private)
source = "terraform-aws-modules/vpc/aws"
version = "5.8.1"
The format is OWNER/NAME/PROVIDER. Terraform goes to
registry.terraform.io, finds this module, and downloads the version.
Git repository
source = "git::https://github.com/myorg/tf-modules.git//vpc?ref=v1.2.3"
git::is the scheme prefix.//vpcis the path inside the repo (after the double slash).?ref=v1.2.3is a tag, branch, or commit.
The version field does not work for git sources; the version is
set through ?ref=.
Local path
source = "./modules/vpc"
source = "../shared/modules/iam"
This is a relative path from the file where the module block is
written. There is nothing to download; terraform just uses the folder
as is.
S3 / GCS / HTTP
Less common, but possible: source = "s3::https://s3.amazonaws.com/bucket/modules.zip".
The archive is downloaded and unpacked.
What terraform init does with modules
- Parses the HCL and finds every
module "..." { source = ... }block, including modules inside modules (recursive). - For each source, decides whether it needs to download (registry, git, http: yes; local path: no).
- Downloads the source into the subfolder
.terraform/modules/<key>/. - Writes the plan of "which module lives where" into
.terraform/modules/modules.json.
On later init runs with no changes, modules are not re-downloaded; the
cached copies are used. After you change source or version, you need
terraform get -update or init -upgrade.
Version constraints for registry modules
module "vpc" {source = "terraform-aws-modules/vpc/aws"
version = "~> 5.8"
}
The pessimistic operator (see tf-version-constraints) works the same
way as for providers: ~> 5.8 allows 5.8.x and 5.9.x, but not 6.0.
For git sources the version is set with a ref: ?ref=v1.2.3 (tag),
?ref=main (branch, but that is a bad idea in production), ?ref=abc123
(commit).
Why modules are not in the lockfile
tf-lockfile pins providers only. There is no "pinned version with a hash" for modules:
- From the registry, exactly the version named in
versionis downloaded. If that version were swapped out in the registry (it should not be, but in theory): there are no hashes, so terraform would not notice. - From git, the git ref itself does the hashing work. Use a commit SHA, not a branch, since that gives you determinism.
This is a known limitation. In critical projects, modules are often vendored into a monorepo or into a private registry with immutable tags.
Gotchas
source = "./modules/vpc"versus"modules/vpc": both work, but the first is explicit. Without the dot terraform still understands it, but IDEs and linters sometimes do not.- Local modules are NOT cached in
.terraform/modules/. They are read from the source path on everyplan. If you change a local module, the changes are visible right away. versiondoes not work with git or local sources. If you are used to writingversionin a registry module, then move to git and forget to removeversion, terraform does not fail, but it ignores the field.- Transitive modules. Module A pulls in module B internally. After
init, both end up in.terraform/modules/. Remove module A and you have toinitagain so terraform forgets about B too. terraform getis an old separate command that updated modules without a full init. Since 0.12+ it has been replaced byinit -upgrade, but you still see it in older READMEs.