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.”
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.
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.
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.
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:
| Condition | Asks | When it is True | Common False reason |
|---|---|---|---|
Synced | Did 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. |
Ready | Does 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.
“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.
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.compositionUpdatePolicy — Automatic (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: credentialsA 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 claimThe 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 checkoutDot 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.
| Layer | What to look at | What a failure here means |
|---|---|---|
Claim (Database) | kubectl describe — events, resourceRef to the XR | No XR created at all → the XRD never became Established, or RBAC blocked it. |
Composite (XDatabase) | Events, compositionRef, resourceRefs | No matching Composition → compositeTypeRef mismatch or referenceable: false. Function errors also surface here. |
| Managed Resources | kubectl get managed, then describe the one that is not Ready | The actual answer, ~90% of the time: a real cloud API error in the Synced condition message. |
| Provider pod | kubectl 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 (v1beta1 → v1) 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.
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
| Option | Model | Best when | Costs you |
|---|---|---|---|
| Crossplane | Kubernetes control plane; continuous reconciliation; composition into your own APIs | You want self-service infrastructure APIs, drift that heals itself, and infra reconciled by the same GitOps engine as your apps | A Kubernetes-shaped learning curve; the cluster becomes tier-0 and holds cloud credentials; Composition authoring is a real skill |
| Terraform / OpenTofu | Run-to-completion CLI with external state | Provisioning the cluster Crossplane will live in; one-off or rarely-changing estates; the widest provider ecosystem and hiring pool | No drift correction between runs; state files to lock and protect; self-service needs a pipeline wrapper |
| Pulumi / AWS CDK | Run-to-completion, in a general-purpose language | Teams that want real loops, types and tests around infrastructure code | Same 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-built | You are all-in on one cloud and want first-party CRDs with no extra abstraction layer | No composition layer, so no platform API — you can still put Crossplane over them |
| Cluster API | Continuously reconciling, but scoped to cluster lifecycle | Creating and upgrading the clusters themselves | Not an alternative — a complement. CAPI makes clusters; Crossplane makes everything the clusters need |
| Backstage | A portal, not a provisioner | Putting a form and a catalogue in front of the API | Not 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.
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
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 providers → kubectl get claim,composite,managed → crossplane beta trace → kubectl 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.
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
- 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.
spec.forProvideris the desired configuration you wrote;status.atProvideris the observed configuration the provider last read back from the cloud API (including generated fields like an endpoint or ARN).- Look at the
Syncedcondition’s message on that Managed Resource (kubectl describe), then the provider pod’s logs.Synced=Falsemeans 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. (ContrastSynced=True, Ready=False, which just means the cloud is still provisioning.) - 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.
kubectl get providers(are they INSTALLED and HEALTHY?) →crossplane beta trace database orders-db -n checkout→ or manuallykubectl describe database orders-db -n checkout, thenkubectl get managed, thenkubectl describethe 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.- Because every edit to a Composition creates a new
CompositionRevision, and XRs set toAutomaticimmediately follow it — so one commit can start changing every database in the fleet at once.Manualpins each XR to a revision so upgrades are deliberate, reviewable and rollback-able. - You can’t —
docs.crossplane.iois not on the exam allowlist (kubernetes.io/docs, kubernetes.io/blog, task-specific Quick Reference links, and local man//usr/sharedocs only). Write it from memory, and usekubectl explainandkubectl api-resourcesagainst the cluster’s installed CRDs. Drill it on Know Cold.