Skip to main content

Crossplane v2.2 Shipped a Debugger and a kro Bridge: Why a PaaS That Said No to Crossplane Is Still Watching

11 min readDora NodaDora Noda
Share
On this page

Within 48 hours in March 2026, Crossplane attacked the same pain from two sides. On March 17, v2.2 shipped with an alpha pipeline inspector — the first supported way to watch requests and responses flow through a composition-function pipeline inside a live control plane. On March 19, the project announced function-kro, which drops kro's YAML-plus-CEL authoring model into that same pipeline as a single step.

One release says "compositions are now debuggable." The other says "you can write them in somebody else's simpler syntax." Both are admissions about where the composition layer has been weakest, and both deserve a closer look even from a team that already decided not to adopt Crossplane — which is exactly what this site did in July.

FeatureWhat shippedChanges anything for a non-Crossplane PaaS?
Pipeline inspector (v2.2, alpha)A gRPC tap forwarding every RunFunctionRequest/RunFunctionResponse to a sidecar socketNo reconsider — but it retires the strongest debuggability objection to the composition model
function-kro (v0.1.0 → v0.3.0)kro's ResourceGraph YAML+CEL syntax running as a Crossplane pipeline step, kro engine embeddedWorth tracking — not for Crossplane, but for what it proves about kro itself

Bottom line: the debugger is a maturity signal, not a reason to reconsider. The kro bridge is the more interesting event, because it suggests kro's authoring model is winning even inside the project it was supposed to compete with. The rest of this post earns both sentences.

Before v2.2, debugging a pipeline meant flying blind

Crossplane's composition functions are the mechanism that turns a platform API into real infrastructure: each step in a pipeline-mode Composition receives a RunFunctionRequest (the desired state so far plus credentials and context) and returns a RunFunctionResponse (the mutated desired state), and the chain runs to completion on every reconcile. The model is expressive — arbitrary logic per step, in Go, Python, or CEL — but until March, observing it in a running control plane was genuinely awkward.

The official options were local simulation and self-instrumentation. You could run crossplane render against your composition on a laptop, which replays the pipeline outside the cluster but never sees live state, live credentials, or the exact inputs the real reconciler produced. Or you could modify the thing you were debugging: add logging to a function, insert a passthrough step that dumps its input, redeploy the pipeline, reproduce the failure, then remember to take the scaffolding back out. As the v2.2 announcement puts it plainly, viewing what goes into each function step and what comes out "required updating the pipeline or the functions themselves." For a platform team chasing a composition that renders wrong only in production — wrong region, wrong account binding, a credential that exists in the cluster and nowhere else — that loop is slow in exactly the way that makes on-call miserable.

This was never a secret weakness. It was the documented cost of the pipeline model, and it is worth naming because v2.2's fix is shaped exactly like the complaint.

The pipeline inspector: a tap on the gRPC stream

The pipeline inspector, alpha in v2.2, is conceptually a packet sniffer for the composition pipeline. When enabled with the --enable-pipeline-inspector flag, Crossplane intercepts every function request and response as the pipeline runs and forwards a copy over gRPC to a user-configured Unix socket (default /var/run/pipeline-inspector/socket, overridable with --pipeline-inspector-socket). The reference consumer is a logging sidecar, xpkg.crossplane.io/crossplane/inspector-sidecar, injected next to the Crossplane pod with a shared emptyDir volume for the socket — a 10-millicore, 64-MiB-requests sidecar whose only job is to receive pipeline traces and do something useful with them.

The design is deliberately open-ended. The announcement sketches a spectrum from a local observer dumping requests to stdout during development to a production-grade system storing pipeline traces for audit or post-incident troubleshooting. Nothing about the mechanism dictates the policy: Crossplane forwards copies, and the operator decides whether those copies become debug logs, an audit trail, or alerting input. That separation is the right call for an alpha — it keeps the core change small (tap the stream) while leaving the ecosystem to figure out what "composition observability" should mean.

Two details confirm this release is about maturing rather than expanding. First, v2.2 also improved crossplane beta trace, the CLI command that walks a composite resource's dependency chain — the same debugging story, tightened from the command-line side. Second, the rest of the release is all hardening in the same vein: schema-aware composition functions (RequiredSchemas in RunFunctionRequest, so functions can advertise and validate against the schemas they need), CEL-based XRD validation beyond the spec, and unified runtime configuration for package dependencies. No new abstraction, no new API surface to learn — just the existing pipeline made more legible, more validated, and more operable. That is what a quarterly maturing release is supposed to look like.

So why doesn't this change the adoption calculus for a team like ours? Because the inspector fixes debugging within the composition model; it doesn't change the model's structural costs. Our July post on skipping the Crossplane-style abstraction named two: a second control plane to operate (Crossplane needs its own Kubernetes cluster to provision resources that have nothing to do with Kubernetes), and a reconciliation layer between the API's answer and the physical truth. A debugger makes the second cost cheaper to pay when things break. It doesn't remove either cost. For a PaaS whose whole thesis is that kubectl get machines should resolve to one named server with no translation step, "easier to debug the translation" was never the missing feature — the translation itself is what we declined.

function-kro: kro's syntax as a pipeline step

The March 19 announcement, two days later, is the stranger and more significant of the two. function-kro is a Crossplane composition function that accepts a kro-style ResourceGraph — resources as YAML templates, dependencies wired with ${...} CEL expressions — as a single pipeline step. Your kro definitions drop in unchanged: same syntax, same CEL, now executing inside Crossplane's pipeline instead of kro's own controller. The function embeds kro's graph builder, CEL evaluator, and runtime engine directly, claiming full feature parity with the latest kro release, which means there is no kro installation to operate alongside Crossplane — the authoring model travels without its control plane.

The canonical example is a NetworkingStack platform API: a VPC, a Subnet, and a SecurityGroup defined as three templated resources with CEL references (${vpc.status.atProvider.id}, ${schema.spec.region}) doing the dependency wiring, followed by a second pipeline step running function-auto-ready. Two steps, one in kro syntax and one stock Crossplane function, composed in a single pipeline. That composability is the point — kro syntax for the resource graph, the rest of the function ecosystem for everything around it.

The logistics signal seriousness. function-kro was donated to the Crossplane community as crossplane-contrib/function-kro, with an explicit invitation to the kro community to collaborate. It went from a v0.1.0 first public release to v0.3.0 fast enough that the official v2.2 getting-started docs already install it as the default YAML+CEL step for new compositions. And third-party corroboration is arriving: Brian Grant's August 2026 survey of Kubernetes configuration-transformation orchestrators singles out Crossplane's patch-and-transform and kro functions as "pretty expressive," noting CEL expressions that reference resource fields across the graph. This isn't a demo integration — it's becoming the documented front door to writing compositions.

Why the bridge matters more than the debugger to a team that said no

Here is the sentence that makes this post worth writing for a non-Crossplane audience: function-kro is evidence that kro's authoring model won, adjudicated by Crossplane itself.

Rewind to this site's September 9 assessment of kro. The verdict then: kro replaces hand-written expansion glue with a single YAML file, but it is expansion, not reconciliation — use it for turning one tenant-facing resource into N Kubernetes objects, keep a real controller for anything stateful. The honest accounting had four columns (authoring cost, expressiveness ceiling, maturity, stack-interaction cost), and kro's weaknesses were youth (pre-1.0, kro.run/v1alpha1, releases in the 0.7–0.9 range) and the cost of operating yet another controller on the stack.

function-kro doesn't fix kro's youth. What it does is validate kro's syntax as the composition idiom worth converging on — validated by the larger, more mature project adopting it rather than the reverse. When the graduated control-plane framework with 1,000-plus production organizations tells its users "write the resource-graph step in kro's language," that is a market signal about which authoring model has the longer future. And it narrows the question this site has been tracking since September: whether kro itself — the leaner of the two, a kubernetes-sigs subproject under SIG Cloud Provider, sitting in the CNCF Technology Radar's Adopt ring next to Helm and Backstage — could become a component a Render-compatible API sits on top of, instead of hand-rolled reconciliation logic.

That question is still open, and function-kro sharpens rather than settles it. On the "adopt kro" side of the ledger, the syntax now has two runtimes (kro's own controller and Crossplane's pipeline), which de-risks the authoring investment: YAML+CEL graphs you write today are portable across both. On the "not yet" side, kro itself remains pre-1.0 with alpha APIs, and embedding kro's engine inside a Crossplane function is not the same as kro's standalone controller being production-hardened. The thing to watch is whether kro's graduation timeline accelerates now that its syntax has a second, larger distribution channel — convergence usually pulls maintainers and bug reports toward the shared core, and the shared core here is kro's graph builder and CEL evaluator, not Crossplane's.

Note the asymmetry with the debugger: the pipeline inspector is useful only if you run Crossplane. function-kro's existence is useful information even if you never will, because it changes the odds on kro — the component a Cluster-API-based PaaS might actually adopt. That is why the bridge leads this post's verdict table and the debugger, for all its practical value to Crossplane operators, reads as a maturity signal from the outside.

The honest limits, all in one place

Three caveats before the conclusion, because a verdict without its boundary conditions is marketing.

First, the inspector is alpha: disabled by default, behind a feature flag, with the sidecar at v0.0.3. Alpha in Crossplane's lifecycle means the API can change and production reliance is at your own risk. Operators should trial it the way you'd trial any alpha tap on a hot path — on a dev control plane first, watching for what full request/response forwarding costs in socket throughput and sidecar memory on wide pipelines.

Second, function-kro still requires the Crossplane runtime. It removes kro-the-controller from the stack, not Crossplane-the-control-plane. If your objection to Crossplane is the second cluster and the multi-cloud translation layer, a nicer authoring syntax inside that same control plane concedes nothing. Our July decision stands on exactly those grounds, untouched by either March announcement.

Third, kro itself hasn't moved: still v1alpha1 APIs, still sub-1.0 releases, still a SIG subproject rather than a graduated CNCF project. Syntax convergence is a leading indicator of ecosystem health, not a substitute for API stability. Any PaaS betting tenant-facing APIs on kro today is still betting on a pre-1.0 foundation — acceptable for internal platform APIs with versioning discipline, premature for anything you can't migrate.

What we're tracking next

Two milestones would change this post's verdicts. If the pipeline inspector graduates to beta with a stable trace schema, the "composition observability" ecosystem it enables — audit trails, pipeline-aware alerting, maybe an MCP surface an agent can query — becomes something a platform team can build on rather than experiment with. And if kro approaches 1.0 with its syntax now battle-tested in two runtimes, the September question of kro-as-component gets a real re-examination: a stable, adopted, dual-runtime expansion engine is a much easier component to justify than an alpha-syntax experiment.

Until then, the March pair stands as a clean illustration of where declarative composition tooling is actually maturing: not new abstractions, but debuggability for the ones we have and convergence on the syntax that survived contact with users. That's a maturing ecosystem behaving the way maturing ecosystems should — and it's worth watching closely from the outside, even when you've decided not to move in.

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