Tools · Crossplane

Crossplane

Crossplane is a CNCF control plane that teaches your Kubernetes cluster to speak cloud: an RDS database, a GCS bucket, an Azure virtual network or a Cloudflare DNS record becomes an ordinary Kubernetes object with a spec, a status and a controller that reconciles it forever. On top of that it lets a platform team define its own APIs — kind: Database, kind: Environment — so developers ask for infrastructure in nine lines of YAML instead of filing a ticket. It solves the platform problem of “our infrastructure is provisioned by a tool that runs once, drifts silently, and needs a human to run it again.”

☺ Explain it like I’m 10

Imagine a vending machine at school. Behind the glass, a machine knows how to make a real sandwich: get bread, get cheese, toast it, wrap it. On the front there is one big button that just says SANDWICH. You press the button; the machine does the twelve fiddly steps. And here is the magic bit — if someone sneaks in at night and steals the cheese out of your sandwich, the machine notices and puts new cheese back. Crossplane is that machine for cloud computers: the platform team builds the insides once, developers press the button, and a tireless robot keeps checking that what came out still matches what you asked for.

🦋🤖Your hosts for this topic: Mira the Butterfly & Recon the Robot — Mira turns the ugly raw cloud YAML “caterpillar” into a beautiful one-button self-service “butterfly” (the XRD and the Composition), and Recon is the provider’s reconcile loop that makes every request come true in the real cloud and stay true.

What Crossplane is and the problem it solves

☺ Like you’re 10: It makes your cluster able to build things outside itself — real cloud stuff — and then keep them fixed.

Crossplane is a CNCF incubating project that installs into a cluster (conventionally the crossplane-system namespace) and turns that cluster into a universal control plane. It does this with the same two ingredients you met in Platform APIs & Operators: a CustomResourceDefinition that teaches the API server a new kind, and a controller that runs a reconcile loop against that kind. Crossplane’s twist is that the controller’s reconcile target is not a pod — it is an external API. Apply a YAML file describing an S3 bucket and a bucket appears in AWS. Delete the bucket in the console and, on the next reconcile, it comes back.

The problem before Crossplane

Classic infrastructure-as-code is run-to-completion: someone (or a pipeline) executes terraform apply, the world is changed, the process exits, and nothing watches the result until the next run. Three headaches follow. Drift is invisible between runs — a console click on Friday is discovered on Tuesday. State lives in a separate file or backend that must be locked, backed up and protected. And the interface is a CLI plus credentials, so “give the payments team a database” means either giving them cloud access or giving them a ticket queue. Crossplane replaces all three with something Kubernetes already gives you for free: a declarative API object, a level-triggered controller, and RBAC.

◆ Key idea

The single sentence to carry into the exam: Terraform runs and exits; Crossplane runs forever. Terraform reconciles at the moment you invoke it and records the answer in state; Crossplane reconciles continuously and stores the desired state in etcd as ordinary Kubernetes objects, with the live cloud state mirrored back into status. That is why Crossplane self-heals infrastructure drift and Terraform does not.

Composition: the part that makes it a platform

Raw cloud resources are still low-level — an RDS instance has dozens of fields no application developer should have to think about. Crossplane’s real power is composition: the platform team defines a small, opinionated API (a Database with one field, size) and a recipe that expands it into the eight real cloud resources it takes to be safe. Encryption, private subnets and backup retention are baked into the recipe, so the guardrail is the golden path. That is the mechanism behind most of what Self-Service & Golden Paths promises.

What it is not

Crossplane is not a GitOps engine — it has no opinion about where your YAML comes from, so you pair it with Argo CD or Flux, which sync claims into the cluster like any other manifest. It is not a developer portal: the friendly form with pictures is Backstage, and Crossplane is the machinery behind the button. It is not a cluster provisioner in the usual sense — that is Cluster API, although a Crossplane provider can create a managed cluster (EKS, GKE, AKS) perfectly well. And it is not a secrets manager; see Secrets Management and External Secrets Operator.

Where it fits in a platform

☺ Like you’re 10: It sits underneath everything, building the roads and warehouses that the apps get parked in.

In the layered model from Platform Architecture, Crossplane straddles two planes. Its bottom half is the resource / infrastructure plane — providers actually calling cloud APIs. Its top half is the developer control plane — the XRDs that publish your platform’s own APIs. That double life is exactly why Crossplane sits at the centre of so many internal developer platforms: one tool both owns the infrastructure and exposes the abstraction over it.

Its neighbours

Upstream, a GitOps controller syncs the claims — because a claim is just a Kubernetes object, infrastructure and applications flow through the same pipeline and the same audit trail, which is the unification GitOps Workflows argues for. Beside it, Kyverno or OPA Gatekeeper validate claims at admission — “no database without a cost-centre label” is a policy, not a code review. Below it, Cluster API handles cluster lifecycle while Crossplane handles everything a cluster needs around it; both are covered together in IaC & Control Planes. Downstream, connection details land in a Kubernetes Secret that the workload mounts, and OpenCost or cloud tagging closes the FinOps loop on what all this costs.

CNPE domain relevance

Crossplane is named on the official CNPE tool list. It lands hardest in Platform APIs & Self-Service (25%) — publishing a platform API a developer can consume without a ticket — and in Platform Architecture & Infrastructure (15%), because it is the control-plane pattern the blueprint keeps describing. It also touches GitOps & Continuous Delivery (25%), because claims are reconciled from Git like any other manifest, and Security & Policy (15%), because a ProviderConfig holding cloud credentials is one of the most sensitive objects on the cluster. Work it alongside the platform-API practice tasks and the exam guide.

How it works — architecture and the API surface

☺ Like you’re 10: There is a small brain that reads recipes, and a plug-in for each cloud that does the actual phoning.

A Crossplane install is smaller than people expect: a core deployment plus one deployment per installed provider. Everything else is CRDs.

🦆 Claim namespaced · Database XR (composite) cluster-scoped · XDatabase 🦋 Platform team authors once XRD = the API shape Composition = the recipe Crossplane core · composition engine runs the function pipeline · pins a CompositionRevision · owns the XR 🤖 provider-aws MR: rds Instance MR: SubnetGroup 🤖 provider-kubernetes MR: Object ProviderConfig · creds ☁️ Real cloud API the only source of truth creates composes Managed Resources observe → status.atProvider · conditions Ready & Synced · repeat forever

Packages: Providers, Functions and Configurations

Crossplane distributes everything as OCI images called packages (xpkg), installed with three CRDs in the pkg.crossplane.io group. A Provider installs the CRDs and controllers for one external API — provider-aws-rds, provider-gcp-storage, provider-kubernetes, provider-helm. A Function installs a composition function used in a Composition pipeline. A Configuration bundles your own XRDs and Compositions so a platform API is itself versioned and shippable. A DeploymentRuntimeConfig tunes how a provider pod runs (service account, resources, extra args) — that is where you attach IRSA / Workload Identity instead of a static key.

Managed Resources — forProvider, atProvider, Ready and Synced

A Managed Resource (MR) is the atom: one Kubernetes object representing exactly one external resource. Its shape is always the same and is worth memorising. spec.forProvider is what you want the cloud thing to look like. status.atProvider is what the provider last observed it to actually look like. spec.providerConfigRef says which credentials to use. And two conditions tell you everything about its health:

ConditionAsksWhen it is TrueCommon False reason
SyncedDid Crossplane manage to talk to the cloud about this resource?The last reconcile succeeded (ReconcileSuccess).ReconcileError — bad credentials, missing permission, invalid field, API throttling. Read the message; it is usually the literal cloud error.
ReadyDoes the external resource actually exist and is it available?The provider observed it in an available state.Creating (still provisioning — an RDS instance takes minutes), Unavailable, or Deleting.

Synced=True, Ready=False means “we asked correctly, the cloud is still working on it” — wait. Synced=False means “we could not even ask properly” — go read the message now. That distinction is the single highest-value debugging skill on this page, and it rhymes exactly with the sync-versus-health split on Argo CD.

🦆 Dot’s-eye view

“I used to open a ticket titled ‘please make me a Postgres’ and wait four days. Now I commit ten lines to my team’s folder, Argo syncs it, and about six minutes later a Secret called orders-db-conn appears in my namespace with a host and a password in it. I have never once opened the AWS console, and I could not tell you what a subnet group is. That is the point.”

XRD, XR, Claim and Composition

Four names, one idea. The CompositeResourceDefinition (XRD) is the schema of a new platform API — it generates the CRDs. The Composite Resource (XR) is an instance of that API; it is cluster-scoped and holds the machinery. The Claim is the optional namespaced face of the same API, created when the XRD sets claimNames — a developer writes the Claim, Crossplane creates a matching XR behind it, and connection details are copied back down into the developer’s namespace. The Composition is the recipe that says which Managed Resources an XR expands into.

◆ Key idea · claim vs composite

Same API, two scopes, two audiences. The XR is cluster-scoped and is what the platform team and the composition engine work with. The Claim is namespaced and is what a developer can be given RBAC on without granting anything cluster-wide. One Claim ⇄ exactly one XR. Deleting the Claim deletes the XR, which deletes the Managed Resources, which — unless you said otherwise — deletes the real cloud resources. Crossplane v2 makes XRs themselves namespaceable and treats claims as a legacy compatibility mode, but the claim model is what current course material, most Compositions in the wild, and the exam expect you to recognise.

Composition Revisions, Functions and EnvironmentConfigs

Three supporting pieces round it out. Every time you edit a Composition, Crossplane records an immutable CompositionRevision; each XR pins to one via spec.compositionUpdatePolicyAutomatic (follow the latest revision) or Manual (stay put until a human bumps compositionRevisionRef). Set production XRs to Manual and you get a reviewable, rollback-able upgrade instead of “I edited the recipe and forty databases started changing.” Composition Functions are the modern way to write the recipe: instead of a static list of resources with patches, mode: Pipeline runs an ordered list of function containers that each receive and return the desired state — function-patch-and-transform for the familiar declarative style, plus KCL, Python, Go templating and CEL functions when you need real logic like loops and conditionals. EnvironmentConfig holds shared per-environment data (VPC IDs, account numbers, region defaults) that a Composition can look up, so one recipe serves dev and prod without being copy-pasted.

The resources you will actually write

☺ Like you’re 10: Here is the real YAML — one file to plug in a cloud, one to describe the button, one recipe, and the tiny thing a developer writes.

A Provider and its credentials

Nothing works until a provider is installed and healthy and pointed at credentials. This is step zero and the source of most “why is nothing happening?” hours.

apiVersion: pkg.crossplane.io/v1
kind: Provider
metadata:
  name: provider-aws-rds
spec:
  package: xpkg.upbound.io/upbound/provider-aws-rds:v1.14.0   # pin the version
  packagePullPolicy: IfNotPresent
---
# The credentials. In real life prefer IRSA / Workload Identity over a static key.
apiVersion: v1
kind: Secret
metadata:
  name: aws-creds
  namespace: crossplane-system
type: Opaque
stringData:
  credentials: |
    [default]
    aws_access_key_id = AKIAEXAMPLE
    aws_secret_access_key = REPLACE_ME
---
# ProviderConfig binds a provider to credentials. Managed Resources reference it by name.
# Its CRD comes from the provider family package, not from Crossplane core.
apiVersion: aws.upbound.io/v1beta1
kind: ProviderConfig
metadata:
  name: default            # MRs with no providerConfigRef use the one named "default"
spec:
  credentials:
    source: Secret         # other sources exist per provider, e.g. IRSA / WebIdentity
    secretRef:
      namespace: crossplane-system
      name: aws-creds
      key: credentials
⚠ The ProviderConfig is the crown jewels

A ProviderConfig points at a Secret that can create and destroy real cloud infrastructure. Give it least privilege, never cluster-admin-of-the-cloud. Prefer workload identity (IRSA on EKS, Workload Identity on GKE) so no long-lived key exists at all. Use a separate ProviderConfig per environment or tenant and restrict which ones a Composition may reference — otherwise a dev-tier claim can quietly build in the production account. See Security & Policy.

The XRD — your platform’s API

The XRD is where you decide what developers are allowed to ask for. Everything you leave out of the schema is a decision you have made on their behalf — which is the whole point.

apiVersion: apiextensions.crossplane.io/v1
kind: CompositeResourceDefinition
metadata:
  name: xdatabases.platform.acme.io      # MUST be <plural>.<group>
spec:
  group: platform.acme.io
  names:
    kind: XDatabase                      # the composite — cluster-scoped
    plural: xdatabases
  claimNames:
    kind: Database                       # the developer-facing claim — namespaced
    plural: databases
  defaultCompositionRef:
    name: rds-postgres                   # used when a claim doesn't pick one
  connectionSecretKeys: [host, port, username, password]
  versions:
    - name: v1alpha1
      served: true
      referenceable: true                # a Composition may target this version
      schema:
        openAPIV3Schema:
          type: object
          properties:
            spec:
              type: object
              properties:
                parameters:
                  type: object
                  properties:
                    size:
                      type: string
                      enum: [small, medium, large]     # the ONLY choice devs get
                      default: small
                    engineVersion:
                      type: string
                      default: "16"
                  required: [size]
              required: [parameters]
            status:
              type: object
              properties:
                endpoint: { type: string }             # surfaced back to the claim

The Composition — the recipe, in pipeline mode

Modern Compositions use mode: Pipeline and delegate the work to functions. Install the functions first as Function packages, exactly like providers.

apiVersion: pkg.crossplane.io/v1
kind: Function
metadata:
  name: function-patch-and-transform
spec:
  package: xpkg.upbound.io/crossplane-contrib/function-patch-and-transform:v0.7.0
---
apiVersion: pkg.crossplane.io/v1
kind: Function
metadata:
  name: function-auto-ready              # marks the XR Ready when composed MRs are Ready
spec:
  package: xpkg.upbound.io/crossplane-contrib/function-auto-ready:v0.4.1
---
apiVersion: apiextensions.crossplane.io/v1
kind: Composition
metadata:
  name: rds-postgres
spec:
  compositeTypeRef:
    apiVersion: platform.acme.io/v1alpha1
    kind: XDatabase                      # which XR this recipe serves
  mode: Pipeline
  writeConnectionSecretsToNamespace: crossplane-system   # where the XR's own secret lands
  pipeline:
    - step: build-resources
      functionRef: { name: function-patch-and-transform }
      input:
        apiVersion: pt.fn.crossplane.io/v1beta1
        kind: Resources
        resources:
          - name: instance
            base:
              apiVersion: rds.aws.upbound.io/v1beta1
              kind: Instance
              spec:
                forProvider:
                  region: eu-west-1
                  engine: postgres
                  username: postgres
                  # the master password is never inline — it is read from a Secret
                  passwordSecretRef:
                    namespace: crossplane-system
                    name: rds-master-password
                    key: password
                  allocatedStorage: 20
                  storageEncrypted: true            # guardrails baked into the recipe
                  publiclyAccessible: false
                  backupRetentionPeriod: 7
                  skipFinalSnapshot: false
                  dbSubnetGroupNameSelector:
                    matchControllerRef: true        # bind to the SubnetGroup we compose below
                writeConnectionSecretToRef:
                  namespace: crossplane-system      # the name is patched in below
            patches:
              - type: FromCompositeFieldPath
                fromFieldPath: spec.parameters.size
                toFieldPath: spec.forProvider.instanceClass
                transforms:
                  - type: map
                    map: { small: db.t3.micro, medium: db.t3.medium, large: db.r5.large }
              - type: FromCompositeFieldPath
                fromFieldPath: spec.parameters.engineVersion
                toFieldPath: spec.forProvider.engineVersion
              - type: FromCompositeFieldPath             # one secret per XR, not one shared
                fromFieldPath: metadata.uid
                toFieldPath: spec.writeConnectionSecretToRef.name
                transforms:
                  - type: string
                    string:
                      type: Format
                      fmt: "%s-rds"
              - type: ToCompositeFieldPath                # push the real endpoint back up
                fromFieldPath: status.atProvider.endpoint
                toFieldPath: status.endpoint
            connectionDetails:
              - name: host                                # atProvider.address is the hostname;
                type: FromFieldPath                       # atProvider.endpoint is host:port
                fromFieldPath: status.atProvider.address
              - name: port
                type: FromFieldPath
                fromFieldPath: status.atProvider.port
              - name: username
                type: FromFieldPath
                fromFieldPath: spec.forProvider.username
              - name: password
                type: FromConnectionSecretKey
                fromConnectionSecretKey: password
          - name: subnet-group
            base:
              apiVersion: rds.aws.upbound.io/v1beta1
              kind: SubnetGroup
              spec:
                forProvider:
                  region: eu-west-1
                  subnetIdSelector:
                    matchLabels: { tier: private }
    - step: ready
      functionRef: { name: function-auto-ready }

The Claim — all a developer writes

# Nine lines, committed to the team's GitOps folder like any other manifest.
apiVersion: platform.acme.io/v1alpha1
kind: Database                    # the CLAIM kind from the XRD (namespaced)
metadata:
  name: orders-db
  namespace: checkout
spec:
  parameters:
    size: small
    engineVersion: "16"
  writeConnectionSecretToRef:
    name: orders-db-conn          # host/port/username/password land HERE, in checkout

Dot cannot create an unencrypted, publicly reachable database on this path, because the schema does not offer those words. That is the developer-experience win and the compliance win in one object.

Day-to-day commands

☺ Like you’re 10: A short list of things you type to see whether the machine is working and where it got stuck.

Install and check the packages

helm repo add crossplane-stable https://charts.crossplane.io/stable
helm repo update
helm install crossplane crossplane-stable/crossplane \
  --namespace crossplane-system --create-namespace

kubectl get pods -n crossplane-system          # core + one pod per provider/function

# Packages: ALWAYS check these two columns first — INSTALLED and HEALTHY
kubectl get providers
kubectl get functions
kubectl get configurations
kubectl get pkgrev                             # the underlying package revisions

kubectl describe provider provider-aws-rds     # why HEALTHY=False lives here
kubectl get crds | grep aws.upbound.io | head  # what kinds the provider taught the cluster

Follow a claim all the way down to the cloud

Crossplane registers three categories, which give you the fastest possible triage sweep. Learn these three lines:

kubectl get claim -A          # every developer-facing claim, all namespaces
kubectl get composite         # every XR (cluster-scoped)
kubectl get managed           # EVERY managed resource of every provider, with READY/SYNCED

# The one command that usually ends the investigation — a tree of the whole chain
crossplane beta trace database orders-db -n checkout        # by claim
crossplane beta trace xdatabase orders-db-abc12             # by composite
# (promoted out of beta in newer CLIs: `crossplane trace ...` — same output)

# Manually, when the CLI isn't installed:
kubectl describe database orders-db -n checkout   # events + which XR it created
kubectl describe xdatabase orders-db-abc12        # "Resource composition" events + refs
kubectl get managed -o wide                       # find the composed MR that is not Ready
kubectl describe instance orders-db-abc12-xyz     # THE conditions + the real cloud error

Render and validate before you ship a Composition

# Dry-run the function pipeline locally: what MRs would this XR produce?
crossplane beta render xr.yaml composition.yaml functions.yaml

# Validate a Composition's output against the provider's CRD schemas
crossplane beta validate crds.yaml composition.yaml

# Package your XRDs + Compositions into a versioned Configuration and publish it
crossplane xpkg build --package-root=./package -o platform-apis.xpkg
crossplane xpkg push -f platform-apis.xpkg xpkg.upbound.io/acme/platform-apis:v0.3.0

# Pause reconciliation on one resource without deleting it (invaluable in an incident)
kubectl annotate instance orders-db-abc12-xyz crossplane.io/paused=true

Add these to your muscle memory alongside the rest of the command reference.

Gotchas and failure modes

☺ Like you’re 10: Here are the exact places the machine gets stuck, and how to find the sticky bit fast.

The provider is unhealthy, or has no credentials

Symptom: you apply a Managed Resource and nothing at all happens — no events, no conditions, sometimes not even a rejected object. Three causes, in the order to check them. The provider never became HEALTHY (image pull failure, a version incompatible with your Crossplane core, or an unsatisfied dependency) — kubectl describe provider says which. The CRD does not exist yet, so your MR was rejected by the API server: providers install their CRDs asynchronously, and a big family provider can take a minute. Or the ProviderConfig is missing, misnamed, or points at a Secret with the wrong key — the MR then sits with Synced=False and a credentials error in the message. A resource whose providerConfigRef is omitted looks for one literally named default; if you renamed yours, every MR silently fails.

The claim is never Ready — trace down, never guess

This is the most common real-world Crossplane page. A Database claim sits Ready=False forever and the claim itself tells you almost nothing, because the claim only aggregates. The rule: never debug at the level of the claim. Walk down.

LayerWhat to look atWhat a failure here means
Claim (Database)kubectl describe — events, resourceRef to the XRNo XR created at all → the XRD never became Established, or RBAC blocked it.
Composite (XDatabase)Events, compositionRef, resourceRefsNo matching CompositioncompositeTypeRef mismatch or referenceable: false. Function errors also surface here.
Managed Resourceskubectl get managed, then describe the one that is not ReadyThe actual answer, ~90% of the time: a real cloud API error in the Synced condition message.
Provider podkubectl logs -n crossplane-system deploy/<provider>Throttling, expired credentials, region/endpoint problems that never made it into a condition.

Ninety percent of the time the answer is a permission or quota error sitting in one Managed Resource’s Synced message, invisible from the top. The same descend-the-ownership-chain discipline drives workload triage and the troubleshooting playbook.

Deletion is real, and other sharp edges

kubectl delete on a claim deletes production data. Finalizers cascade the delete through the XR to the Managed Resources to the actual RDS instance. Guard it: set spec.deletionPolicy: Orphan on resources that must outlive their MR, use managementPolicies to run a resource in observe-only mode, protect prod claims with a Kyverno policy, and remember that Argo CD’s prune will happily delete a claim when someone moves a directory. Beyond that: composed resources can be orphaned if you rename a resource entry in a Composition, because Crossplane tracks composed resources by that name — the old MR loses its owner and keeps running (and billing). Editing a live Composition changes every XR set to Automatic, so use Manual in production. Patch paths fail silently when a field path is wrong, producing a resource with a missing value rather than an error — which is precisely what crossplane beta render exists to catch. Provider upgrades can change CRD API versions (v1beta1v1) and break Compositions, so pin versions. And a very large Composition is its own maintenance burden: if it models every knob, you have rebuilt the cloud API with extra steps — an anti-pattern worth naming out loud.

🦫 Benny’s workshop · 25 min

On a throwaway kind cluster, no cloud account needed. Install Crossplane with Helm, then install provider-nop (it fakes external resources) or provider-kubernetes. Confirm kubectl get providers shows INSTALLED=True HEALTHY=True. Write the XRD and a tiny Composition above, apply a claim, and run kubectl get claim,composite,managed to watch the chain appear. Now break it on purpose three ways: delete the ProviderConfig and watch Synced flip to False; typo a fromFieldPath and see the patch fail silently; and delete a composed Managed Resource by hand and watch Crossplane rebuild it within a reconcile interval. Finally run crossplane beta trace on the claim and admire how much time you just saved future-you.

Alternatives and when to choose it

☺ Like you’re 10: Other tools also build cloud things — here is how to pick.

The real question is never “which is best” but “run-to-completion or always-on, and do I need to publish an API on top?”

The comparison that decides it

OptionModelBest whenCosts you
CrossplaneKubernetes control plane; continuous reconciliation; composition into your own APIsYou want self-service infrastructure APIs, drift that heals itself, and infra reconciled by the same GitOps engine as your appsA Kubernetes-shaped learning curve; the cluster becomes tier-0 and holds cloud credentials; Composition authoring is a real skill
Terraform / OpenTofuRun-to-completion CLI with external stateProvisioning the cluster Crossplane will live in; one-off or rarely-changing estates; the widest provider ecosystem and hiring poolNo drift correction between runs; state files to lock and protect; self-service needs a pipeline wrapper
Pulumi / AWS CDKRun-to-completion, in a general-purpose languageTeams that want real loops, types and tests around infrastructure codeSame run-and-exit gap; more power means more ways to write something nobody else can read
Cloud operators (ACK, Config Connector, ASO)Continuously reconciling Kubernetes CRDs, but single-cloud and vendor-builtYou are all-in on one cloud and want first-party CRDs with no extra abstraction layerNo composition layer, so no platform API — you can still put Crossplane over them
Cluster APIContinuously reconciling, but scoped to cluster lifecycleCreating and upgrading the clusters themselvesNot an alternative — a complement. CAPI makes clusters; Crossplane makes everything the clusters need
BackstageA portal, not a provisionerPutting a form and a catalogue in front of the APINot an alternative at all — Backstage is the glass front, Crossplane is the machinery behind it

A practical rule

Choose Crossplane when you have many consumers asking for the same few shapes of infrastructure and you want that request to be a merged pull request rather than a ticket. Stay with Terraform when the estate is provisioned rarely by a small team who are already fluent in it. And note that the two coexist beautifully: Terraform builds the account, the network and the management cluster; Crossplane lives inside that cluster and serves everything above it. The one thing you must not do is let both own the same resource — they will revert each other forever. See IaC & Control Planes for that boundary drawn properly, and The Tool Landscape for where this sits among the named projects.

🎬 At the Platform Guild
🦊

Foxy: So it’s Terraform, but you have to learn Kubernetes first? Bold sales pitch.

🦋

Mira: It’s Terraform that never leaves the room. And the bit Terraform can’t do — I publish a kind: Database with one field, and Dot never sees a subnet group in her life.

🤖

Recon: BEEP. Someone deleted the RDS instance in the console at 02:14. I observed. I recreated. Ready=True at 02:21. Nobody woke up.

👺

Gizmo: Just give the ProviderConfig full admin on the prod account. One Secret, all the clouds, zero tickets. 🤑

🐢

Timmy: One Secret and one bad Composition, Gizmo. Least privilege per environment, workload identity over static keys, and deletionPolicy: Orphan on anything holding data.

🦆

Dot: My claim has said Ready=False for ten minutes. Do I panic?

🦋

Mira: Never panic at the claim — walk down. crossplane beta trace, find the Managed Resource that isn’t Ready, read its Synced message. Yours says the account hit its RDS instance quota. That’s a FinOps conversation, not an outage.

Exam relevance and going further

☺ Like you’re 10: On exam day you can’t open Crossplane’s website — so the shapes have to already be in your head.

Crossplane is on the official CNPE tool list, and it is the most likely vehicle for a “publish a self-service platform API” task. Expect to install or inspect it, diagnose a provider or claim that will not become Ready, read a Composition and say what it creates, and explain — in words — why a continuously reconciling control plane behaves differently from a run-to-completion IaC tool.

The documentation allowlist — read this twice

⚠ Crossplane’s own docs are not available during the exam

During the CNPE the only documentation you may open is kubernetes.io/docs, kubernetes.io/blog, task-specific documentation explicitly linked in the exam’s Quick Reference box, and local man pages and /usr/share docs on the exam machine. docs.crossplane.io is not on that list. Unless a task’s Quick Reference hands you a Crossplane link, you write the XRD, Composition and Claim from memory — and you lean on kubectl explain and kubectl api-resources, which work offline against whatever CRDs the cluster already has. Drill the shapes on Know Cold; that page exists precisely for the manifests you cannot look up.

⚖ CNPA vs CNPE — That allowlist is a CNPE-specific mechanic — it exists only because CNPE is hands-on and a narrow set of live lookups is technically reachable mid-task. CNPA is stricter, not looser: it is a fully closed-book multiple-choice exam with zero external resources and zero lookups of any kind, not even kubernetes.io. Even so, the XRD/Composition/Claim shapes above are exactly the kind of concept-level knowledge CNPA's closed-book recall draws on.

What to be able to do without notes

Name the five pieces (Provider/ProviderConfig, Managed Resource, XRD, Composition, XR and Claim) and say who authors each. Write an XRD from a blank file: apiVersion: apiextensions.crossplane.io/v1, a metadata name of <plural>.<group>, names plus claimNames, and a version marked referenceable: true. Explain spec.forProvider versus status.atProvider, and Ready versus Synced. Explain claim (namespaced, developer) versus composite (cluster-scoped, machinery) and where the connection Secret lands. State the Terraform contrast in one sentence. And know the triage spine cold: kubectl get providerskubectl get claim,composite,managedcrossplane beta tracekubectl describe the unhealthy Managed Resource and read its conditions.

Official resources for after the exam

Outside the exam, start at docs.crossplane.io (the Composition and Composition Functions guides repay slow reading), the project home at crossplane.io, the source and community functions at github.com/crossplane/crossplane and github.com/crossplane-contrib, the package registry at marketplace.upbound.io, and the CNCF landing page at cncf.io/projects/crossplane. Then pair this page with Self-Service & Golden Paths for the product story, IaC & Control Planes for the Terraform boundary, Platform APIs & Operators for the machinery underneath, and the glossary whenever a term stops making sense.

🐢 Timmy’s checkpoint

1. In one sentence, how does Crossplane differ from Terraform? 2. On a Managed Resource, what do spec.forProvider and status.atProvider each hold? 3. A resource shows Synced=False, Ready=False. Where do you look, and what does Synced=False specifically tell you? 4. What is the difference between a Claim and a Composite Resource, and which one does a developer get RBAC on? 5. Your Database claim has been Ready=False for twenty minutes. Give the exact sequence of commands you run. 6. Why set compositionUpdatePolicy: Manual on production XRs? 7. During the exam, where do you look up the XRD schema?

Check your answers
  1. Terraform is run-to-completion — it reconciles when invoked, then exits, so drift goes uncorrected until the next run. Crossplane is a control plane — controllers reconcile the cloud against Kubernetes objects continuously and forever, so deleted or altered infrastructure heals itself.
  2. spec.forProvider is the desired configuration you wrote; status.atProvider is the observed configuration the provider last read back from the cloud API (including generated fields like an endpoint or ARN).
  3. Look at the Synced condition’s message on that Managed Resource (kubectl describe), then the provider pod’s logs. Synced=False means the reconcile itself failed — Crossplane could not successfully talk to the cloud about the resource — so it is almost always credentials, a missing IAM permission, an invalid field, or throttling. (Contrast Synced=True, Ready=False, which just means the cloud is still provisioning.)
  4. The Claim is namespaced and is the developer-facing handle; the Composite Resource (XR) is cluster-scoped and holds the composed resource references. One claim maps to exactly one XR. Developers get RBAC on the Claim, which is precisely why claims exist — no cluster-scoped grant required.
  5. kubectl get providers (are they INSTALLED and HEALTHY?) → crossplane beta trace database orders-db -n checkout → or manually kubectl describe database orders-db -n checkout, then kubectl get managed, then kubectl describe the Managed Resource that is not Ready and read its conditions → if still unclear, kubectl logs -n crossplane-system deploy/<provider>. Always walk down the chain; never diagnose at the claim.
  6. Because every edit to a Composition creates a new CompositionRevision, and XRs set to Automatic immediately follow it — so one commit can start changing every database in the fleet at once. Manual pins each XR to a revision so upgrades are deliberate, reviewable and rollback-able.
  7. You can’t — docs.crossplane.io is not on the exam allowlist (kubernetes.io/docs, kubernetes.io/blog, task-specific Quick Reference links, and local man//usr/share docs only). Write it from memory, and use kubectl explain and kubectl api-resources against the cluster’s installed CRDs. Drill it on Know Cold.