Skip to content

Module Sources and Versioning


Writing a module locally is only the beginning. Once a module becomes useful, you may want to reuse it from another configuration, keep it in Git, or consume a module published to a registry.

Terraform’s module source mechanism lets you do that without changing the basic module interface you already learned.

Local Modules

The simplest source is a local directory:

module "network" {
  source = "./modules/network"
}

Terraform reads the module configuration from that directory.

A common local structure is:

infrastructure/
├── main.tf
└── modules/
    └── network/
        ├── main.tf
        ├── variables.tf
        └── outputs.tf

Local modules are particularly useful when the module is developed together with the configuration that calls it.

They are also a good way to learn module design because there is very little infrastructure around the module itself.

Git Modules

A module can also come from a Git repository.

For example:

module "network" {
  source = "git::https://github.com/example/terraform-network.git"
}

The source tells Terraform where to retrieve the module.

You can also reference a Git ref:

module "network" {
  source = "git::https://github.com/example/terraform-network.git?ref=v1.2.0"
}

The ref identifies a Git reference such as a tag, branch, or commit.

For reusable infrastructure, a tag or commit is generally much easier to reason about than a moving branch because the reference identifies a particular point in the module’s history.

Do not treat this as a Git lesson. The important Terraform concept is that a remote module has a source address, and that source can identify a particular revision.

Terraform Registry Modules

Terraform also supports modules published to registries.

A registry-style module source can look like:

module "network" {
  source  = "example/network/aws"
  version = "1.2.0"
}

The exact source address identifies the module.

The version argument then selects a compatible module version when the source supports registry versioning.

This gives you a useful distinction:

source
  │
  ▼
Where does the module come from?

version
  │
  ▼
Which released module version should be used?

Do not confuse the two.

A source identifies the module location and identity. A version constraint selects a module release from a versioned module source.

Module Initialization

After adding or changing a module source, Terraform may need to download it.

Run:

terraform init

Terraform initializes the modules required by the configuration.

You will normally run initialization when first setting up a configuration and whenever module source requirements change.

After initialization, normal Terraform operations continue to work:

terraform plan
terraform apply

The important idea is that module installation is part of Terraform’s initialization process; you do not manually copy the module’s implementation into your root configuration.

Version Constraints

Suppose a registry module has releases:

1.0.0
1.1.0
1.2.0
2.0.0

A configuration might specify:

module "network" {
  source  = "example/network/aws"
  version = "~> 1.2"
}

The constraint communicates which versions Terraform may select.

Version constraints are valuable because modules are code. A module can change its behavior, its interface, or the infrastructure it creates.

Blindly accepting an arbitrary new release can therefore create surprises.

For production configurations, module versions should be selected deliberately.

Why Pinning Matters

Consider a configuration that always follows a moving Git branch:

module "network" {
  source = "git::https://github.com/example/terraform-network.git?ref=main"
}

Today, main might contain one implementation.

Tomorrow, it might contain another.

The same Terraform configuration can therefore retrieve different module code at different times.

That makes reproducibility harder.

A tagged revision is more predictable:

module "network" {
  source = "git::https://github.com/example/terraform-network.git?ref=v1.2.0"
}

Now the configuration explicitly selects a release.

A commit reference can provide an even more precise immutable point:

module "network" {
  source = "git::https://github.com/example/terraform-network.git?ref=0123456789abcdef"
}

The exact reference strategy depends on how your organization manages releases, but the principle remains:

Know which module implementation your configuration is consuming.

Updating A Module

When a module version changes, the change should be deliberate.

For a registry module, you might update:

version = "~> 1.2"

to a newer constraint.

For a Git module, you might change:

?ref=v1.2.0

to:

?ref=v1.3.0

Then reinitialize:

terraform init

Terraform can update the module installation based on the new source or version requirements.

After updating a module, inspect the result:

terraform plan

Do not assume that changing a module version is harmless. The module may contain resource changes that produce infrastructure changes.

Module Versions And Provider Versions Are Different

There are two versioning concepts that are easy to mix up.

A module can have a version:

module "network" {
  source  = "example/network/aws"
  version = "1.2.0"
}

The provider can have its own version constraint:

terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 6.0"
    }
  }
}

These are separate.

Module version
      │
      ▼
Which module implementation?

Provider version
      │
      ▼
Which provider implementation?

Updating one does not automatically mean updating the other.

This distinction becomes especially important when a module and the provider evolve independently.

Public And Private Modules

Modules do not have to be public.

A team can maintain modules in private source repositories or use a private registry.

The architectural idea remains the same:

Caller
  │
  ▼
Module Source
  │
  ▼
Versioned Module

The difference is who can access and publish the module.

You do not need to introduce private module infrastructure to understand the design. The important lesson is that module reuse can happen inside a team or organization just as it can happen across public projects.

Do Not Treat Latest As A Strategy

One of the easiest mistakes with reusable modules is assuming that the newest version must always be the correct version.

A module update can include:

  • new inputs
  • removed inputs
  • changed defaults
  • resource changes
  • behavior changes
  • breaking interface changes

A controlled update gives you a chance to inspect the change through terraform plan and verify that the new version behaves as expected.

Warning

A moving Git branch makes the module source change independently of your Terraform configuration. For important environments, use a deliberate version or revision strategy rather than casually following a moving branch.

Source And Versioning Form Part Of Reproducibility

A useful mental model is:

Module Source
     │
     ▼
Module Version / Revision
     │
     ▼
Module Implementation
     │
     ▼
Terraform Configuration

If the source or revision can silently change, the configuration is harder to reproduce.

This does not mean every module must be published to a registry. A local module can be perfectly appropriate.

It means that as modules become shared dependencies, you should know exactly what code each configuration is consuming.

A Practical Progression

Module usage often grows through a natural progression:

Local module
     │
     ▼
Shared Git module
     │
     ▼
Versioned Git releases
     │
     ▼
Registry module

You do not need to jump straight to the final stage.

Start with the simplest source that solves the problem. Introduce a stronger distribution and versioning mechanism when reuse across configurations makes it worthwhile.

The key distinction to remember is:

Source answers where the module comes from. Versioning answers which version or revision you intend to use.

That distinction becomes essential when a module moves from a local experiment into a shared infrastructure component.

Last updated on