Crossplane is the control-plane way to build the abstraction from section 3.1: you define a new API (XRD), write the recipe that expands it into real resources (Composition), and developers create instances (XR) that reconcile forever like any operator-managed object.

needsmake up api

Orientation

competency 3.4 · automation frameworks for self-service

The lab's three files in examples/crossplane/ are one complete platform API: AppEnvironment in, namespace plus quota out. Read them in the order XRD → Composition → XR and the whole model falls out.

The one-line model

XRD = the API you promise (schema + names). Composition = the recipe that fulfils it (a pipeline of functions producing composed resources). XR = an instance a developer creates. Crossplane generates the CRD from the XRD, then reconciles each XR by running the composition and applying the result. It does that forever, so drift on the composed resources is corrected like any other controller's.

The v2 model, because versions matter here

what changed, and why online examples mislead

This lab runs Crossplane v2, and the differences from v1 content you may find online are exactly the things that will bite:

Topicv1 (most tutorials)v2 (this lab)
ScopeXRs are cluster-scopedXRs can be namespaced (spec.scope: Namespaced in the XRD)
Developer-facing objecta Claim (namespaced) referencing a cluster XRno claims for the v2 scopes: developers create the XR directly; scope: LegacyCluster still keeps claim behaviour for migrating v1 APIs
Composition styleinline resources[] with patch listsmode: Pipeline, a list of functions
When docs and the cluster disagree, the cluster wins

kubectl explain composition.spec settles it in three seconds. Practising that recovery is worth more than memorising either dialect, because every fast-moving tool in this curriculum will eventually hand you a version-skew problem.

Walk the chain in the files

  • xrd.yaml defines schema and names for AppEnvironment; this is what generated the CRD you inspected in 3.2. The schema here is the same OpenAPI you hand-wrote there, which is the payoff of doing 3.2 first.
  • composition.yaml declares compositeTypeRef back to that kind, then a pipeline: function-patch-and-transform renders two provider-kubernetes Objects (one wrapping a Namespace, one a ResourceQuota) with FromCompositeFieldPath patches pulling spec.team and the quota fields from the XR, plus one ToCompositeFieldPath patch flowing the namespace name back into XR status. Then function-auto-ready marks the XR Ready when its composed resources are.
  • xr.yaml is the ten-line developer experience the other two files buy.
XRD AppEnvironment ──generates──▶ CRD appenvironments.platform.lab.local
                                        ▲
XR team-c-dev  spec: {team, cpuQuota, memoryQuota} ─┘
   │  reconciled by Crossplane through…
   ▼
Composition (mode: Pipeline)
   ├─ function-patch-and-transform ─▶ Object/ns    → Namespace team-c
   │                                   Object/quota → ResourceQuota tenant-quota
   └─ function-auto-ready ──────────▶ XR Ready when composed resources are Ready

Providers, functions, and package health

Crossplane's own extension points are packages: Providers (controllers for external APIs: AWS, GCP, Kubernetes, Helm) and Functions (the pipeline steps that render composed resources). A ProviderConfig (or ClusterProviderConfig) tells a provider how to authenticate.

Step zero of any Crossplane diagnosis: kubectl get providers.pkg.crossplane.io,functions.pkg.crossplane.io; every package must be Installed and Healthy before anything downstream can possibly work. A composition referencing a function that never came up produces an XR stuck with a fairly opaque message, and thirty seconds of package checking beats twenty minutes of manifest staring.

Two gotchas the lab already paid for
  1. A namespaced XR may not compose cluster-scoped resources (ownerReferences forbid it), so the composition uses the namespaced Object variant (kubernetes.m.crossplane.io, not kubernetes.crossplane.io) even though the manifest inside it is a cluster-scoped Namespace. Wrong-variant errors read as cannot apply cluster scoped composed resource.
  2. That namespaced variant authenticates via ClusterProviderConfig, not ProviderConfig. Same words, different kind, and the failure is an auth error rather than a schema error.

Now you have seen both sentences before the exam has.

Operating a Crossplane platform API

read paths and failure bubbles

Failure bubbles bottom-up: a composed resource fails, its condition says why, the XR's Ready goes False with a summarised message, and the developer sees only the XR. So the read path for every Crossplane incident is: XR conditions → composed resource conditions → the underlying object's own error.

crossplane resource trace appenvironment team-c-dev    # the whole tree with statuses, one shot
kubectl get appenvironment team-c-dev -o jsonpath='{.status.conditions}' | jq
kubectl -n default get objects.kubernetes.m.crossplane.io -o yaml | grep -A3 message
kubectl get providers.pkg.crossplane.io,functions.pkg.crossplane.io

Two more operational facts worth carrying:

  • Composition selection. An XR picks its composition by compositionRef, by compositionSelector labels, or by the XRD's default. When the wrong recipe runs, that is where to look, not in the composition itself.
  • Changing the recipe changes existing instances. Edit a composition and every XR using it reconciles to the new shape without anyone touching them. That is the platform team's side of the contract, and it is also the blast radius: a bad composition edit hits every tenant at once. Version compositions (and use compositionRevision policies) when that matters.
Crossplane vs an operator, in one sentence

An operator encodes domain behaviour in Go (failover, backups, upgrades); Crossplane encodes composition in YAML (this API expands into those resources). If the task is "keep Postgres alive", you want an operator. If the task is "offer teams a one-field way to ask for a Postgres, a namespace and a quota together", you want Crossplane wrapping that operator.

Exercises

tick the dot when its check passes

kubectl apply -f examples/crossplane/xr.yaml (or confirm team-c-dev exists from install), then trace the expansion:

kubectl get appenvironment team-c-dev -o jsonpath='{.status.conditions}' | jq
kubectl get appenvironment team-c-dev -o jsonpath='{.status.namespace}{"\n"}'
kubectl -n default get objects.kubernetes.m.crossplane.io
kubectl get ns team-c && kubectl -n team-c get resourcequota tenant-quota -o jsonpath='{.spec.hard}'
verify: Ready True, status.namespace says team-c, two composed Objects, and the quota's hard values match the XR's spec (cpuQuota: "4"), not the composition's placeholders. That last check proves the patches ran, which is the difference between "installed" and "works".

Edit the composition to also set requests.memory default differently or to add a label to the namespace, apply, and watch existing XRs reconcile to the new recipe without anyone touching them.

verify: kubectl get ns team-c --show-labels gains your label within a minute. Compositions are the platform team's side of the contract; this exercise is why that separation matters.

Add podQuota (integer, default 10) to the XRD schema, patch it into the quota's spec.hard[pods] in the composition, and set it in a new XR team-f-dev. Order matters: XRD first, wait Established, then composition, then XR.

verify: kubectl -n team-f get resourcequota tenant-quota -o jsonpath='{.spec.hard.pods}' prints your number. You have now done a schema change, a recipe change, and a consumer change as three separate actors, which is the whole platform-API workflow.

Create an XR with team: Team-G (capital letter, invalid DNS label for a namespace). The XR will not go Ready. Find the failure without guessing: XR conditions first, then the composed Object's conditions (kubectl -n default get objects.kubernetes.m.crossplane.io -o yaml | grep -A3 message), where the API server's rejection of the namespace name surfaces. crossplane resource trace appenvironment <name> (the CLI is installed) draws the whole tree with statuses in one shot.

verify: you can point at the exact condition message and fix the XR. Failure bubbles bottom-up through composed-resource conditions; that is the read path for every Crossplane incident.

Self-check

answer before opening
Name the three objects and who owns each.

XRD (platform team: the promised API and its schema), Composition (platform team: the recipe, changeable without touching the API), XR (developer: an instance). The generated CRD is Crossplane's doing, not yours, which is the whole labour saving.

An XR is stuck not-Ready and the message is unhelpful. Diagnostic order?

Packages first (providers, functions Installed + Healthy), then crossplane resource trace, then the composed resources' own conditions, then the underlying API error inside them. Bottom-up, because that is the direction failure propagates.

Why can a namespaced XR not compose a cluster-scoped resource directly?

Ownership rules: a namespaced object cannot own a cluster-scoped one, and Crossplane relies on ownerReferences for lifecycle. The workaround the lab uses is a namespaced Object (provider-kubernetes) that contains the cluster-scoped manifest, authenticated with a ClusterProviderConfig.

You edit a Composition. What happens to the twelve XRs already using it?

They all reconcile to the new recipe, without anyone touching them: powerful and blast-radius-shaped. If that matters, pin instances to a composition revision or version the composition and migrate deliberately.

When would you choose kro or a plain operator over Crossplane?

kro when you want the same declarative expansion with far less machinery and can accept a young API (3.6). A plain operator when the value is domain behaviour over time (failover, backup, upgrade orchestration) rather than composing existing resources. Crossplane's sweet spot is composing many things, including external cloud APIs, behind one abstraction.

Docs to know your way around

study time, not exam time
  • docs.crossplane.io: Composite Resource Definitions, Compositions (the function pipeline page), and provider-kubernetes's Object documentation on the Upbound marketplace.
  • Offline: crossplane resource trace --help, the single most useful diagnostic in the ecosystem, plus kubectl explain composition.spec and kubectl explain xrd.spec.