You maintain a shared Terraform module. A breaking change is coming. Which repos will it hit?
You own terraform-modules/networking. Or infra/modules/vpc. Or a module published to your internal Terraform registry. Dozens of teams call it. Some pin to a specific version. Some track a branch. Some you haven’t heard from in months.
Now you need to remove a variable, rename an output, or restructure the resource hierarchy. The question is the same one it always is: which repos across the org depend on this module, and at which version?
This should be a five-second lookup. It isn’t.
The scenario
Your platform team publishes a shared Terraform module. Across the org, other repos call it in their root modules or in other shared modules:
module "vpc" {
source = "git::https://gitlab.company.com/infra/terraform-modules.git//networking?ref=v3.2.0"
cidr = "10.0.0.0/16"
regions = ["eu-west-1"]
}
Some teams pin to a tag like v3.2.0. Some track main. Some reference a specific commit SHA for reproducibility. Some pull the module from your internal Terraform registry. Some use Terragrunt, where the source is declared in a terragrunt.hcl file rather than a .tf file at all.
You need to find all of them before you merge that breaking change — not after.
What existing tools give you (and where they stop)
HCP Terraform Explorer
HashiCorp Cloud Platform’s Module Explorer is the closest thing to a native solution. It builds a graph of module relationships across your HCP Terraform workspaces, showing which workspaces call which modules and at which version.
The limitation is the boundary: it only knows about workspaces registered in HCP Terraform. If your org runs Atlantis, the open-source Terraform runner, or manages state in S3 without using HCP Terraform at all, Module Explorer has no visibility into those workspaces. It also maps workspace-to-module relationships, not source-repo-to-module relationships — which means you know which HCP workspace is affected, but not which engineering team owns it or which Git repo to open a PR against.
Renovate
Renovate can detect module source blocks in .tf files and open pull requests when a newer version is published to a registry. In that sense, it implicitly knows which repos use which modules — it has to, in order to know where to open PRs.
But Renovate doesn’t surface this as a queryable view. You can’t ask it: “before I publish this breaking version, show me every repo that will receive a PR.” It reacts after a new version appears. The blast radius before you publish is not something it answers.
Atlantis
Atlantis tracks which repos it manages and which Terraform workspaces they contain, but it operates per pull request. It applies plans and shows you the output for a given PR, not a cross-repo dependency graph. There’s no built-in way to query “which repos across all projects call this module.”
Grep
The fallback. Clone every repo, run grep -r "terraform-modules.git//networking", collect results. It works once, the results are stale by the next commit, and it misses the cases described below.
Why this is harder than it looks
Terraform modules can be referenced in at least four structurally different ways, each of which requires a different parsing strategy.
Git sources with subpaths and refs:
source = "git::https://gitlab.company.com/infra/terraform-modules.git//networking/vpc?ref=v3.2.0"
The // separates the repo from the subpath. The ?ref= is the version pin. Grepping for the module name finds this. Grepping for the version doesn’t, if someone parameterises it with a local variable.
Internal Terraform registry sources:
source = "registry.company.com/infra/networking/aws"
version = "~> 3.0"
This looks nothing like a Git URL. It’s a registry source with a semver constraint. Connecting this back to a Git repo requires knowing how your registry resolves module sources — which is out-of-band information no grep can recover.
Relative paths between modules:
source = "../../modules/networking"
Common in monorepos or when a module calls a sub-module. Resolving this dependency requires knowing the directory structure of the calling repo and normalising the path. A cross-repo grep for the module name won’t find this at all.
Terragrunt:
Terragrunt wraps Terraform and declares module sources in terragrunt.hcl instead of .tf files:
terraform {
source = "git::https://gitlab.company.com/infra/terraform-modules.git//networking?ref=v3.2.0"
}
This is syntactically similar to HCL but lives in a different file entirely. Any tool that only scans .tf files misses every Terragrunt-based consumer.
Beyond source formats, there’s the transitive dependency problem. If Module A depends on your networking module, and Module B depends on Module A, a breaking change to your module cascades through two layers. A list of direct consumers isn’t enough if you need to know the full blast radius before shipping.
What the full answer requires
To reliably answer “who consumes this module,” you need a system that:
- Scans every repo in the org without requiring registration or manual catalog entries
- Parses
.tfandterragrunt.hclfiles so consumers of either style are captured - Resolves all source formats — Git URLs, registry sources, and relative paths — back to an identifiable module identity
- Tracks version pins and constraints so you know which consumers are already on the new version and which still need updating
- Follows transitive edges so you can see second- and third-order dependents, not just direct callers
- Keeps results current through scheduled rescans rather than a one-time snapshot
This is one of the specific problems Riftmap is built to solve. It connects to your GitLab or GitHub organisation, clones every repo, and runs static analysis across .tf files, terragrunt.hcl files, and CI configs. It resolves module sources against your org’s internal registry, your Git hosting, and public registries, then builds a cross-repo dependency graph you can query by module.
The result is the same kind of view described in the Docker base image post in this series: click on the module in the graph, see every repo that depends on it, what version each one uses, and who owns it. Direct consumers and transitive dependents, not just a grepped list.
If you’re making a change to a shared Terraform module and want to know the blast radius before you merge, that’s the answer the tooling currently lacks — and what this graph makes available without a manual trawl through hundreds of repos.
You can try it at riftmap.dev, or reach me at [email protected] if you want to talk through your setup.
This is the second post in the Find Every Consumer series. Previous post covers Docker base images. Next up: GitHub Actions workflows.