cloudposse/terraform-null-label is a Terraform module that does nothing at runtime. It generates names and tags. Its own README calls it a “Terraform module designed to generate consistent names and tags for resources,” and that is the whole job. The kind of module nobody opens a PR against on a Tuesday and thinks twice about.

Open its Dependents tab on GitHub, the tab that is meant to tell you which repositories rely on a repository, and GitHub says: “We haven’t found any dependents for this repository yet.”

That is the single most-depended-on module in the entire Cloud Posse org, and the most obvious tool a sceptical reader would reach for reports zero. A Riftmap scan of all 242 Cloud Posse repos found that one module, terraform-null-label, is declared as a direct dependency by 147 of them, 61% of the org, nearly always on the same generated line of a file called context.tf. If you have read the earlier posts in this series, you have seen the 147 before: it is the corroborated landmark, not the news. The news is the graph around it. A drop from 147 consumers to 15 with nothing in between, two different centres, and a blast radius that fans across 61% of the org while staying one hop deep. That is the shape I want to plant before we go further.

This is the third org this series has taken apart, and it is the sharpest shape yet. The first two were a hub-and-spoke org and a loose federation. Cloud Posse is a keystone: hundreds of independent modules, one convention module holding up the majority of them, on a line most reviews never read.

The shape of the org at a glance

Across 242 repositories, Cloud Posse’s public org resolves to 491 cross-repo dependencies pointing at 125 distinct artifacts. All but seven of those 125 are Terraform modules. This is an org that talks to itself almost entirely in Terraform.

It is worth saying how thin that in-org signal is against everything Cloud Posse declares. Set beside those 491 resolved in-org edges are roughly 12,900 dependency declarations that resolve somewhere else entirely: the AWS provider, the public Terraform registry, upstream Docker images. That is a single aggregate figure, so read it as an order of magnitude rather than a precise ratio, but the order of magnitude is the point. Roughly twenty-six declarations pointing out of the org for every one that stays inside it. Cross-repo-within-the-org is the rare, interesting signal here, not the common one, which is exactly why nobody has it assembled in one place.

Cloud Posse is a real business, and the description that matters is theirs, not mine. Its GitHub org bio calls it a “DevOps Accelerator for AWS Infrastructure.” Its own docs describe running one test workflow “for all of our hundreds of terraform modules.” Its flagship non-module project, Atmos, is a “Terraform Orchestration Tool for DevOps” that composes those modules into deployable stacks. So the org is not one big system. It is hundreds of small, independent, publicly published terraform-aws-* modules that mostly do not import each other. Which makes the one that they all import stand out.

The graph view: a star with two different centres

Drawn as a graph, Cloud Posse is a star, and it has two different centres. One node that almost everything points at, and one node that points at almost everything. They are not the same repo.

The first centre is the supplier. terraform-null-label sits at 147 inbound edges, roughly ten times the next module’s count. Almost every module in the library reaches back to it. The second centre is the consumer. cloudposse/terraform-aws-components, Cloud Posse’s reference-architecture “components” catalog, declares 110 outbound cross-repo dependencies, about eleven times the next-busiest repo (cloudposse/.github, at 10). One repo supplies the org. A different repo assembles it. A supplier hub and a consumer hub, and reading a single repo’s clone shows you neither, because both facts only exist in the relationships between repos.

Of the 491 edges, 359 are Terraform module { source = ... } blocks, which is 73% of everything. Another 36 are GitHub Actions uses: references, 8 are Dockerfile FROM lines, 4 are reusable-workflow uses:, and exactly 1 is a go.mod require. That is the declared graph: every one of those edges is read straight from a manifest the tooling already executes.

The remaining 83 edges are not a parsed ecosystem at all, and I want to be careful not to dress them up as one. They are git_url_reference edges: the output of a heuristic regex layer that scans arbitrary text, line by line, for GitHub URLs that point back into the org. A git clone in a README, a fetch command in a shell script. Every one of those 83 sits at 0.6 or 0.7 confidence by design, because a URL in a comment is not the same kind of fact as a Terraform source block. This is the inferred layer sitting beneath the declared one, a safety net for edges no formal parser owns, and I keep it visibly separate from the declared graph rather than counting it as a sixth ecosystem. The cross-repo dependency graph that matters here is the 408 declared edges. The 83 heuristic ones are there so a real dependency written in a place no parser reads does not vanish silently.

The pattern at the centre: one module, 147 consumers

The centre of Cloud Posse is terraform-null-label, declared by 147 of the 242 repos. The number is the landmark this series keeps returning to. The fresh part is the shape of the drop-off after it, and the drop-off is a cliff. Ranked by consumer count, the top of the in-degree table reads:

  • terraform-null-label: 147 consumers
  • terraform-aws-route53-cluster-hostname: 15
  • terraform-aws-security-group: 13
  • terraform-aws-iam-role: 9
  • terraform-aws-s3-log-storage and terraform-aws-sns-topic: 7 each

The number two module is consumed by 15 repos. The keystone is consumed by 147. There is nothing in the middle. This is a genuinely different shape from the other two orgs in this series, and the difference is where the blast radius concentrates. Prometheus was hub-and-spoke: a few core repos, a readable ring of consumers around each, so losing any one hub takes down its own ring and no more. The kubernetes-sigs org was a federation: many sub-projects, dependencies spread thin and evenly, no single load-bearing centre to lose. Cloud Posse is neither. It is a keystone. Pull it and 61% of the org is standing on nothing at once, and there is no second load-bearing node to absorb the fall. Every one of those 147 modules would still parse, still plan, still look fine in its own repo. The dependency that decides the blast radius is the one nobody remembers is load-bearing, because it is boring and it has always been there.

Where the receipts live: context.tf, line 24

Of the 147 Cloud Posse repos that declare terraform-null-label, 133 declare it on the same line of the same file: context.tf, line 24. This is not a coincidence and it is not inference. It is Cloud Posse’s own documented convention. Their conventions doc explains it plainly: “By convention, we install this file as context.tf which is why we call it the context.tf pattern.” A context object carries the naming and tagging inputs, every cloudposse/terraform-* module exposes it as module.this, and the file that defines it is meant to be copied byte-for-byte from the canonical copy in terraform-null-label itself. The dependency is not authored per repo. It is generated, and the generator puts the module "this" block in the same place every time.

Here is the module "this" block a real consumer carries, fetched live from cloudposse/terraform-aws-route53-cluster-zone; the source line is line 24:

module "this" {
  source  = "cloudposse/label/null"
  version = "0.25.0" # requires Terraform >= 0.13.0

That cloudposse/label/null string is a Terraform registry short-address, and it resolves to the cloudposse/terraform-null-label repository through the registry’s naming convention. Every one of the 133 repos that use context.tf puts that block at line 24 exactly, with no exceptions in the population. The other 14 of the 147 declare the same dependency somewhere else: main.tf in seven of them, public.tf in two, and one each in a handful of other files, at various line numbers. So the honest quantifier is not “every one of the 147.” It is 133 of 147, about nine in ten, on context.tf line 24, and the remaining fourteen off the beaten path. The copy-paste convention is enforced by discipline and codegen, not by the language, which is exactly why a real scan finds a long tail that a “they all do it the same way” assumption would miss.

The version story is just as legible from the receipts. Of the 147 consumers, 139 pin a clean semantic version and 8 pin a git-tag path like tags/0.3.1 rather than a bare version string. None float on a range. 138 are on the current 0.25.0; 9 sit behind it. Those 9 are exactly the 8 git-tag pins, which can never string-match a semver release, plus one repo cleanly pinned to the previous 0.24.1. That reconciles a discrepancy an earlier post in this series flagged and left open: “9 lagging” is a recency cut and “8 on a git-tag path” is a syntax cut, and both are correct at once. They were never in conflict. They were counting two different things.

If I bump terraform-null-label, what breaks?

I opened a PR bumping terraform-null-label to a new tag, then asked Riftmap what it would touch across the org. I ran the impact query the way Post B’s merge gate runs it: confidence floor at 0.8, up to three hops out. The answer came back at 147 affected repositories, and every one of them at depth one. Nothing at depth two. Nothing at depth three.

That flatness surprised me the first time, and then it made complete sense. The keystone does not cascade because Cloud Posse’s high-fan-in modules mostly sit at a single layer rather than stacked in chains. The repos that consume terraform-null-label’s direct consumers already are direct consumers themselves. The blast radius is exactly as wide as the direct-consumer count, and it stops there. Wide and flat. One file change, 61% of the org in scope, no transitive tail. I should be precise about what this call proves and what it does not: it enumerates the 147 repos whose declared dependency chain resolves back to the module. It does not prove each one would break on a version bump. It is the list of everything you now have to think about, not a verdict that all 147 are broken.

Now the part this post exists to show. Take that same one-line bump and ask what each detection method would actually return.

Grep gets you close to the headline count, and I am not going to pretend otherwise. gh search code --owner cloudposse "cloudposse/label/null" would surface most of the 147, because for this particular construct the string genuinely is the receipt. That is the honest concession, and it is different from the Go or protobuf cases where the resolved edge is nowhere in the literal text. What grep cannot do is tell you that 8 of those matches pin a git-tag path and 139 pin a semver and none float, or which of the 133-versus-14 files and line numbers each match lives on, or how cleanly each reference resolved. And it cannot tell you how deep the chain runs. Grep returns an undifferentiated list of string matches with no depth attached, so it cannot tell a blast radius that stops at its direct consumers from one that runs several hops past them. That this one is flat is a finding the graph produced, not something grep could confirm. It finds the string. It does not assemble the graph, and it does not carry the state.

A symbol graph returns nothing, and this is the cleanest possible version of that claim. I have a lot of respect for Sourcegraph; its SCIP symbol graphs are excellent at the category they are in. But that category is programming-language symbols, and a Terraform module block’s source is HCL configuration data, not a symbol in any language a SCIP indexer parses. Sourcegraph’s own list of indexers runs scip-go, scip-typescript, scip-clang, scip-java, rust-analyzer, scip-python, scip-ruby, scip-dotnet. There is no Terraform or HCL indexer on it, and there is no partial-credit exception the way there is for go.mod and package.json, where the import path is itself a language symbol. A symbol graph does not resolve this edge poorly. It never looks at the file. Symbol graphs and artifact graphs are different categories.

A catalog returns whatever a human registered, which for this is almost certainly nothing. I want to be fair to catalogs first, because they hold things parsing cannot see: ownership, on-call, the tier of a service, which team you page. But a Backstage relation, in Backstage’s own words, “is commonly generated based on spec.dependsOn of the component or resource in question,” and Port works the same way inverted, with a human writing the relation into their IaC. Nobody hand-registers 147 context.tf module sources across 147 repos and keeps them current. So a catalog either has no record of this dependency or a stale, partial one, and you cannot tell which parts are current from inside the catalog.

Riftmap returns the parsed graph, current as of the scan: 147 consumers, the 8 git-tag pins and the 139 semver pins, the 133 on context.tf line 24 and the 14 elsewhere, the flat depth-one blast radius. Not because the count is secret knowledge. The value is not that 147 is un-greppable. The value is resolution, state, transitivity, and honest confidence, assembled once into one graph the org already declared but never had in one place. Which is the whole reason GitHub’s Dependents tab still reads zero for it: the graph was always sitting in the manifests, and nobody had assembled it.

See it live

You can see a rendered org graph of exactly this star shape on Riftmap’s Prometheus showcase page, which draws the live interactive graph rather than a static screenshot. A published Cloud Posse showcase page is not up yet. Until it is, the Prometheus graph is the closest live view of the same rendering, on a different org shape, and the numbers in this post come straight from the same production API that draws it.

What Riftmap didn’t see, and why

Cloud Posse produces eight kinds of artifact, Riftmap parsed all eight, and only three of them are consumed by another repo inside the org. The production footprint is genuinely multi-ecosystem: 156 Terraform modules, 154 Terraform root configs, 69 Helm charts, 60 generic artifacts, 19 Docker images, 15 Go modules, 11 npm packages, 2 GitLab CI templates. But only Terraform modules, Docker images, and Go modules show up as cross-repo edges at all. Of the 156 Terraform modules the org produces, 118 are consumed by a sibling repo; of the 19 Docker images, 6 are; of the 15 Go modules, 1 is. Zero of the 69 Helm charts, zero of the 11 npm packages, zero of the 154 Terraform root configs, and zero of the 2 GitLab CI templates are consumed by a sibling Cloud Posse repo in this scan.

That is not a parser gap, and the why is worth being precise about, because the honest answer is more interesting than “we missed some.” The Terraform root configs are terminal by design: a root module is meant to be deployed, not sourced by another module, so it is an orphan the same way a deployable stack is an orphan, on purpose. The Helm charts and npm packages most plausibly have real consumers, just not inside this org: Cloud Posse publishes charts and a CLI for the public to use, and an intra-org scan is structurally blind to consumption that happens outside the org boundary. That is the same intra-org caveat the first post in this series already carries. Separately, 43 of the 242 repos, about 18%, touch no cross-repo edge at all, either because they declare nothing the scan recognises as cross-repo or because everything they declare resolves outside the org. The scan skipped nothing and errored on nothing. What it did not draw, it did not draw for a reason it can name.

Methodology, briefly

The scan read all 242 repos with zero skipped and zero errors, in about twelve and a half minutes. No model sits anywhere in that parsing path; every edge is read from a manifest construct, not inferred from text or names or embeddings.

The one number worth dwelling on is the confidence distribution, because it is the opposite shape of the last org-scan and the difference is the interesting part. In this scan, 20.2% of edges resolve at confidence 1.0 and the dominant band is 0.9, at 61.1% of everything. That is not doubt. A confidence score on a parsed edge is not “do we think this edge is real,” it is how cleanly the reference resolved to a known target. And 300 of the 359 Terraform edges resolve through the registry short-address convention, turning cloudposse/label/null into the cloudposse/terraform-null-label repo, which the resolver grades 0.9 because it took a documented naming rule rather than an exact string match. A real, deterministic, declared edge, resolved by a rule instead of an identity. The heuristic git_url_reference band is the only part sitting below the 0.8 deterministic floor, at 16.9%. Three org-scans in this series, three different confidence shapes, each one explained entirely by how that particular org names and resolves its own dependencies.

Two asks

Two asks, both small. First, if you find an edge Riftmap drew wrong or missed on a public org, tell me. A misresolved short-address or an unparsed construct is a bug I want to fix, and I credit the report. The honesty beats above exist because someone has to go check the long tail, and a second pair of eyes on a public graph is worth more than another adjective. Second, and it is the ask that actually matters: run this same scan against an org you own, not one you are reading about.

Where this sits in both series

This post is the third entry in one series and the fourth in another. As “what an org actually depends on,” it sits alongside the Prometheus and kubernetes-sigs scans, three real orgs with three genuinely different shapes: hub-and-spoke, federation, keystone. As the fourth “blast radius before merge” post, it is the worked org behind the argument the other three make in the abstract: Post A’s three layers, where this post’s contrast of what each detection method returns comes from; Post B’s merge gate, whose impact contract produced the flat 147; and Post C’s declared, inferred, registered taxonomy, which is why the 83 heuristic edges sit apart from the 408 declared ones.

Scan your own org

You can run the same scan against your own org. Everything above came out of one read-only token, an org-level install, and about twelve and a half minutes, with no per-repo YAML to write and no catalog to maintain. If you want to know which repos break before you bump a shared module or a base image, including the dependency that used to live only in the head of the engineer who just left, point Riftmap at your org and look at your own keystone before you change it.

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.

Common questions

What does the Cloud Posse org depend on? Internally, almost entirely on itself, in Terraform. A Riftmap scan of all 242 Cloud Posse repos (2026-07-04) found 491 cross-repo dependencies pointing at 125 distinct artifacts, and 73% of those edges are Terraform module { source = ... } blocks. The single most-depended-on artifact is cloudposse/terraform-null-label, the naming-and-tagging module that 147 of the 242 repos declare. Most of what the org declares actually resolves outside itself, to the AWS provider and public registries, so cross-repo-within-the-org is the rare signal.

How many repos depend on terraform-null-label? 147 of Cloud Posse’s 242 repos, which is 61% of the org, each declaring it through a Terraform module { source = "cloudposse/label/null" } block. 133 of those 147 declare it on the same generated line, context.tf line 24, following Cloud Posse’s own copied-file convention. 138 are on the current 0.25.0 and 9 sit behind it. GitHub’s own Dependents tracker, by contrast, reports zero dependents for the repository, because it does not parse Terraform source blocks.

What breaks if you bump a Terraform module used across an org? For terraform-null-label specifically, the deterministic blast radius is 147 repositories, and all of them are direct consumers: at a 0.8 confidence floor over three hops, nothing appears at depth two or three, so the impact is wide but flat. That count enumerates every repo whose declared dependency chain resolves back to the module; it is the set you now have to review, not a verdict that all 147 will fail to plan. grep can approximate the count because the module source string is the receipt, but it cannot tell you which references are pinned to a git tag versus a semver, which file and line each lives on, or how deep the dependency chain runs. Here it is flat, but grep cannot tell a flat radius from a deep one because its matches carry no depth.