See it as a diagram
Everything below, as a diagram you can edit. Describe yours and see it in seconds.
No account needed · Editable canvas, not a picture
The lock file does not cover modules
The .terraform.lock.hcl file tracks provider dependencies with checksums, and the Terraform documentation states the gap outright: the dependency lock file tracks only provider dependencies, Terraform does not remember version selections for remote modules, and it will always select the newest available module version that meets the specified constraints. A registry module declared with ~> 5.0 therefore resolves afresh at every init, which means two engineers running init a month apart can be building different module code from an identical repository.
Git-sourced modules pin differently, in the source string itself, with a ?ref= pointing at a tag or a commit. A source without a ref tracks a branch, and a branch is a moving target. tflint ships a rule for exactly this, terraform_module_pinned_source, which flags a git or mercurial source with no version pin and, in its flexible style, also flags a ref pointing at a default branch such as main or master.
Transitive versions are the sharper edge. If a module you pin exactly itself calls a submodule with a range, your pin buys you less than it looks like. Checking that the modules you depend on pin their own children is part of the review, not a paranoid extra.
# floats: init picks the newest match, and the lock file will not stop it
module "vpc" {
source = "terraform-aws-modules/vpc/aws"
version = "~> 5.0"
}
# pinned: reproducible, upgraded on purpose
module "vpc" {
source = "terraform-aws-modules/vpc/aws"
version = "5.8.1"
}
# git source: the pin lives in the ref, and a missing ref means a branch
module "platform" {
source = "git::https://example.com/infra.git//modules/platform?ref=v2.3.0"
}Put the version on the box
A registry or git module cannot be expanded from your repository alone, because its contents are not there. Rather than pretending otherwise, Datadef draws each such call as one component labelled from the call itself, in the form Module terraform-aws-modules/vpc/[email protected], and treats it as a major node that never gets rolled into a group. Its input references still resolve, so the box wires to the rest of the diagram through what it consumes.
The version on the box is read from the literal version argument. A pin assembled from a variable or a local shows as a module with no declared version, which is the honest reading, and a call with no version argument at all shows the same way.
That single choice makes an upgrade review visual. A repository that composes five registry modules shows five versioned components, and the ones with no version declared stand out as the finding they are. Local modules, which the parser can expand, render instead as zones containing their actual resources.
The generated architecture doc carries the inventory side in table form: one row per module with its source and its resource count, where a registry module reads external because its contents are not in the repository. The versions live on the diagram nodes, the counts live in the table, and both come from a single parse, so they cannot disagree.
Composition edges come from outputs, not depends_on
The honest way to find which module depends on which is to follow the references. When module B is given module.a.storage_account_id as an input, that reference resolves through module A's output to the declared resource on the other end, which gives you a true edge and a label: the attribute that carried it.
Datadef resolves references that way rather than reading depends_on, following variables through call sites and locals to what they are wired to. An explicit depends_on is still recorded, but it is a coarse signal, because it states an ordering without saying what is actually consumed.
The practical payoff is upgrade blast radius. Before bumping a shared local module, the diagram shows every stack and module that consumes its outputs, which is the list of things to test. Version pins tell you what you are changing; reference edges tell you who feels it. Related reading: how modules render as zones.
FAQ
Does the Terraform lock file pin module versions?
How do I see which module versions my repository uses?
Should registry modules be pinned to an exact version?
How are dependencies between modules determined without running Terraform?
What does a diagram show for a module it cannot expand?