You maintain a shared Terraform module. You need to make a breaking change. Which repos across your org actually source it — and at which version? Here's why the answer is harder than it should be.
You maintain an internal Terraform module. Maybe it's terraform-aws-vpc or modules/rds-cluster or a shared networking baseline that half the org builds on top of. It started as a convenience — one team wrote a good VPC module, others adopted it, and now it's load-bearing infrastructure for dozens of repos.
Then you need to change it. Maybe you're upgrading the AWS provider from v4 to v5 and the module interface needs to change. Maybe you're deprecating a variable that seemed like a good idea eighteen months ago. Maybe you're fixing a security misconfiguration and the fix requires consumers to pass a new required variable.
The question is simple: which repos across our org source this module, and at which version?
If you can't answer this, you're deploying blind. You either make the change and wait for terraform plan failures to trickle in across teams, or you don't make the change at all and let the tech debt compound. Neither option is good.
The scenario
Here's what this typically looks like. Your platform team publishes a module, and repos across the org consume it:
module "vpc" {
source = "git::https://gitlab.company.com/infra/terraform-aws-vpc.git?ref=v2.3.0"
cidr_block = "10.0.0.0/16"
environment = var.environment
}
Some repos pin to a specific Git tag. Some pin to a branch. Some point at main with no ref at all, which means they're consuming whatever happens to be at HEAD when they next run terraform init. Some source the module from your internal Terraform registry instead of Git. Some use a relative path because the module lives in a monorepo alongside the consuming root module.
You need to find all of them, understand which version each one uses, and figure out which teams to notify before you push the breaking change. Right now, most teams do this by grepping, searching the Terraform registry UI, or asking in Slack. None of these give you a complete answer.
What existing tools give you (and where they stop)
Several tools address pieces of the Terraform module consumer problem. Each one is useful in its own context. None of them answer the full question.
HCP Terraform (Terraform Cloud) Module Registry
If you use HCP Terraform or Terraform Enterprise, the private module registry tracks which workspaces consume a published module. The registry UI shows a module's versions, usage statistics, and which workspaces reference it.
This works within the HCP Terraform ecosystem. The limitation is scope: it only sees modules published to its registry and consumed by its workspaces. If a team sources the module directly via a Git URL — which is extremely common, especially in orgs that adopted Terraform before the private registry existed — that consumption is invisible. If some repos use HCP Terraform and others use a different backend (local state, S3, Spacelift, env0, Atlantis), you're seeing a partial picture. It's also worth noting that the HCP Terraform free tier was discontinued in March 2026, so this option now requires a paid plan.
Spacelift / Scalr / env0
Spacelift has the most advanced module consumer tracking of the orchestration platforms. Its module registry tracks which stacks consume each module version, and trigger policies can automatically kick off runs on consumer stacks when a new module version is published. Scalr offers a modules report that shows which modules and versions are used across all workspaces, including which workspaces are running outdated versions. env0 has similar workspace-level dependency awareness.
These are genuinely useful capabilities — if all your Terraform runs flow through a single platform. The limitation is the same for all of them: they only see what's inside their own ecosystem. If your org uses Spacelift for production infrastructure but engineers run terraform plan locally for development, or if one team uses Atlantis while another uses GitHub Actions with raw terraform commands, no single orchestrator has the full picture. And none of them see the module source references in the code — they see module consumption at the workspace execution level, which is a different layer.
terraform providers and terraform graph
Terraform's built-in terraform graph command outputs a DOT-format dependency graph for a single root module. terraform providers lists the providers required by a configuration and its modules.
These commands work per-configuration, not per-org. They answer "what does this root module depend on?" They don't answer "which root modules across my org depend on that shared module?" You'd need to clone every repo, run the command in every Terraform root, and aggregate the results — which is essentially building a custom scanner.
Grep / GitHub code search
The most common approach. Clone everything (or use GitHub's code search), and search for the module source URL:
grep -r "terraform-aws-vpc" --include="*.tf" ~/repos/
This finds direct string matches. It works for a one-off audit. But it breaks down in several ways that matter:
- It doesn't resolve registry sources.
source = "app.terraform.io/company/vpc/aws"andsource = "git::https://gitlab.company.com/infra/terraform-aws-vpc.git"might reference the same module — grep treats them as unrelated strings. - It doesn't extract versions. You can see that a repo references the module, but parsing out
?ref=v2.3.0or theversion = "~> 2.0"constraint from a registry source requires additional work per match. - The results are stale immediately. The search reflects the state of whatever you cloned. By tomorrow, three repos might have changed their module references.
- At 100+ repos, it takes long enough that nobody does it proactively. It becomes an incident response activity, not a planning tool.
Renovate
Renovate understands Terraform module sources and can open pull requests when a new version is available. It parses source and version attributes in .tf files and supports Git refs, registry modules, and GitHub releases.
Like with Docker images, Renovate implicitly knows who consumes what — it's configured per-repo and has parsed the module sources. But it doesn't expose this as a queryable view. You can't ask Renovate "which repos in my org source terraform-aws-vpc?" It's a dependency updater, not a dependency mapper. It reacts after a new version exists. It doesn't give you the blast radius before you publish the new version.
Why this is harder than it looks
Terraform module sourcing is more complex than most people realise, because the same module can be referenced through at least five different source types — and each one looks completely different in the HCL.
Git sources are the most common for internal modules:
module "vpc" {
source = "git::https://gitlab.company.com/infra/terraform-aws-vpc.git?ref=v2.3.0"
}
The URL might use HTTPS or SSH. The ref might be a tag, a branch, or a commit SHA. Some teams use the git:: prefix, others rely on Terraform's URL inference. The same module can appear as:
source = "git::https://gitlab.company.com/infra/terraform-aws-vpc.git?ref=v2.3.0"
source = "git@gitlab.company.com:infra/terraform-aws-vpc.git?ref=v2.3.0"
source = "gitlab.company.com/infra/terraform-aws-vpc?ref=v2.3.0"
These all point at the same module. A grep for one form misses the others.
Registry sources use a completely different syntax:
module "vpc" {
source = "app.terraform.io/company/vpc/aws"
version = "~> 2.0"
}
This references the same underlying module, but through the registry. The version constraint is in a separate attribute, not in the source URL. Matching this to the Git-sourced references requires knowing that the registry module company/vpc/aws maps to the Git repo infra/terraform-aws-vpc.
Local paths appear when modules live alongside root configurations:
module "vpc" {
source = "../../modules/vpc"
}
This is a dependency on an internal module, but there's no URL to grep for. The relationship is purely structural — you need to resolve the relative path within the repo's directory tree.
GitHub/GitLab shorthand sources add another variant:
module "vpc" {
source = "github.com/company/terraform-aws-vpc?ref=v2.3.0"
}
Terraform interprets this as a Git clone from GitHub, but the syntax differs from both the explicit git:: form and the registry form.
Subdirectory references complicate things further:
module "vpc_endpoints" {
source = "git::https://gitlab.company.com/infra/terraform-aws-vpc.git//modules/endpoints?ref=v2.3.0"
}
The // separator means "clone this repo, then use the modules/endpoints subdirectory." This is a dependency on the same repo but a different module path within it. A consumer tracking system needs to handle both the repo-level relationship and the subpath.
Then there's the transitive problem. Module A sources module B, which sources module C. If you change module C, both B and A are affected — but A never mentions C anywhere in its code. Understanding the full blast radius requires resolving the entire chain, not just direct consumers.
None of the tools in the previous section handle all of these source types and resolve them to a unified view. This is why teams keep falling back to grep and tribal knowledge — and why both keep failing at scale.
What the full answer requires
To reliably answer "who consumes this Terraform module," you need a system that:
- Scans every repo in the org, not just those registered in a specific orchestration platform
- Parses all five source types — Git URLs (HTTPS and SSH), registry sources, local paths, GitHub/GitLab shorthand, and subdirectory references — and normalises them to a single identity per module
-
Extracts version constraints from both
?ref=parameters in Git sources andversionattributes in registry sources - Resolves transitive dependencies so you can see not just direct consumers but the full downstream blast radius
- Keeps the graph current through scheduled or event-triggered rescans, not one-off audits
-
Makes the result queryable: "show me every consumer of
terraform-aws-vpc, grouped by version, with the team that owns each consuming repo"
This is one of the specific problems Riftmap is built to solve. It scans a GitLab or GitHub org, parses every .tf file across every repo, resolves all five module source types to a unified dependency graph, and lets you click on any module to see every consumer — direct and transitive — with the version each one pins to.
The result: before you push the breaking change, you open the graph, click the module, and see exactly which repos and teams are affected. You know who to notify. You know who's still on v1.x and who's already on v2.x. You know which repos pin to main and will break immediately versus which pin to a tag and have time to migrate.
No grepping. No Slack polling. No "let's just push it and see who complains."
How is your team solving this today? I'd genuinely like to know — drop a comment or find me at riftmap.dev.
Top comments (0)