Skip to main content

Crossplane's API-First Infrastructure Bet: What Agent-Operated Infra via Declarative CRDs Gets Right That a REST Deploy API Doesn't

8 min readDora NodaDora Noda
Share
On this page

An AI agent asks a REST API "did the deploy finish?" and the API says yes — because the endpoint returns the last state someone wrote to a database row, not the state of the thing actually running. Ask a Kubernetes-style declarative API the same question and it can't lie the same way: the status field is what a controller observed a second ago, continuously, whether or not anyone asked. That distinction is the entire argument CNCF made in March 2026 for why Crossplane's CRD model is a better substrate for AI agents than a bespoke REST API — and it's a sharp enough claim that any platform exposing a deploy API to agents, bex included, has to answer it directly instead of nodding along.

The short version, before the mechanics: Crossplane's CRDs get three things right that a naive REST deploy API doesn't — schema an agent can discover without reading docs, state an agent can trust without polling, and a reconciliation model that survives a missed event without anyone noticing. None of the three require Kubernetes specifically. They require a REST API to stop acting like a request/response ledger and start acting like a control loop. That's the fix bex is building toward, and it costs something Crossplane doesn't pay: nobody has to install a cluster to call it.

What "API-first infrastructure" actually claims

Crossplane's pitch, laid out in the CNCF post and its companion piece on Crossplane's own blog, starts from a specific failure mode: "desired state lives in Git, while the actual state lives in cloud providers." Policy sits in pipeline configs. Institutional knowledge sits in wikis nobody reads. Humans route around that mess by asking a coworker or checking three dashboards. Agents can't — they fail "not because it lacks capability, but because the platform wasn't built for programmatic access."

Crossplane's answer is the same shape every Kubernetes controller uses: a resource has a spec (what you want) and a status (what a controller actually observed), and the controller's whole job is closing the gap between them, forever, without being asked. An agent — or a human, or a dashboard, or another controller — reads one object and gets the schema, the desired state, and the real state in the same place.

That's the pitch. It's also, worth saying up front, a bet with a real cost — not a free upgrade.

The bet: these guarantees aren't free, they're a toll booth

Getting spec/status semantics, watchable state, and admission-time validation the Crossplane way means every caller — human or agent — goes through the Kubernetes API: RBAC roles, a running control plane, CRDs installed and versioned, kubectl-shaped tooling or a client library that speaks the Kubernetes object model. That's a real tax. A team with no Kubernetes cluster doesn't get any of these guarantees for free; they get them after standing up a control plane just to answer "is my deploy done."

Crossplane's bet only pays off if enough of the calling population — increasingly agents, not just humans — is willing to pay that toll. If most agents calling a deploy tool would rather speak plain HTTP with a JSON body (which is what every LLM tool-calling framework defaults to), then the mechanical advantages Crossplane has are real but stranded behind an entry cost most callers won't cross. The interesting question isn't "are CRDs better than REST" — it's "can the mechanics that make CRDs better be had over plain HTTP, without asking every agent to learn Kubernetes first."

Mechanism by mechanism: what CRDs get right

Five concrete gaps separate a typical hand-rolled REST deploy API from a Kubernetes-style declarative one. Each is a specific, checkable mechanism — not a vibe.

MechanismKubernetes/CrossplaneTypical REST deploy API
Schema discoveryOpenAPI schema generated straight from the CRD's Go/CUE types — an agent introspects the live API server and gets an accurate, versioned contractWhatever the docs say, if they're current — no guaranteed source of truth an agent can query at runtime
Real-time actual statestatus subresource, written only by controllers, reflects what's actually running right nowA GET /deploys/:id returns whatever the last write to the deploy table says — accurate only if nothing failed silently between the write and now
Change notificationwatch — a long-lived, resumable stream keyed to a resource version; reconnect and resume exactly where you left offPoll on an interval, or a webhook that fires once and is gone if the receiver was down
Failure resilienceLevel-triggered reconciliation: the controller re-derives "what's true now" from current state, so a missed event, a restart, or a dropped connection self-heals on the next reconcile passEdge-triggered: if the one webhook delivery is lost, the caller's picture of reality is wrong until it thinks to ask again
Authorization / validationRBAC plus admission controllers reject a bad request before it's ever persistedApp-level checks run after the request lands — invalid state can exist, briefly or not, before something notices

The one worth dwelling on is failure resilience, because it's the mechanism a REST API can't bolt on without a real redesign. Kubernetes controllers are level-triggered — reconciliation asks "what should be true, and what is true, right now," not "what happened since the last event." That's textbook controller design (Kubebuilder's own docs describe periodic re-list alongside watch specifically to catch anything a watch stream missed), and it's why a controller can crash, restart, and miss an arbitrary window of events without ever producing wrong state — the next reconcile pass just recomputes the diff from scratch. A webhook-based REST API has no equivalent unless the receiver builds one, and most don't.

The strongest counterargument: REST isn't inherently naive

The honest counter to all five rows above is that a well-built REST API doesn't have to lose on any of them — Stripe's API is the existence proof. Idempotency keys make a retried POST safe. Webhooks are signed, retried with backoff, and delivery failures are visible. Critically, the events API exists precisely so a consumer isn't required to trust webhook delivery — you can always ask "what actually happened, in order, since I last checked," which is a reconciliation primitive bolted onto REST rather than baked into Kubernetes's object model.

That's a real answer to rows 2 and 3. It doesn't answer row 4. Stripe's events API gives you the inputs to build a level-triggered reconciler — it doesn't give you the reconciler. Every Stripe integration that wants Kubernetes-grade resilience has to write and maintain that reconciliation loop itself; Crossplane's controllers get it from controller-runtime for free, because the framework assumes every controller needs it and builds it in once. The gap isn't "REST can't do this" — it's "REST makes every API implement its own version of a primitive Kubernetes ships as infrastructure."

The actual verdict: borrow the semantics, not the transport

None of the five mechanisms above require an agent to speak the Kubernetes object model. They require a REST (or GraphQL) API to stop modeling a deploy as a fire-and-forget action and start modeling it the way a controller does — desired state in, continuously reconciled actual state out. That's the fix bex is building toward for its own Render-compatible API, in three concrete pieces:

  • A real status endpoint, not a status field. GET /deploys/:id needs to return what the release-history row and the underlying process supervisor agree is true right now — not just the last state some handler happened to write. That's the REST equivalent of a status subresource: read-only, controller-written, trustworthy on its own.
  • A resumable watch stream instead of webhook-only. An SSE or WebSocket stream keyed to a resource version an agent can reconnect to and resume — "give me everything since revision N" — instead of a single webhook delivery that's gone the moment it's missed. This is the row-3 fix, and it's transport-agnostic; it doesn't need kubectl or a cluster to work.
  • Idempotent declarative writes. A deploy request should be PUT /services/:id/spec (here's the desired state; make it so) rather than POST /deploys (do this action now). The difference matters the moment an agent retries after a timeout — a declarative PUT is safe to resend; an imperative POST risks a second deploy stacked on the first.

None of that is "adopt CRDs." It's closer to admitting Kubernetes controllers solved a real problem and porting the solved shape — spec, status, watch, idempotent writes — onto a transport an agent can call without first learning a cluster's RBAC model. The kubernetes-mcp-server project makes direct Kubernetes API calls available to agents today, and that's the right answer for teams already running Kubernetes. For a platform whose whole pitch is that pushing a git repo shouldn't require a Kubernetes manifest in the first place, the same guarantees have to show up over plain HTTP — or the "agents as first-class operators" promise only holds for the subset of agents willing to pay Crossplane's toll.

Bex.co is the open-source, AI-native Render alternative — push a git repo, get a running HTTPS service on machines you own, with a Render-compatible API built around exactly these guarantees: real status, resumable watch, idempotent declarative deploys — no Kubernetes manifest required to get them. Star the repo on GitHub or deploy your first app today.

Related articles

Give your agents a chain backend

Autonomous agents hit RPC endpoints very differently than people do. See what bex router handles on their behalf.

Read the agents guide