Skip to main content

Why bex Skips the Crossplane-Style Multi-Cloud Abstraction Layer

8 min readDora NodaDora Noda
Share
On this page

Ask kubectl get machines on a bex cluster and you get back a Hetzner server ID, or a Docker container name if you're running locally. Not a region code. Not an abstract resource handle that might resolve to AWS today and Azure tomorrow. One physical answer, every time.

That's a deliberate choice, and it runs against the direction the rest of the industry just moved. On October 28, 2025, the Cloud Native Computing Foundation graduated Crossplane — its highest maturity tier, reserved for projects the CNCF considers stable, broadly adopted, and safe to bet a platform on. Crossplane now runs in more than 1,000 organizations in production, with contributions from over 3,000 people across 450-plus companies. Its pitch, repeated in CNCF's own 2026 write-up on API-first infrastructure, is that a platform team should be able to provision a database, an object store, or a whole environment through one Kubernetes-native API — regardless of which cloud actually sits underneath it.

bex is also a Cluster API-based platform, also chasing "infrastructure as an API." And bex still doesn't do this. Here's the reasoning, and what an operator gives up — and keeps — by skipping it.

What Crossplane's abstraction actually buys you

Crossplane extends the Kubernetes control loop past pods and into everything else a platform needs: a managed Postgres instance, an S3-compatible bucket, a DNS zone, a whole downstream cluster. You write a Composition once, expose it as a Custom Resource, and every team that needs "a database" applies the same YAML shape whether it lands on AWS RDS or Google Cloud SQL. Allianz Technology — one of the larger case studies CNCF cites — uses exactly this pattern: the Kubernetes API becomes the contract between the platform team and every application team building on top of it, so nobody downstream needs to know or care which cloud account the resource actually lives in.

That's a real, well-earned win for a certain shape of organization: many application teams, several cloud accounts, and a platform team whose job is to hide that sprawl behind one consistent surface. It's also the shape most large enterprises are actually in — which is why Crossplane's graduation reads less like a surprise and more like CNCF catching up to what Nike, Autodesk, and NASA Science Cloud were already running in production.

What the abstraction costs

The bill for that consistency shows up in two places.

First, Crossplane's control plane is itself infrastructure. Provisioning a plain Postgres instance or an S3 bucket — resources that have nothing to do with Kubernetes — still requires a running Kubernetes cluster underneath Crossplane, and that cluster now needs the same patching, upgrade, backup, and disaster-recovery discipline as any other production system. You've added a cluster to operate in order to stop operating clouds directly.

Second, the abstraction adds a layer to debug through. When a Composition fails, the failure isn't "the S3 bucket didn't get created" — it's a chain: the composite resource, the managed resource it expands into, and the underlying provider's own logs, each of which can fail independently and each of which you now have to check in order. A stuck bucket might mean the Composition rendered wrong, or the managed resource never reconciled, or the cloud provider's own API rejected a field three layers down — and you don't know which until you've walked the whole chain. Teams that evaluate Crossplane and walk away tend to cite exactly this: not that the abstraction doesn't work, but that the operational weight doesn't pay for itself once you count the extra cluster and the extra layer of indirection.

The tradeoff Crossplane is actually making

None of this makes Crossplane wrong — it makes it a bet, and the bet is about which slice of your infrastructure is worth abstracting. A useful (if informal) way people frame it: most of what a cloud sells — compute, object storage, a managed queue — is commodity enough across providers that hiding the vendor behind a common API costs little and buys real flexibility. The rest is where a provider's specific capability, price, or hardware access is the actual reason you chose it, and flattening that difference away doesn't make it disappear, it just makes it a lot harder to see.

The failure mode is designing for the lowest common denominator everywhere, not just at the commodity edges — trading away a provider's real advantages in exchange for a portability guarantee most teams will invoke rarely, if ever. A specific Hetzner bare-metal box with dedicated cores and no per-request meter running underneath it isn't a commodity detail — that's precisely the thing a team picked Hetzner for, and a common API designed to also fit AWS or Azure has no slot for "and it's a physical machine you can point at."

bex is built around a specific answer to that trade for one thing in particular: which physical machine is running a given workload. That's not a commodity detail bex wants hidden. It's the thing an operator — human or agent — needs to be able to name, instantly, without asking a reconciliation layer to translate an abstract resource ID back into a real box first.

What bex does instead

bex doesn't author an abstraction on top of Cluster API — it uses Cluster API's own provider model directly, with nothing hiding underneath it.

Locally, that's CAPD, the Cluster API provider for Docker: a "machine" is a container on your laptop. In production, it's CAPH, the Cluster API provider for Hetzner: a "machine" is a real Hetzner server, bare metal or cloud, that bex's infra cluster provisioned by talking to Hetzner's API directly. Moving from one to the other is a provider-overlay swap in infra/clusterapi/overlays/local-capd becomes hetzner-caph — not a rewrite of some universal resource schema bex invented to paper over the difference. The two providers already speak Cluster API's shared Cluster/Machine/MachineDeployment contract; bex just uses it as-is.

The payoff is that kubectl get apps.app.bex.co and kubectl get machines are never a step removed from the truth. An app's status.phase, status.revision, and status.url point at pods, and those pods are bin-packed onto nodes that are, one-to-one, either a named Hetzner server or a named Docker container — never a reference that a multi-cloud reconciliation layer still has to resolve. There's no second control plane translating "give me a database" into "which of N providers actually handles this today." What you asked for and what's running are the same object, one hop away.

The portability question, answered honestly

The honest counter-argument: isn't skipping the abstraction just lock-in to Hetzner, dressed up as a principle?

Not quite — it's a different kind of portability, not none. Cluster API already ships providers for AWS, Azure, vSphere, bare metal, and a dozen others, maintained by their own communities. bex proved the model works by shipping two providers, CAPD and CAPH, sharing one operator and one API on top: the operator, bex-api, and every App CR are identical bytes whether the nodes underneath are Docker containers or Hetzner servers — only the provider overlay in infra/clusterapi/overlays/ changes. Adding a third provider later — say, cluster-api-provider-aws for a customer who needs an AWS region specifically — means adopting another Cluster API provider that already exists and is maintained upstream, not inventing a new abstraction layer to sit above all of them simultaneously.

That's portability by sequential swap: move the whole platform from one substrate to another, deliberately, one overlay change at a time. What you don't get is Crossplane's simultaneous multi-cloud promise — one API call fanning out to whichever of several live providers happens to be cheapest or least loaded this week. bex isn't trying to make the substrate invisible or swappable mid-flight. It's trying to make sure that whichever substrate you picked, you can always point at the exact machine running your code.

Why this matters more once agents are the ones asking

bex's whole premise is that AI agents, not just humans, need to operate a deployment platform — deploy, check status, roll back, suspend, all as API calls a tool-calling agent can make directly. That thesis pushes hard in the same direction as the machine-visibility choice above: an agent reasoning about a failed deploy needs status.phase to resolve to one real answer, not a hop through a reconciliation layer that might currently be routing between two or three clouds. A reconciliation layer built to make the substrate swappable is one more thing an agent has to trust got the mapping right. A platform where the API's answer is the physical answer removes that step entirely — one fewer place for an autonomous deploy loop to be wrong about what's actually running.

Bex.co is the open-source, AI-native Render alternative — push a git repo, get a running HTTPS service on machines you own. Star the repo on GitHub or deploy your first app today.

Related articles

Run this on infrastructure you own

bex is the open-source, AI-native Render alternative — push a git repo and get a running HTTPS service on your own machines.

Get started with bex