Skip to content
SRE & DevOpsDeep Dive Published Updated 8 min readViews unavailable

Terraform Module Sources: Pin Versions and Review Git References Deliberately

Control Terraform module source changes with exact registry versions or immutable Git commits, and understand why the provider lock file does not pin modules.

Terraform modules are executable infrastructure dependencies. A module can create, replace, or destroy resources, define outputs that other configuration consumes, and impose provider requirements on its callers. Its source reference is therefore part of the deployment decision, not just a way to avoid copying files.

Provider lock files make a selected provider version reproducible, but Terraform does not use that lock file to remember remote module version selections. A registry module uses its own version constraint. A Git module uses the revision selected by its source URL, which can be a branch, tag, or commit. If a team assumes that committing .terraform.lock.hcl freezes every dependency, a mutable module branch can still change beneath a plan.

Understand the source classes

A module source can refer to a local path, a public or private registry, a Git repository, or other supported package locations. The source type determines how Terraform installs it and which version controls are available. A local source changes with the checked-out repository. A registry source uses a module address and can include a version constraint. A Git source can use the repository’s default branch or a ref query parameter.

module "network" {
  source  = "terraform-aws-modules/vpc/aws"
  version = "6.0.1"
}

For registry modules, use a version constraint that matches the project’s release policy. A range such as >= 6.0, < 7.0 allows compatible releases to be considered when Terraform installs or upgrades the module. An exact version such as 6.0.1 makes the chosen release explicit. Choose deliberately between controlled patch uptake and strict reproducibility; do not leave a production module unconstrained merely because initialization succeeded once.

Module version constraints are not interchangeable with provider constraints. A root module can constrain provider versions in required_providers, while each registry module call has its own version argument. The dependency lock file currently tracks provider selections, not the chosen remote module release. Keep module source changes visible in configuration review and do not infer a pinned module from an unchanged lock file.

Registry modules: version constraints and upgrades

Terraform selects a registry module version that matches the module block’s version constraint. Once installed, the module source is placed in Terraform’s working directory cache. If the source or version changes, rerun terraform init so Terraform installs the requested module. A cached copy is not a dependency declaration and should not be committed as a substitute for the source address.

For a controlled upgrade, change the version constraint in a dedicated branch and initialize the configuration. Review the module’s release notes and source diff, then inspect the plan for changes to resource arguments, resource addresses, provider mappings, lifecycle behavior, and outputs. A module update can change infrastructure behavior even if the root module’s own .tf files are untouched.

Do not treat a semantic-version range as proof that every version in the range is operationally interchangeable. Read the publisher’s compatibility policy and test the candidate release. A wide range may cause a new checkout to select a later release than a previous checkout. A narrow constraint may delay fixes or conflict with another module. Use the lock file for provider version repeatability and the module block for module version policy.

Git references: branches, tags, and commits

Git module sources can include a ref query parameter. A branch tracks a moving name; the same source address can resolve to different commits at different times. A tag is more human-readable but can be moved by a repository maintainer unless the repository enforces immutable tags. A full commit hash identifies a specific Git commit and is the most reviewable choice when exact source reproducibility matters.

module "database" {
  source = "git::https://github.com/example/terraform-database.git?ref=4f1d9c2a8b7e6d5c4a3b2f1e0d9c8b7a6f5e4d3c"
}

Use a commit hash that actually exists in the intended repository and verify it through an approved review process. The example hash is illustrative, not a real release. Do not copy it into a production configuration. A commit pin prevents ordinary branch movement from changing the selected code, but it does not prove that the code is correct, safe, or compatible with the provider versions in the root configuration.

When a module lives in a subdirectory of a larger repository, Terraform supports a package subdirectory in the source address. Keep the subdirectory and ref readable, and confirm that the selected commit contains the expected path. Do not build a source string from a plan-time value: module installation happens during initialization, before ordinary plan-time variables are available. Keep source selection static and reviewable.

Private sources and credentials

Private module registries and Git repositories require credentials, but those credentials should not be embedded in source URLs or committed Terraform files. Use the authentication mechanism supported by the registry, Git client, or CI runner. Avoid URL-embedded tokens because source addresses can appear in logs, process arguments, diagnostics, or repository history.

Keep separate identities for source retrieval and infrastructure operations. A read-only module-download identity should not automatically inherit permission to apply production infrastructure, and a cloud apply identity should not need permission to publish shared modules. In CI, configure credentials through the approved secret or workload identity mechanism, scope them to the minimum repository or registry, and prevent logs from exposing them.

For private modules, review who can publish a version or move a tag. Restrict write access to release maintainers, require review for release tags, retain release artifacts, and record the commit behind a published version. If a module source changes unexpectedly, preserve the installed source and logs before deleting the cache. Confirm the resolved repository, ref, commit, and source contents before another initialization.

Upgrade workflow that keeps plans attributable

Use a separate, explicit workflow for dependency upgrades:

  1. Select one module source or a small coordinated set and state the reason for the update.
  2. Change the source or version constraint in a branch and run terraform init to install it.
  3. Inspect the installed module source, release notes, provider requirements, and deprecations.
  4. Run formatting, validation, module tests, and a plan with the approved Terraform CLI and provider lock file.
  5. Compare the plan with the baseline and explain every create, update, replacement, and destroy.
  6. Apply only the reviewed saved plan under the normal approval process.

Do not regenerate a plan after approval and assume it is the same artifact. A saved plan binds a proposal to the configuration and dependency context used to create it. Protect saved plans because they can contain sensitive values, and discard them when the configuration or source inputs change.

For broad upgrades, split the work so that module-source changes are attributable. If a module upgrade also requires a provider-major-version update, make the coupled change explicit and test the combination. When the plan changes unrelated resources, identify whether the cause is the module, provider, refreshed remote data, or a changed CLI before applying.

Caches and reproducibility

Terraform’s module cache is a local installation detail. It can speed up repeated initialization, but it is not the authoritative record of which module version the configuration requests. A clean CI runner is useful because it demonstrates that the declared sources and credentials are sufficient to obtain the dependencies. If a build works only because a laptop cache contains an old module, the repository does not fully describe the deployment input.

Avoid running uncontrolled module upgrades on a production workspace. Keep dependency updates in a reviewed branch, initialize from the same source state that CI will use, and check the exact selected module version or Git revision in the initialization output. Keep source references out of generated files that CI silently rewrites after the reviewer approved a different change.

An exact registry version and a Git commit pin are useful but not complete supply-chain controls. They do not validate publisher identity, inspect malicious code, guarantee that a repository remains reachable, or establish that a module is safe to apply. Use source review, trusted registries, code ownership, version retention, provider constraints, dependency lock checksums, and policy review as complementary controls.

Diagnose unexpected module changes

When Terraform reports a module source change, verify the current working directory, branch, module address, version constraint, and ref. Run initialization from the root configuration that owns the module block. Check whether the module is a local path, registry entry, default branch, moving tag, or fixed commit. Confirm whether the change was intentional and whether the installed cache reflects the current configuration.

When a plan changes after a module update, do not attribute every difference to the module name alone. Compare source commits, module requirements, resource addresses, provider selections, refreshed data, and the exact plan. A change in a module output can propagate into root resources; a refactor can also use moved blocks to preserve addresses. Understand the graph before deciding whether a replacement is expected.

Use a clean checkout to reproduce the installation and plan. Keep the prior source revision accessible until the deployment is approved and rollback requirements are understood. If a registry publisher withdraws or replaces an artifact, preserve any trusted copy and escalate through the project’s dependency incident process rather than weakening version constraints to fetch “something that works.”

Reliable module sourcing requires an explicit policy for selection, review, authentication, and upgrades. Keep registry versions or Git refs in the configuration, understand what the provider lock file does not cover, and make the selected module revision visible in the same review that approves its infrastructure plan.

Related:

Sources:

Comments