I scanned 244 public repositories to find every consumer of one Terraform module. 149 of them declare it. Eight are still pinned to git tags from the 0.3 and 0.5 series, and nothing in that organisation records it.
Somebody once wrote ?ref=tags/0.3.1 into a main.tf and moved on. It is still there. Seven other repositories in the same organisation carry a line of the same shape, pinned to git tags from the 0.3 and 0.5 series, and whatever reason each of those people had for it now lives nowhere but the line itself.
I know that because on 7 August 2026 I scanned the public cloudposse GitHub organisation, all 244 repositories, and ranked its Terraform modules by consumer count. Top of the list is cloudposse/terraform-null-label, declared by 149 of them across six distinct version pins. 141 sit on 0.25.0, the current release. The other eight are the ones above, and nothing in the organisation records that they lag.
Finding every consumer of a Terraform module is a parsing problem rather than a search problem: every tool that already answers it answers only inside its own boundary, and module source values come in four structurally different formats that no single search pattern matches. That is how 149 of the 244 repositories in the public cloudposse organisation declare one module while nothing anywhere holds the list.
When the breaking change is yours to ship, the question is which repos depend on this module and at which version. It should be a five-second lookup. The reason it is not sits in the source attribute:
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 a tag. Some track main. Some pull the module from an internal registry, where the string looks nothing like a git URL. Some use Terragrunt, where the source is not in a .tf file at all. You need all of them before you merge, not after.
What existing tools give you (and where they stop)
Every tool that already answers "which repos consume this module" answers it inside its own boundary, and the boundary is rarely the same shape as your estate.
Explorer in HCP Terraform
Explorer, inside HCP Terraform, is the closest thing to a native answer, and it has got better since I first wrote this post. HashiCorp's own name for the feature is just Explorer, although plenty of people including me have called it Module Explorer: a modules view listing every module in use with its version and the workspaces on it, queryable through the /explorer API. On 14 April 2026, the same day this post first went up, HashiCorp's changelog recorded that "Explorer now surfaces remote modules sourced from GitHub, Bitbucket, S3, GCS, and other VCS or HTTP hosts alongside registry modules", where before it was effectively registry-only. HashiCorp also ships module lifecycle management, and its own docs are specific about what deprecating a module version does: it "adds warnings to the module's registry page and warnings in the run outputs of any users of that version". That is a genuinely good mechanism, and grep gives you nothing like it.
The boundary is in the word the API keeps using. Everything Explorer knows is scoped to workspaces registered in that HCP Terraform organisation, and what it returns is workspace-to-module: the list field in the response is workspaces. The workspace-to-repo join is yours to do, and a CLI-driven workspace may have no repository attached to join to. That boundary is a property of the category rather than of HashiCorp. Scalr's modules report is the same shape with the same edge, showing module usage across "all of your workspaces in the account."
Which is the strongest argument against this entire post. If your whole estate runs through one control plane, that platform's own modules report is your answer and you do not need this post. I keep writing it for the estates that are not one platform: the team still on Atlantis, the one running raw CLI against an S3 backend, the subsidiary acquired on a different tool. I have not measured how many estates look like that and I will not pretend a number exists. I have just never worked on one that did not.
Renovate
Renovate holds the mapping you want and cannot be asked for it. I mean the open-source bot almost everyone runs, not Mend's paid Enterprise server. Its Terraform manager detects module source blocks in .tf files and opens pull requests when a newer version is published, so it necessarily knows which repos use which modules. What it does not have is a queryable view of that. The dependency dashboard is an issue Renovate opens inside each repository, one per repo, not an org-wide list. It reacts after a version exists. Before you publish the breaking one, it will not tell you who is about to receive a PR.
Atlantis
Atlantis does what it was built for well. The plan and the apply land on the pull request, where the review already is. Its configuration model is scoped to that single repository, though, and its own docs say of the paths it watches, "The paths are relative to the project's directory." There is no built-in way to ask which repos across all projects call a given module.
Code search and grep
Code search is what most people try first, and it gets you most of the direct callers: type the module's source string into GitHub or GitLab code search, scoped to the org. At a couple of dozen repositories that is a perfectly good afternoon and I would not talk anybody out of it.
It stops in three places, all structural. It cannot connect a registry coordinate to the repository that produces the module, because that mapping is not in the file. It does not span a mixed GitHub and GitLab estate in one query. And it returns file hits rather than a consumer list with the pin attached, which cloning the org and running grep -r does not fix either.
Underneath all of it, search only ever finds the string you already knew to type. The module you have forgotten is load-bearing is exactly the one you will not think to search for.
Why this is harder than it looks
Terraform module sources are written in at least four structurally different ways, and each one needs its own 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 repository from the subpath inside it. The ?ref= carries the version pin. Grepping the module name finds this. Grepping the version does not, if the caller parameterises it through a local.
Registry sources, in two shapes.
# public registry shorthand
source = "cloudposse/label/null"
version = "0.25.0"
# internal registry, addressed by hostname
source = "registry.company.com/infra/networking/aws"
version = "~> 3.0"
Neither looks like a git URL and neither names a repository. The first is the shape most of those 149 repos use, and it maps back to a repo only through HashiCorp's terraform-<provider>-<name> publishing convention, which is a naming rule rather than anything written in the file. The second is harder: connecting registry.company.com/infra/networking/aws to a repository means knowing how your registry resolves coordinates, and that is out-of-band information no grep can recover.
Relative paths.
source = "../../modules/networking"
Common in monorepos and whenever a module calls a submodule, and a cross-repo grep for the module name never finds it. This is the one format where that is the right outcome: a relative source resolves against whatever filesystem layout surrounds the checkout, and that layout is knowledge the file does not contain, so there is no way to get from this line to a producing repository on the manifest alone. An org-wide consumer search is right to treat it as internal structure rather than a missing dependency.
Terragrunt.
Terragrunt wraps Terraform and declares the source in a terragrunt.hcl file rather than a .tf file:
terraform {
source = "git::https://gitlab.company.com/infra/terraform-modules.git//networking?ref=v3.2.0"
}
That is still the canonical shape in August 2026, and the same block reference now documents a second one: orgs that have adopted Stacks declare unit { source = ... } blocks in a terragrunt.stack.hcl file, using the same URL schemes. A unit's source points at a Terragrunt unit rather than at the module directly, meaning a directory of Terragrunt configuration whose own terragrunt.hcl declares the module. Both are HCL. Neither is a .tf file. Any tool that only scans .tf files misses every Terragrunt consumer in the org.
Beyond source formats there is the transitive problem. A module that consumes your module is itself consumed, so a breaking change cascades past the repos you warned. A list of direct consumers is not the blast radius. It is the first layer of it.
Fan-in and depth are independent properties of a module
In a 7 August 2026 scan of the public cloudposse GitHub organisation, changing one Terraform module reaches 13 repositories over three hops: cloudposse/terraform-aws-iam-role is declared by terraform-aws-cloudwatch-logs, which is declared by terraform-aws-lambda-function, which is declared by terraform-aws-github-action-token-rotator. Every hop is a resolved Terraform source edge, and none of the repositories past the first declares the root module directly.
The two properties turn out to be independent, which I did not expect. terraform-null-label, the 149-consumer module, has zero second-order dependents: not one of them is itself consumed by a third repo in this graph. The org's biggest fan-in module is also its flattest. Fan-in tells you how many teams to warn. Depth tells you how far the change keeps travelling after you have warned them. A module can be heavy on one and empty on the other, and you cannot infer either from the other.
I wrote up the whole organisation at 242 repositories, and how it writes its version pins in a companion post. Nobody edited a catalog entry when it grew.
See it live
Every module consumer, in one pane
There is a public graph of this organisation on the site, built from a scan dated 7 July 2026, so its counts sit a little behind the ones above. The interaction is the point: click the terraform-null-label node and read the blast radius straight off the graph, no sign-in, instead of a search whose string you have to guess first.
What the full answer requires
Answering "who consumes this module" across an estate you do not fully control needs a system that:
- Scans every repo in the org, with no registration and no catalog to maintain
-
Reads both file types,
.tfandterragrunt.hcl -
Resolves each
sourceto a producing repo, git URLs and registry coordinates alike - Carries the version pin through to the answer
- Walks transitive edges, not just direct callers
- Re-runs on a schedule, because a snapshot is wrong by the next commit
Riftmap is where I have put that argument into code. One read-only token, every .tf file parsed, and each module block's source resolved back to the repository that produces it: git URLs including the //subdir path and the ?ref= pin, and public Terraform Registry shorthand where the namespace and HashiCorp's terraform-<provider>-<name> publishing convention line up with a repo in the org. Those two halves are worth separating, because this is where "parsed, not inferred" gets tested. The edge is declared, sitting in a manifest Terraform already executes, and read rather than guessed. The convention only supplies the address, and it is HashiCorp's published rule rather than a heuristic I invented. Transitive dependents come from a repo-level breadth-first search over those resolved edges, depth-labelled, up to 20 hops, above a confidence floor of 0.8 for how sure the resolver has to be of an edge before the walk crosses it. Scheduled rescans keep the graph current.
Two things it does not do today, and I would rather you read them here. Riftmap does not parse terragrunt.hcl, so every Terragrunt consumer in an org is invisible to it, and I am not going to name that gap for other tools and quietly exempt my own. It is planned, and additive rather than a new ecosystem, because Terragrunt hands the identical source string to Terraform's own module downloader. If you run Terragrunt and want to know when it lands, tell me. The other is the internal registry: a hostname-bearing source like registry.company.com/infra/networking/aws is not resolved to a producing repo today.
The list was already written, just nowhere you can read it
Every one of those 149 edges was written down before I scanned anything. 149 repositories declare that module in plain text, in a file the pipeline already runs, on a line somebody committed with a message. The organisation does not have a knowledge problem. It has a reading problem, and the gap is between what its repos say and what any one person can hold in their head.
That is also why the bus-factor version of this is so unglamorous. The dependency that lived only in the head of the engineer who left is not hidden and it is not lost. It is sitting in a source attribute in a repository nobody has opened in two years, and the only thing that went missing was the memory that it mattered.
A module's blast radius is not something anyone remembers. It is something you parse.
A few questions, answered directly
How do I find every consumer of a Terraform module? Parse every repository in the organisation for module blocks and resolve each source back to the repository that produces the module, because the source may be a git URL with a ?ref= pin, a registry coordinate like cloudposse/label/null, a relative path, or a terraform { source = ... } block inside a terragrunt.hcl file, and no one grep pattern matches all four. A full scan of the public cloudposse GitHub organisation on 7 August 2026 found one module declared by 149 of its 244 repositories, across six distinct version pins, 141 on 0.25.0 and eight on older git tags.
How do I find Terragrunt consumers of a Terraform module? Terragrunt declares module sources in a terragrunt.hcl file, inside a terraform { source = ... } block; Stacks adopters add a terragrunt.stack.hcl file whose unit { source = ... } blocks point at Terragrunt units, directories of Terragrunt configuration that declare the module in turn. Either way the edge is written in HCL that is not a .tf file, so a scanner that only reads .tf files misses every one of those consumers. The source strings themselves are identical to Terraform's, which makes this a file-pattern problem rather than a new ecosystem. Riftmap's own coverage, as of August 2026: git URLs resolve to the producing repo, including the //subdir path and the ?ref= pin, and public Terraform Registry shorthand resolves too, but only where the namespace and HashiCorp's terraform-<provider>-<name> convention line up with a repo the org has connected. terragrunt.hcl is not parsed yet, and that is planned. Hostname-bearing internal registry sources are not resolved to a producing repo, and relative paths are excluded by design: a relative path cannot be resolved to a producing repository from the file alone.
Can Renovate tell me which repos use my Terraform module? Not as a query, in the open-source bot almost everyone runs. Renovate detects module source blocks in .tf files and opens pull requests when a newer version is published, so it necessarily knows where to open them, but it surfaces that per repository through a dependency dashboard issue rather than as one org-wide view. Before publishing a breaking change you cannot ask it which repositories are about to receive a PR. Checked against Renovate's docs on 7 August 2026.
How do I check the blast radius of a Terraform module change before I merge it? Resolve the module to every repository that declares it, then walk the graph outward, because fan-in and depth are independent and neither predicts the other. In a 7 August 2026 scan of the public cloudposse organisation, the module with the most consumers, 149 of 244 repositories, had zero second-order dependents, while cloudposse/terraform-aws-iam-role reached 13 repositories over three hops. Counting direct callers tells you how many teams to warn; walking the edges tells you how far the change keeps going after that.
About Riftmap
Riftmap maps cross-repo dependencies across your entire GitLab or GitHub
organisation — Terraform, Docker, CI templates, Helm, and more. One read-only
token. No YAML to maintain.
This is the second post in the Find Every Consumer series. Previous post covers Docker base images. Next up: GitHub Actions workflows.
Top comments (0)