Crossplane graduated the CNCF in November 2025. Four months later, CNCF's own blog called it "the case for API-first infrastructure" — the thing that makes Kubernetes clusters legible to AI agents instead of just to humans with kubectl muscle memory. It's a good pitch. It's also, quietly, the second time a CNCF project has made that exact pitch: Cluster API has been exposing infrastructure as a Kubernetes API since the sig-cluster-lifecycle days, years before "AI-operable" was a phrase anyone used.
Both projects answer the same question — how do you let something other than a human, driving a form or a kubectl apply, request infrastructure and get a Kubernetes-native answer back? They just answer it with opposite bets on generality. Crossplane bets you don't know your resource catalog in advance, so it gives you a compiler for building new APIs. Cluster API bets you know exactly what you need — a fleet of machines running Kubernetes — so it gives you a fixed API for that one job and nothing else. If you're building a self-hosted PaaS, you will eventually have to pick a side of that bet for your own tenant-facing infrastructure, and picking wrong means either months hand-writing controllers you didn't need to, or a general-purpose composition engine bolted onto a product that only ever needed three resource types.
What Crossplane Actually Buys You
Crossplane's pitch is: define a new Kubernetes API without writing a Go controller. Three pieces make that possible.
A CompositeResourceDefinition (XRD) declares the schema for a new custom resource — the equivalent of a CRD, but one Crossplane will supply the reconciler for. Say a platform team wants tenants to self-serve a Postgres instance. They write an XRD for DatabaseClaim with fields like size, region, version.
A Composition is the template that says what a DatabaseClaim actually turns into. In current Crossplane (Pipeline mode is the default), a Composition is an ordered pipeline of composition functions — small, testable units that each take the claim's current state and emit the resources it should produce. One function might render a CloudNativePG Cluster; another might render the Secret holding its credentials; a third might render a Service to expose it.
Crossplane calls the pipeline every time the claim changes, diffs the result against what's live, and reconciles the difference — the same level-triggered control loop as every other Kubernetes controller, except you configured it instead of coding it.
Crossplane v2 (released 2025) changed one structural thing that matters for multi-tenant platforms: Composite Resources are namespaced by default. In v1, an XR was cluster-scoped and tenants interacted with a separate, namespaced Claim object that pointed at it — the "claim/XR duality" that confused almost everyone who touched it. v2 collapses that: you create the resource directly in the tenant's namespace, and namespace RBAC, ResourceQuota, and NetworkPolicy apply the way they would to any other namespaced object.
For a PaaS built on tenant namespaces as the isolation unit, that's not a nice-to-have — it's the whole reason the platform's existing per-tenant RBAC and quota story would still work with Crossplane in the mix, instead of needing a second, parallel isolation model.
The upshot: if a platform team wants to add "tenants can request an S3-compatible bucket" or "tenants can request a DNS record" next quarter, the entire job is writing an XRD and a composition function — no new Go binary, no new controller to build, test, and ship.
What Cluster API Actually Buys You
Cluster API doesn't do any of that, on purpose. Its API surface is fixed: Cluster, Machine, MachineSet, MachineDeployment. A MachineSet keeps a stable count of Machines running, the same way a core Kubernetes ReplicaSet keeps a stable count of Pods; a MachineDeployment rolls changes across two MachineSets the same way a Deployment rolls across two ReplicaSets. There's no schema you define, no composition pipeline you configure — the API is exactly as expressive as "provision, scale, and upgrade a fleet of machines that run Kubernetes," and not one bit more.
That narrowness is the entire value proposition. A MachineDeployment means the same thing whether the underlying infrastructure provider is Docker (CAPD, for local dev), Hetzner (CAPH), AWS, or bare metal — because the API was never generic in the first place, just implemented against different backends. You don't compose a MachineDeployment out of smaller pieces, and you don't extend its schema.
Cluster API traded Crossplane's flexibility for something a fleet-lifecycle problem values more: an API surface stable enough that six different infrastructure providers can implement it identically, and a schema simple enough that nobody has to review anyone else's Composition. There's no abstraction to design badly, because there's no abstraction to design.
The Concrete Trade-Off
| Crossplane | Cluster API | |
|---|---|---|
| What you get | A generic engine: define any new resource type via XRD + Composition | A fixed API for one job: machine fleet lifecycle |
| Who writes reconciliation logic | The platform team, as a composition-function pipeline | The Cluster API maintainers and infra-provider authors, as Go code you never touch |
| Adding a new resource type | Write an XRD + Composition — no new binary | Not applicable — the resource types are fixed |
| Blast radius of a bad design | A poorly designed Composition is now every tenant's API for that resource — mistakes compose | Bounded — the schema is fixed, so there's no composition-design mistake to make |
| Isolation model (v2 / current) | Composite Resources namespaced by default — fits tenant-per-namespace | Cluster-scoped by nature — the fleet is shared platform infrastructure, not a tenant resource |
| Right fit | An open-ended, growing catalog of tenant-requestable primitives | A single, well-understood lifecycle problem that doesn't change shape |
The two aren't really competitors — they solve different layers of the same platform. The question for a self-hosted PaaS isn't "Crossplane or Cluster API," it's "where does each one belong," and for the machine-fleet layer, the answer is usually "just Cluster API, because that's the one job it does better than a generic engine would."
Applying It: What bex's Own Roadmap Actually Needs
bex — the open-source, AI-native Render alternative this blog is written for — is a useful test case because it's already made both of these decisions, not hypothetically but in shipped code.
For the machine-fleet layer, bex already runs on Cluster API: CAPD locally for development, CAPH in production against Hetzner. That's Cluster API doing exactly the job it's built for — provisioning and scaling the machines tenant workloads bin-pack onto — and there's no argument for replacing it with a Crossplane composition here. Nobody needs a custom schema for "a machine that runs Kubernetes"; Cluster API's fixed API already is that schema, implemented against the exact provider (Hetzner) bex targets.
For the tenant-facing layer — the actual question the "does bex need an XRD layer on top" framing is asking — bex has faced the "expose a new resource type to tenants" problem twice already, and solved it the same way both times, without Crossplane. App (the resource a git push becomes) is a hand-written CRD and kubebuilder controller that reconciles into a Deployment + Service + Ingress + TLS. Database — bex's Render-compatible managed Postgres — is a second hand-written CRD and controller, this time projecting onto a CloudNativePG Cluster in the tenant's namespace.
Both were deliberate architecture decisions, not defaults nobody questioned. The Postgres ADR explicitly evaluated the alternatives and chose a purpose-built CRD wrapping CNPG over a generic composition layer, because CNPG's own CRDs already mapped one-to-one onto every axis of the product spec Render's Postgres offering needed.
That's the evidence, and it points to a specific, falsifiable answer rather than "it depends": bex does not need a Crossplane XRD layer today. Its entire tenant-facing catalog is two resource types, both mirrored one-to-one onto a fixed product surface — Render's API shapes — that doesn't grow by tenant request. A tenant can't ask for a resource type that isn't App or Database, because bex's product isn't "request arbitrary infrastructure," it's "push a git repo and get a URL, optionally with a managed Postgres." Bolting a generic composition engine onto a two-resource-type catalog buys configurability nobody's using and adds an abstraction layer between the platform and the CRDs it already controls directly.
The trigger that would flip this call is concrete, and worth naming so it doesn't get missed when it happens: recurring tenant demand for a resource type outside App and Database — a managed Redis, an object-storage bucket, a DNS zone delegation — rather than a one-off request. That's the point where hand-writing a third, fourth, and fifth narrow CRD stops paying for itself, and Crossplane's actual value proposition — write an XRD instead of a controller, every time — starts buying back more engineering time than the composition layer costs to operate. Until that catalog starts growing on its own, two purpose-built CRDs beat one generic engine configured to produce two things.
The Portable Version of This Decision
Strip out bex specifics and the test generalizes to any self-hosted PaaS or platform-engineering team eyeing Crossplane: is your tenant-facing resource catalog a fixed menu that mirrors a product spec you control, or an open catalog that tenants (or their agents) can extend by asking for something new? A fixed menu of two or three resource types is cheaper, more legible, and easier to review as hand-written CRDs — you're not paying for genericity you never call on. An open, growing catalog is where Crossplane's bet — that you don't know the resource types in advance — starts being true instead of theoretical, and the composition-function model earns back its own operational overhead by turning "write a new controller" into "write a new XRD."
CNCF's AI-operability argument for Crossplane is really an argument about which side of that line is moving. As agents start requesting infrastructure primitives that don't exist on a platform's fixed menu — "give this workload a queue," "give this workload a cache" — instead of picking from options a human product manager pre-selected, the open-catalog case gets stronger over time even for platforms that start on the fixed-menu side. That's worth tracking on a roadmap. It isn't a reason to build the generic engine before the catalog actually needs one.
Bex.co is the open-source, AI-native Render alternative — push a git repo, get a running HTTPS service on machines you own, already running on Cluster API under the hood. Star the repo on GitHub or deploy your first app today.
Sources
- Crossplane and AI: The case for API-first infrastructure — CNCF
- Crossplane's Graduation Announcement — CNCF
- Composite Resource Definitions — Crossplane docs
- Compositions — Crossplane docs
- What's New in v2? — Crossplane docs
- Announcing Crossplane 2.0
- Cluster API: A Deep Dive On Declarative Cluster Lifecycle Management
- MachineSet — The Cluster API Book
- Cluster API — kubernetes-sigs/cluster-api on GitHub