Crossplane: Kubernetes as the Universal Control Plane

Reading Time: 5 minutes

Kubernetes Ecosystem: From User to Contributor, Episode 6
← EP05: Cluster API · EP06: Crossplane · EP07: Crossplane vs Terraform →

12 min read


TL;DR

  • Crossplane extends the exact reconciliation pattern EP05 covered for cluster infrastructure to any cloud resource — an S3 bucket, an RDS instance, a DNS record all become Kubernetes CRDs, continuously reconciled
  • Managed Resources represent one real cloud resource each; Compositions bundle several Managed Resources behind a single, simpler custom API a platform team defines and app teams consume
  • Composition Functions are Crossplane’s newer, more flexible replacement for its older YAML-based patch-and-transform templating — real code (Go, Python, or others) instead of declarative patches
  • Crossplane continuously reconciles like any Kubernetes controller — a manual change to a cloud resource outside Crossplane gets reverted on the next reconcile loop, which is a real surprise for teams used to Terraform’s plan/apply model
  • Provider CRD counts can bloat a cluster’s etcd significantly — this drove the ecosystem’s move toward smaller, split “provider families” instead of one monolithic provider per cloud
  • Contribution opportunity: several providers still haven’t migrated to the family-split pattern — a real, currently-tracked, achievable upstream contribution

The Big Picture

App team writes:                    Platform team defined this Composition
                                     once, behind the scenes:
apiVersion: platform.example.com/v1
kind: Database                       XRD "Database" ─── composes ───┐
metadata:                                                             │
  name: my-app-db                                                     ▼
spec:                                                        ┌────────────────┐
  size: small                                                │ RDSInstance    │
                                                                │ SecurityGroup  │
   │                                                            │ ParameterGroup │
   │ app team never sees                                        └────────────────┘
   │ or touches these three                                     each a real Managed
   ▼                                                            Resource, a real
Crossplane reconciles all three,                                cloud API call
continuously, forever

Crossplane’s pitch as a universal control plane is literal: instead of app teams filing tickets or writing their own Terraform for a database, they request a Database — a custom API the platform team designed — and Crossplane’s controllers translate that into the actual RDS instance, security group, and parameter group underneath, then keep reconciling all three toward the declared state indefinitely.


Managed Resources: Cloud Infrastructure as Kubernetes CRDs

$ kubectl apply -f - <<EOF
apiVersion: s3.aws.upbound.io/v1beta1
kind: Bucket
metadata:
  name: app-uploads-prod
spec:
  forProvider:
    region: us-east-1
  providerConfigRef:
    name: aws-prod
EOF

$ kubectl get bucket app-uploads-prod
NAME               READY   SYNCED   AGE
app-uploads-prod   True    True     30s
#                  ^^^^    ^^^^^^ — READY: resource exists and is healthy
#                          SYNCED: Crossplane's last reconcile succeeded

Every field under forProvider maps directly to that cloud API’s actual parameters — this is a thin, honest translation layer, not an abstraction hiding what’s actually being created. READY/SYNCED becoming True means an actual S3 bucket now exists in that AWS account, exactly as declared.


Compositions and XRDs: Building Your Own Abstract Platform API

This is Crossplane’s real differentiator over just using individual Managed Resources directly:

# The platform team defines the abstract API app teams will see
apiVersion: apiextensions.crossplane.io/v1
kind: CompositeResourceDefinition
metadata:
  name: xdatabases.platform.example.com
spec:
  group: platform.example.com
  names:
    kind: XDatabase
    plural: xdatabases
  claimNames:
    kind: Database        # ← this is what app teams actually create
    plural: databases
  versions:
  - name: v1
    schema:
      openAPIV3Schema:
        properties:
          spec:
            properties:
              size: {type: string, enum: ["small", "medium", "large"]}

App teams interact only with the simple Database claim shown in the Big Picture diagram above. The Composition resource (not shown here for brevity) is what actually maps size: small to a specific RDS instance class, storage size, and backup configuration — the platform team’s opinions, encoded once, consumed self-service by every app team afterward.


Composition Functions: Crossplane’s Newer, More Flexible Approach

Older Crossplane Compositions used a YAML-based “patch and transform” templating language to map the abstract API’s fields onto Managed Resource fields — functional, but limited for anything beyond straightforward field mapping. Composition Functions replace that with actual executable code:

$ crossplane beta render xr.yaml composition.yaml functions.yaml
---
apiVersion: rds.aws.upbound.io/v1alpha1
kind: Instance
metadata:
  name: my-app-db-instance
spec:
  forProvider:
    instanceClass: db.t3.micro   # ← computed by real Go logic based on
                                  #   spec.size, not a static YAML patch
    engine: postgres

Composition Functions run as small, packaged pieces of logic (often distributed as OCI images) that Crossplane’s engine invokes during reconciliation — giving platform teams real conditionals, loops, and validation instead of the older templating language’s more limited patch syntax.


Providers and the Provider Ecosystem

Each cloud’s resources are supplied by a separate providerprovider-aws, provider-gcp, provider-azure, and increasingly split into smaller provider families (provider-aws-s3, provider-aws-rds, etc.) rather than one enormous provider per cloud:

$ kubectl get providers
NAME                   INSTALLED   HEALTHY   AGE
provider-aws-s3        True        True      10d
provider-aws-rds       True        True      10d
#         ^^^^^^ — installing only the families you actually use, instead
#                  of one monolithic provider-aws with every AWS service's
#                  CRDs installed regardless of whether you use them

The family split exists specifically because a single monolithic cloud provider can register thousands of CRDs — a real, measurable strain on a cluster’s etcd and API server that the ecosystem is still in the process of migrating away from.


⚠ Production Gotchas

Crossplane reconciles continuously — a manual change to a cloud resource outside Crossplane gets reverted on the next loop. Teams coming from Terraform’s plan/apply model, where nothing changes until you explicitly run apply again, are frequently surprised the first time a manual “quick fix” in the AWS console gets silently undone minutes later.

Monolithic providers can register thousands of CRDs, and that has a real, measurable etcd and API-server cost. If you’re on an older, non-family provider version and seeing API server memory pressure, check CRD count before assuming it’s an unrelated capacity issue.

Deleting a Composition’s underlying claim doesn’t always tear down cleanly if finalizers on the Managed Resources are stuck — a Managed Resource that failed to delete cleanly from the cloud side (a non-empty S3 bucket, for instance) will block the whole claim’s deletion until that’s resolved manually.


Quick Reference

kubectl get managed                        # every Managed Resource, all providers
kubectl get compositeresourcedefinitions   # XRDs — the abstract APIs defined
kubectl get compositions                   # the mapping logic behind each XRD
kubectl get providers                       # installed providers + health
crossplane beta render <xr> <comp> <fns>    # render a Composition locally, no cluster needed
kubectl describe <managed-resource-kind> <name>   # sync status + underlying cloud errors

Contribution Opportunity: Migrating Providers to the Family Pattern

The limitation: Not every Crossplane provider has migrated from the older, monolithic-per-cloud model to the smaller “provider family” pattern that registers only the CRDs for services actually in use. Clusters running an un-migrated provider carry the etcd and API-server overhead of thousands of unused CRDs, and this is a known, actively-discussed problem in the Crossplane community — not a hypothetical one.

Why it’s hard to fix: Splitting a monolithic provider into families isn’t a mechanical find-and-replace — it means restructuring code generation, versioning, and release processes for every resource type the provider covers, while keeping a migration path that doesn’t break existing users who depend on the old provider’s CRDs. It’s real, unglamorous engineering work that has to happen provider-by-provider, cloud-by-cloud, and each provider’s maintainer bandwidth varies.

What a contribution-shaped fix looks like: The Crossplane and Upbound-maintained provider repositories publicly track which providers still need family-splitting — this is documented, wanted work, not a gap you’d have to go discover yourself. A concrete starting contribution: pick one still-monolithic provider (checking the project’s own tracking issues for an unclaimed one), and work through the documented family-split process the already-migrated providers (like provider-aws) used as a reference implementation. This is real upstream OSS work with an existing template to follow, not a design problem you have to solve from scratch.


Key Takeaways

  • Crossplane’s Managed Resources make individual cloud resources real Kubernetes CRDs, continuously reconciled rather than applied once
  • Compositions and XRDs are the actual value proposition: platform teams define a simple, opinionated API once; app teams self-serve against it without needing to know what’s underneath
  • Composition Functions replace older YAML patch-and-transform templating with real executable logic — a genuinely evolving, more flexible part of the project
  • Continuous reconciliation means manual out-of-band changes get reverted — a real behavioral difference from Terraform’s plan/apply model, not just a implementation detail
  • The provider family migration is documented, wanted, achievable contribution work — not a gap you’d need to discover on your own

What’s Next

Crossplane’s composition model and Terraform’s HCL module model solve the same underlying problem — reusable, parameterized infrastructure definitions — from genuinely different architectural starting points. EP07 puts them side by side and gives a clear recommendation for which fits which team.

Next: EP07 — Crossplane vs Terraform: Composition vs HCL for Infrastructure as Code

Get EP07 in your inbox when it publishes → linuxcent.com/subscribe