Modeling a project's infrastructure as a graph

A flat list of resources can't tell a load-bearing database from one nobody uses anymore. A dependency graph can, by walking it from the project's entry points.

Published
September 24, 2026
Reading time
2 min read

On CNP, teams ask for the parts of their stack that aren't containers, like a managed database, and the platform provisions them with access scoped to their project. I owned that layer end to end: the model for what a team asks for, the provisioning behind it, and the API and front end teams use to ask. This post is about the change to it I'd point to first, how a project describes its infrastructure.

Claims

What a team writes is a claim: a short description of what it needs, a database of some size for some project, with nothing about how that's built. Crossplane turns the claim into the actual cloud resources behind it, and those are the platform's problem, not the team's.

What the team writesWhat gets provisionedvia the API or the front endkind:Databaseproject:paymentssize:smallCrossplanemanaged databasenetwork access rulesIAM role, this project onlycredentials secret
A team writes a few lines. Crossplane expands them into every cloud resource those lines imply.

Most of the work was keeping that boundary simple enough that nobody needs to understand the resources on the right to get the left side correct. That boundary is also where the next problem showed up.

What a flat list can't say

A project used to be a flat list of claims. That's fine for a handful of resources, but a list has no way to say which resources depend on which, so it can't tell a database that three services use from one that nothing has touched since a migration. Both are one more line, and both cost money every month. Nobody deletes a database they aren't sure about, so unused resources just pile up.

A project as a graph

I proposed the alternative: a project as a dependency graph. Each resource declares what it depends on, and the project's entry points, the things the team actually runs, are the roots.

webworkerapiqueuepostgresredisbackups bucketpostgres-v1v1 dumps bucketentry pointentry pointnothing reaches these: unused
Walking from the entry points reaches everything in use. Whatever is left over gets flagged, including what only a dead resource depended on.

Walking such a graph from the roots would reach everything that's in use, and anything the walk doesn't reach is dead weight or a mistake, either way worth showing to the team. It also catches what a per-resource check would miss. An old database's backup bucket is still depended on by something, the old database, so asking "does anything use this?" says yes. Only reachability from the roots shows that the whole branch is dead.

What exists today is the interface, where a team describes its project as that graph. The part that stores the graph and walks it isn't built yet.

One repo per resource

Beyond my own part I worked with the team on how changes reach the cluster. Each resource gets its own git repo, and ArgoCD reconciles each repo onto the cluster as its own application, under one parent application whose only job is managing the others (the app-of-apps pattern).

One repo eachARGOCDClusterparent app: manages appsdatabase repoin syncdatabasequeue reponew commitsyncingqueueonly this changesbucket repoin syncbucket
A commit to one resource's repo syncs that resource and nothing else

So a change to one resource syncs that resource and nothing else, and every resource has its own history that can be reviewed or reverted on its own, instead of one shared repo where every team's changes are tangled together.