Most Backstage-on-AKS internal-platform tutorials reach for Crossplane to do the resource provisioning. We started there too. Six months in, we replaced Crossplane with Cluster API Provider for Azure (CAPZ) plus Azure Service Operator v2 (ASO) — and the result is a platform that does less tutorial-shaped magic and more of what we actually need.
This is when each choice makes sense, what we hit that pushed us off Crossplane, and the architecture we settled on.
What each tool actually does
Crossplane is a control-plane-of-control-planes. You define Compositions that map a high-level abstraction (MyDatabase) to a graph of cloud resources. The Crossplane controller in your cluster reconciles those Compositions against the cloud. Cloud-agnostic in design, with a strong opinion about XR (composite resources) and Compositions.
CAPZ (Cluster API Provider for Azure) is specifically about provisioning AKS clusters and their associated infrastructure. It's the standard way to "Kubernetes-manage your Kubernetes clusters." If you have many AKS clusters, CAPZ makes sense.
ASO v2 (Azure Service Operator v2) is a 1:1 mapping of Azure resources to Kubernetes CRDs. ResourceGroup, StorageAccount, KeyVault — each is a CRD. You write a manifest, the operator creates the resource. No Composition layer. No abstraction.
Crossplane and ASO occupy different parts of the stack. Crossplane is "build my abstractions." ASO is "give me a CRD per Azure resource." Many teams use them together — Crossplane Compositions that internally reference ASO resources.
What pushed us off the pure-Crossplane setup
Three problems showed up in month 4-5:
1. Composition authoring was a specialized skill we couldn't spread. Writing a good Composition requires understanding XR/CR mechanics, patch-and-transform syntax, and the upstream provider quirks. We had two engineers comfortable doing it. The rest of the team treated Compositions as black boxes. When something broke inside a Composition, debugging required one of those two engineers. That's not a platform — that's a bottleneck.
2. Drift detection was loose. Crossplane reconciles continuously, but the "drift" it detects is at the level of the Crossplane resource, not the underlying Azure resource. If someone clicked something in the Azure portal, Crossplane wouldn't necessarily catch it depending on how the Composition was written. We had a few cases where prod state diverged from declared state and we didn't notice until an audit.
3. We didn't actually need cloud-agnostic. Crossplane's biggest selling point is "you can swap providers without changing your abstractions." We're an Azure shop with no plans to move. Paying the cost of cloud-agnostic abstractions for a benefit we'll never use is just paying the cost.
The architecture we moved to
Application teams -> Backstage scaffolder -> Git PR
|
v
ArgoCD (sync to AKS)
|
v
ASO v2 CRDs in cluster
|
v
Azure resources
For cluster fleet management:
Management cluster (CAPZ controllers)
|
v
Provisions / upgrades / deletes
|
v
Workload AKS clusters
Two operators (ASO + CAPZ), no Compositions, no XR/CR. Backstage scaffolder templates produce ASO manifests directly. PR review on the manifests is straightforward because they look like 1:1 Azure resources with familiar field names.
What got better
Onboarding time for new platform engineers. Used to take ~3 weeks before someone could meaningfully edit a Composition. With ASO, new platform engineers were submitting useful PRs in their first week — because an ASO StorageAccount resource looks exactly like a Bicep StorageAccount, just in YAML. Familiar names, familiar fields.
Drift detection. ASO v2 reconciles each resource against the Azure ARM API on a configurable interval (default 15 minutes). When a portal click changes a resource, ASO pulls it back. We confirmed this in tests — clicked things, watched them snap back. Crossplane could do this with the right Composition, but ASO does it as the default behavior.
PR reviewability. A PR adding a new Azure resource is now a YAML file with apiVersion: storage.azure.com/v1api20230101 and the recognizable fields. Reviewers without specialist Crossplane training can spot issues. Reviewers WITH security context can confirm allowBlobPublicAccess: false is set without learning a Composition's patch syntax.
What got harder
No abstractions. When 12 services each need a similar storage setup, with Crossplane Compositions you write the Composition once and call it 12 times. With raw ASO, you copy the manifest 12 times. We mitigated with Helm-template-based scaffolder templates and a strong "DRY at the template level" discipline. It's not as elegant. It works.
Cross-cloud is genuinely off the table. If we ever decided to run anything on AWS, we'd need a different operator stack. Crossplane would have made that pivot easier. We're betting that won't happen.
Some Azure resource types lag in ASO support. ASO v2 covers a wide swath but not 100%. We hit one resource type (a specific Front Door SKU) that wasn't supported and had to fall back to Bicep deployment for that single resource. Manageable but annoying.
When I'd still choose Crossplane
If any of these are true for your team, stay with Crossplane (or pick it for new platforms):
- You're multi-cloud now or plan to be within 18 months
- You have a team with strong Composition authoring experience
- You need abstractions like "platform service" that span multiple cloud providers
- Your engineering org has bought into the XR/CR mental model
If most of these are true, choose ASO + CAPZ:
- You're committed to Azure
- Your team comes from a Bicep / ARM / Terraform background
- You want PRs to look like provisioning requests, not abstractions
- You value drift detection as a built-in behavior
The Backstage integration
Backstage doesn't care which provisioner is underneath. The scaffolder produces YAML, ArgoCD syncs it, the operator reconciles it. Backstage shows the user "your storage account is being provisioned" by polling the relevant CRD's status field. ASO's status fields are well-shaped for this; Crossplane's status fields are also fine. Tied on this dimension.
What I'd do differently if starting today
Spend a week prototyping each option with the same use case (provisioning a new microservice end-to-end) and have two engineers from outside the platform team review the resulting code. Pick the one they understand faster.
We made the original decision based on conference talks. The conference talks weren't wrong, but they were optimizing for "look how powerful this is" rather than "look how easy this is to maintain." Six months in, easy-to-maintain matters more.
I would NOT mix the two operators heavily. We briefly had a setup with ASO managing some resources and Crossplane Compositions managing others; the seam between them was confusing and led to a couple of "which one owns this?" incidents. Pick one mental model and commit.

Conversation
Reactions & commentsLiked this? Tap a reaction. Want to push back, share a war story, or ask a follow-up? Drop a comment below — replies are threaded and markdown works.