Module Sources and Versions

Scenario: A team's shared network module publishes a breaking change on its main branch, and every consumer whose module block still points at that mutable branch silently picks up the break on their very next init.

A module's source argument doesn't just say where the code lives - it also determines how safely you can control which version of that code you're actually running, and this is where teams most often get burned. A Terraform Registry source supports an explicit version constraint, exactly like the provider constraints you learned in Module 3, so you can pin to a compatible range and update deliberately. A VCS source (a Git repository, say) should always point at an immutable reference - a tagged release or a specific commit SHA - never a mutable branch name like main, because a branch can have new commits pushed to it at any time, and your next terraform init will silently pull in whatever the branch currently points to. Local path sources are simplest of all, but implicitly coupled to whatever version of your own repository you happen to have checked out.

One subtlety worth internalizing: the provider dependency lock file you met in Module 3 does not pin remote module versions the same rigorous way it pins providers - so for modules, disciplined source pinning in the module block itself is the primary safeguard, not something the lock file will catch for you. And remember that a module isn't inert documentation - when you call it, its resource blocks execute with your caller's provider credentials, so pulling in and running an unreviewed third-party module is functionally similar to running unreviewed code with real infrastructure permissions.

Analogy: A module source is a software dependency that arrives with real authority to create, modify, and destroy your infrastructure the moment you call it. Pinning it to a specific reviewed version is choosing a specific, already-inspected edition of a product; leaving it unpinned against a mutable branch is more like grabbing whatever happens to be sitting on the shelf the next time you walk in - which might be a completely different, untested product by then.

A worked configuration

module "network" {
  source  = "app.terraform.io/platform/network/fakecloud"
  version = "~> 3.2"
  cidr    = var.cidr
}

For discoverability and governance at scale, many organizations run an internal module registry or a curated allow-list of approved sources, rather than letting every team pull arbitrary third-party modules directly. Whenever you do intentionally upgrade a module version, record upgrade notes, a test plan, and any required state migration steps right alongside the change - a module version bump deserves the same rigor you'd give an application library upgrade that ships its own database migration.

Common mistakes in production

Goal: Treat the plan as a change contract: understand every create, update, replacement, and destroy before approving it. Reinforce this lesson in the tf-version-lockfile Terraform lab. Open /labs/terraform and choose slug tf-version-lockfile; the lab runs real Terraform against the offline FakeCloud provider.