linuxlab.io
Tutorials▾
  • Linux & networking
    File system, processes, TCP/IP, BGP and OSPF
    →
  • Terraform & IaC
    HCL, state, plan/apply on a LocalStack sandbox
    →
  • Git & GitHub
    Object model, plumbing, branching, GitHub Actions
    →
All tutorials →
PricingAboutSign inCreate account
/
Intro
Lessons
Footer
linuxlab-TutorialsPricingAboutPrivacy & cookies
Copyright © 2026 LinuxLab. All rights reserved.
linuxlab.io
Tutorials▾
  • Linux & networking
    File system, processes, TCP/IP, BGP and OSPF
    →
  • Terraform & IaC
    HCL, state, plan/apply on a LocalStack sandbox
    →
  • Git & GitHub
    Object model, plumbing, branching, GitHub Actions
    →
All tutorials →
PricingAboutSign inCreate account
/
  • Introduction
  • Lessons
  • How it works
  • Knowledge base
  • Cheat sheet
  • Capstone
  • Interview prep
home/terraform/kb/Terraform basics/tf-init-modules

kb/core ── Terraform basics ── beginner

How terraform init pulls in modules

When your HCL has a module block, terraform init downloads the module source (from a registry, git, or a local folder) into .terraform/modules/. This course does not write modules; this article is an overview of how the mechanics work.

view as markdownaka: terraform-modules-init, module-installation

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:

hcl
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)

hcl
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

hcl
source = "git::https://github.com/myorg/tf-modules.git//vpc?ref=v1.2.3"
  • git:: is the scheme prefix.
  • //vpc is the path inside the repo (after the double slash).
  • ?ref=v1.2.3 is a tag, branch, or commit.

The version field does not work for git sources; the version is set through ?ref=.

Local path

hcl
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

  1. Parses the HCL and finds every module "..." { source = ... } block, including modules inside modules (recursive).
  2. For each source, decides whether it needs to download (registry, git, http: yes; local path: no).
  3. Downloads the source into the subfolder .terraform/modules/<key>/.
  4. 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

hcl
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 version is 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 every plan. If you change a local module, the changes are visible right away.
  • version does not work with git or local sources. If you are used to writing version in a registry module, then move to git and forget to remove version, 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 to init again so terraform forgets about B too.
  • terraform get is an old separate command that updated modules without a full init. Since 0.12+ it has been replaced by init -upgrade, but you still see it in older READMEs.

§ команды

bash
terraform init

Downloads modules if they are declared in HCL and not yet cached.

bash
terraform init -upgrade

Forces a re-download of modules, honoring version constraints (for the registry) and refs (for git).

bash
terraform get -update

An alternative to init -upgrade aimed at modules only, without re-pulling providers and the backend.

bash
cat .terraform/modules/modules.json | jq

Shows which modules are installed and from which sources. Useful when you are tracking down where a folder came from.

§ см. также

  • tf-initterraform init: the first command in any projectterraform init downloads the provider plugins (AWS, GCP, and so on), creates a lockfile that pins their versions, and prepares the working directory. Without it, neither plan nor apply will run.
  • tf-lockfile.terraform.lock.hcl: pinning provider versionsThe lockfile pins the exact provider versions and their hashes, so you and CI always run the same build. It is created on terraform init and updated through init -upgrade. Commit it to git.
  • tf-version-constraintsVersion constraints in Terraform: required_version and providersrequired_version pins which versions of terraform may run this code. required_providers.version does the same for providers. The pessimistic operator ~> 5.60 is the standard: it allows minor updates and blocks major ones.
  • hcl-syntaxHCL: the language you write Terraform inHCL (HashiCorp Configuration Language) is the language you use to describe the desired state of your infrastructure. It looks like JSON, but it is easier to read: you can write comments, variables, and loops.
Footer
linuxlab-
Copyright © 2026 LinuxLab. All rights reserved.
Tutorials
Pricing
About
Privacy & cookies