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 provider — provider-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