You’ve been here before. A tool shows up, promises to eliminate boilerplate, and for the first week, it feels like magic. You’re shipping things in YAML that used to take a whole sprint. You tell your team. You write a blog post. Then someone says, “We need to manage this one resource that doesn’t fit the pattern.” And the magic stops.
That’s Crossplane. Or rather, that’s the story most engineers tell themselves about Crossplane before they’ve actually pushed it past the demo stage.
Every abstraction promises to save you from complexity. The fine print is always the same: only for the easy parts.
Here’s what Crossplane actually does well: it lets you define cloud resources — S3 buckets, RDS instances, VPCs — as Kubernetes custom resources. You write YAML, Crossplane’s controllers provision the underlying infrastructure. No Go code. No operator scaffolding. No reconciler loops you have to debug at 2 AM. For standard AWS, Azure, and GCP resources, it genuinely works. You compose resources into higher-level abstractions, expose them to developers, and suddenly your platform team looks like they’ve built something real.
Then reality walks in.
Your company uses a SaaS platform that has no Crossplane provider. Or you need to manage a resource with lifecycle logic so specific that the generic provider can’t express it — a database that needs a schema migration before it’s marked ready, or a DNS record that depends on a certificate validation step that the provider doesn’t model. You start writing workarounds. Hooks. External scripts glued to Crossplane via Kubernetes events. And slowly, you realize you’re building a custom controller — just worse, because it’s scattered across five files of YAML and a shell script no one will understand in six months.
The moment you need to do something the framework didn’t anticipate, you’re not a user anymore — you’re a maintainer.
This is where most teams split into two camps. Camp one doubles down on YAML, writing increasingly convoluted compositions and external logic to force-fit their use case. They’ll spend weeks fighting the tool, convinced that the answer is “more Crossplane.” Camp one ships late and quietly hates the platform team.
Camp two writes a custom provider. In Go. The exact thing Crossplane was supposed to save them from.
And here’s the twist: camp two is doing it right.
Because the promise of Crossplane was never “you’ll never write Go again.” The promise — the real one, the one that matters — is that you’ll have a standardized interface for infrastructure. Kubernetes-native, declarative, composable. The YAML layer handles the 80% of resources that follow predictable patterns. The custom provider layer handles the 20% that don’t. And that 20%? It’s where all the actual engineering happens.
The real architecture isn’t in the YAML. It’s in the boundary where YAML stops working and Go begins.
When you write a custom Crossplane provider, you’re implementing a controller that manages a specific resource type. You define the CRD schema, implement the logic to create, update, and delete the external resource, and Crossplane handles the Kubernetes integration — the reconciler loop, the status reporting, the event emission. You write the domain logic. Crossplane handles the plumbing. That’s not a failure of the abstraction. That’s the abstraction working exactly as designed: standardizing the interface while leaving room for custom implementation.
The problem isn’t that Crossplane makes you write Go sometimes. The problem is that most teams don’t plan for it. They adopt Crossplane believing the YAML layer is the product, when in reality, the YAML layer is the floor. The ceiling is defined by how well you can write and maintain custom providers when the standard ones run out of gas.
If you’re evaluating Crossplane for your platform, here’s the question that actually matters: not “can it manage our standard resources?” — it can — but “what happens when we hit a resource it can’t?” Do you have Go expertise on the team? Do you understand the provider pattern well enough to extend it? Are you prepared to own and maintain a custom provider for years, with the same rigor you’d apply to any other production controller?
Low-code doesn’t eliminate complexity. It relocates it.
The teams that succeed with Crossplane aren’t the ones who never write Go. They’re the ones who know exactly when to stop writing YAML and start writing Go — and who treat that transition as a design decision, not a failure. They define clear boundaries: this far, YAML. Beyond this, a custom provider. And they document the boundary so the next engineer knows which side of the line they’re standing on.
Crossplane’s real value isn’t that it replaces custom controllers. It’s that it gives you a framework to decide which resources deserve a custom controller and which ones don’t. That decision — knowing where the abstraction ends and the engineering begins — is the actual product. Everything else is just YAML.
FAQ
Q: If Crossplane still requires writing Go for custom providers, why not just write controllers from scratch?
A: Because Crossplane standardizes the interface. You still write domain logic in Go, but Crossplane handles the Kubernetes boilerplate — reconciler loops, status reporting, event emission. You're writing the 20% that matters, not the 80% that's identical across every controller.
Q: How do I know when to use YAML vs. when to write a custom provider?
A: If the resource lifecycle follows a standard create-update-delete pattern and the provider supports it natively, use YAML. If you need custom lifecycle logic, multi-step provisioning, or a resource no provider exists for, write a custom provider. The boundary is: does the standard pattern express your intent, or are you fighting it?
Q: Isn't this just admitting that low-code infrastructure tools always fail?
A: No — it's admitting that low-code tools have a ceiling, and the good ones give you a structured escape hatch. Crossplane's custom provider pattern isn't a failure of the platform; it's the platform respecting the reality that not all infrastructure fits a generic pattern. The failure is teams not planning for it.