One Promise, Two Clouds: kro vs Crossplane
I’ve spent some time with Crossplane. Not committed, just far enough in to know what it feels like. The problem I keep coming back to is easy to say and hard to build. Say you run a platform across AWS and Azure. A developer asks for an object store. On AWS they get S3, on Azure a storage account, and they shouldn’t have to know which. One way to ask, a different answer per cloud.
I also have a bias towards fewer things. Every controller and provider you install is something you upgrade, debug and explain to the next person. So when kro kept coming up, I spun up a throwaway cluster and spent some time with it. This is what I learnt. It doesn’t end in a decision, because I haven’t made one.
TL;DR
| Crossplane | kro | |
|---|---|---|
| Promise and delivery | Separate. One definition, many implementations | Same file. One implementation per API |
| Multi-cloud | Built in. Each request can pick an implementation | A pattern you build. One implementation per cluster, plus a test |
| Moving parts | Crossplane, its providers and its functions | kro, plus each cloud’s own controllers |
| Managed by AWS on EKS | No | Yes |
| Managed by Azure on AKS | No | No |
| Cloud layer built by | Upbound, generated from Terraform | AWS (ACK) and Microsoft (ASO) |
| Upgrades | Best tooling, more layers | Done for you on EKS, by you on AKS |
| Maturity | CNCF graduated | Alpha |
For one promise across two clouds, Crossplane’s model fits better. kro is simpler and checks your definitions automatically when you apply them, but you build the multi-cloud part yourself.
What kro is
kro, pronounced “crow”, lets you define your own Kubernetes API in a single file. You describe what a developer can ask for, and which resources to create when they do. kro works out the order to create them in from how they reference each other, and keeps them in that shape. When I edited one of its resources by hand, kro quietly put it back.
The part that impressed me is how early it catches mistakes. kro checks your definition against the real schemas of everything it creates, the moment you apply it. A typo in a field name came back as an error that read more like a compiler than a controller. It’s still alpha, and it shows in places, but that check felt like a glimpse of how infrastructure tooling should behave.
Where Crossplane and kro differ
Crossplane separates the promise from the delivery. One object defines what a developer can ask for. Other objects define how each request is fulfilled, and you can have as many of those as you like, one per cloud if you want.
kro doesn’t. The promise and the delivery live in the same file, one file per API. That’s deliberate. kro doesn’t try to abstract over cloud providers. It composes whatever is already in the cluster, such as the controllers AWS and Azure publish for managing their own resources from Kubernetes.
So in kro, multi-cloud is a pattern you build rather than a feature you configure. I tried two.
Branching in one file. Add a “which cloud” field and let the definition choose. It works, but now the developer has to know which cloud they’re on, which is exactly what I was trying to hide. The results start to differ per cloud too. And when I changed that field on a live resource, kro deleted one and created the other. With a real bucket, that’s deleting someone’s data.
One implementation per cluster. Accept that each cluster lives in one cloud. Keep the shared definition in one place, layer a cloud-specific implementation on top, and point each cluster at its layer.

The shared part only speaks the platform’s language. A developer asks for sydney, not a cloud region, and names follow the strictest rule of either cloud. On AWS that becomes a single bucket. On Azure it becomes three resources. Different shapes, same request.
The catch is that nothing in kro guarantees the two clouds keep offering the same API. So I wrote a test that builds both and compares what each one exposes. Change one cloud without the other, and the test fails.
“In Crossplane the guarantee is the architecture. In kro it’s a test I wrote.”
Is Crossplane’s model better?
For this problem, yes. Separating the promise from the delivery is exactly what multi-cloud wants.
But kro has its own pull. Mistakes surface the moment you apply, with no extra tooling. It’s a single controller, and its expression language is deliberately limited, which caps both what you can build and what can go wrong. And if every cluster lives in one cloud, Crossplane’s flexibility is a choice nobody needs to make. I once wrote that good platforms remove choices. I’m not sure yet whether kro’s one implementation per cluster is a missing feature or that principle applied to me.
The practical bits
- Fewer things, partly. AWS now runs kro, its own resource controllers and Argo CD as a managed service for EKS. I couldn’t find an equivalent on Azure, so that side is self-hosted.
- Deletion. In the current release, delete something in kro and everything it created goes with it. An opt-in way to leave resources behind has just landed for the next release, behind an alpha feature flag. Until that matures, the safety net for anything holding data has to come from the cloud controllers underneath, which can be told to keep the real resource.
- Terraform stays. Something has to create the clusters these controllers run in, and Terraform shows you what will change before it changes it. kro doesn’t. Terraform for the foundation that rarely changes, controllers for the things developers ask for often, is the split that makes sense to me.
Upgrades
The part I underestimated is who builds the layer that talks to each cloud, because they decide how upgrades feel.
- Crossplane’s main AWS and Azure providers are built by Upbound, not AWS or Microsoft, and generated from the Terraform providers. Crossplane has the best upgrade tooling of the three: install a new version, then switch over when you’re ready. But there are more layers that can change underneath you. And since 2025, free users can only pull the latest version of Upbound’s own builds, so pinning or rolling back means using the community builds or paying.
- On AWS, EKS can upgrade kro and AWS’s controllers for you. The trade is control. I couldn’t find a way to choose when. If you run them yourself instead, each AWS service has its own small controller, so one upgrade touches one service.
- On Azure, one operator covers every Azure resource, and you upgrade it yourself, one minor version at a time. The operator also drops old API versions over time, so the Azure layer needs regular small updates.
Whichever way you go, tell the cloud layer to keep anything holding data before you upgrade it.
What I’m watching
kro’s roadmap has no plan to separate the promise from the delivery, and I don’t expect one. It does have versioned APIs, gradual rollouts and offline validation, all of which I’d want before trusting it with something important. The maintainers include people from AWS, Google, Microsoft and SAP, but the largest share of recent work I could attribute came from AWS. For anything spanning Azure, how much Azure invests is the question I’d most like answered.
What I don’t know yet
I don’t know if a test is enough, or whether I’m slowly rebuilding Crossplane’s model out of config files and bash.
I don’t know what day two looks like. Changing an API that hundreds of resources depend on, with no preview and no gradual rollout, is the scenario that would decide this, and I haven’t lived it.
What I do know is that “which tool” was the wrong first question. The better one is where the promise lives. In Crossplane it lives in the platform. In kro it lives in the repository, the build and a test. Both are real answers. I’ll keep experimenting until I know which one I’d trust to keep a promise across two clouds.
Further reading
- What is kro?: the project’s own overview
- Crossplane composite resources: how a request picks its implementation
- Amazon EKS Capabilities: managed kro, ACK and Argo CD on EKS
- AWS Controllers for Kubernetes (ACK): AWS’s controllers for managing AWS from Kubernetes
- Azure Service Operator: Microsoft’s equivalent for Azure
- An update on Upbound’s Official Providers: the 2025 change to how Crossplane providers are distributed