Platform APIs and Provisioning Infrastructure
Worth 12% of the six-domain CNPA blueprint — fourth-largest of the six, behind Core Fundamentals, Observability/Security/Conformance and Continuous Delivery — and the one that explains how a platform is actually built. Everything else on the exam rests on one mechanism: Kubernetes is not a container runner, it is an extensible API with control loops attached. Learn that and this domain’s four competencies collapse into one idea — add nouns to the API (CustomResourceDefinitions), attach robots that make those nouns real (controllers and operators), then point the robots at anything: Deployments, cloud databases, whole clusters.
Imagine a toy shop where you can invent a brand-new kind of order slip. Today it only understands “I want a teddy bear.” You teach it a slip that says “I want a birthday party” — and you hire a tireless helper who reads every such slip and goes off to book the room, bake the cake and post the invitations. Nobody rebuilt the shop; you taught it a new word and gave that word a helper. CRDs are the new word. Operators are the helper.
What this domain is really testing
☺ Like you’re 10: Four exam bullets, one idea — teach Kubernetes new words, and give each word a robot.
The curriculum lists exactly four competencies here, each with its own section below: Kubernetes Reconciliation Loop, APIs for Self-Service Platforms (CRDs), Infrastructure Provisioning with Kubernetes, and Kubernetes Operator Pattern for Integration. In that order they tell a story: the loop is the engine, CRDs are the vocabulary, provisioning is the payoff, and the operator pattern is the packaging.
For context, the published CNCF curriculum weights six domains: Platform Engineering Core Fundamentals 36%, Platform Observability, Security, and Conformance 20%, Continuous Delivery & Platform Engineering 16%, Platform APIs and Provisioning Infrastructure 12%, IDPs and Developer Experience 8%, and Measuring your Platform 8% — 100% in total. This page is the fourth.
Why the API, not the container, is the point
Most engineers meet Kubernetes as “the thing that runs my containers.” That framing is a trap here. A Deployment is not special: it is a record in etcd, watched by a controller that creates ReplicaSets, watched by another that creates Pods. Container-running is a consequence. What Kubernetes offers a platform team is a uniform, declarative, RBAC-protected, auditable API surface you can add your own resources to.
Kubernetes = declarative API + control loops. Every capability you add has the same two-part shape: a resource describing what you want, and a controller that keeps making it true. Apply that one sentence to an unfamiliar example and you can answer most questions in this domain.
Declarative versus imperative
An imperative instruction says do this step — kubectl create deployment web --image=nginx. A declarative one says this is the end state I want. Only the declarative form makes reconciliation possible, because an imperative command leaves nothing behind to compare reality against.
The Kubernetes reconciliation loop
☺ Like you’re 10: The robot repeats three steps forever — look at what you asked for, look at what’s really there, fix the difference.
A controller runs one loop: read the desired state (usually .spec), observe the actual state, diff, act to close the gap, and write what it observed into .status. Then it does it again. And again. This is the reconciliation loop, the single most examinable concept in the domain.
spec is desire, status is observation
Nearly every Kubernetes object splits into two halves, and the exam loves the distinction. .spec is written by the user and states intent (“three replicas”). .status is written by the controller and reports observation (“three ready”). Users never write status; controllers never invent spec. Custom resources are identical: a Database has spec.engine: postgres and, once provisioned, status.conditions[Ready]=True.
Level-triggered, not edge-triggered
An edge-triggered system reacts to an event: “a Deployment was created — go make pods.” If that reaction fails, nothing fixes it. A level-triggered system repeatedly compares current level against target, so a missed event, a crashed controller or a hand-deleted resource all self-correct on the next pass — precisely why the platform is self-healing. Two properties follow, both fair game: reconciliation must be idempotent (ten runs are indistinguishable from one), and the system is eventually consistent, so “not ready yet” is normal rather than an error.
Watches, informers and the work queue
A controller does not hammer the API server in a while true loop. It opens a watch — a long-lived stream of change events — feeding an informer that caches objects in memory, and drops keys onto a de-duplicating, rate-limited work queue (fifty rapid edits collapse into one reconcile; failures retry with backoff). Recognise the words; CNPA won’t ask you to write the plumbing.
“The controller runs when I apply the manifest.” No — it runs continuously; applying a manifest only changes what it compares against. That is why hand-editing a managed resource with kubectl edit gets silently undone, why deleting a pod from a Deployment brings a new one back, and why a GitOps agent with self-heal reverts your emergency fix. All the same loop, doing its job.
APIs for self-service platforms (CRDs)
☺ Like you’re 10: A CRD teaches the cluster a brand-new word. After that, kubectl get understands it just like it understands pods.
A CustomResourceDefinition (CRD) is itself a Kubernetes object that registers a new kind with the API server. Apply one and within seconds a new endpoint exists: listed by kubectl api-resources, documented by kubectl explain, protected by RBAC, recorded in the audit log, stored in etcd. It inherits the whole ecosystem free — GitOps agents, admission control and policy engines all work on it unmodified. That is why platform teams choose CRDs over a bespoke REST service.
CRD versus CR: the noun and the instance
Two terms questions deliberately confuse. The CRD is the definition — the schema, cluster-scoped, written once by the platform team, teaching Kubernetes the word Database. The Custom Resource (CR) is an instance — one Database named orders-db, written by a developer in their namespace. One CRD, many CRs. And critically: a CRD on its own does nothing. It gives storage and validation, not behaviour. Without a controller watching it, a Database object is a very well-validated sticky note.
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: databases.platform.acme.io # MUST be <plural>.<group>
spec:
group: platform.acme.io
scope: Namespaced # or Cluster
names:
plural: databases
singular: database
kind: Database
shortNames: [db]
categories: [platform] # kubectl get platform → shows all our kinds
versions:
- name: v1alpha1
served: true # is this version reachable over the API?
storage: true # exactly ONE version may be the storage version
subresources:
status: {} # gives the CR its own /status endpoint
additionalPrinterColumns: # what kubectl get shows in the table
- name: Engine
type: string
jsonPath: .spec.engine
- name: Ready
type: string
jsonPath: .status.conditions[?(@.type=="Ready")].status
schema:
openAPIV3Schema:
type: object
properties:
spec:
type: object
required: [engine, sizeGb]
properties:
engine:
type: string
enum: [postgres, mysql] # invalid values rejected at apply time
sizeGb:
type: integer
minimum: 10
maximum: 1000
highAvailability:
type: boolean
default: false # defaulting, for freeValidation, defaulting and the developer-facing shape
The openAPIV3Schema is the platform’s first guardrail, not decoration. A structural schema is required in apiextensions.k8s.io/v1 and buys three things without a line of code: validation (a typo or out-of-range value is rejected before it is ever stored), defaulting, and documentation via kubectl explain. Add printer columns for a useful kubectl get db table, and the status subresource so users can edit spec without forging status. Then the developer writes the small thing — the entire point of the domain:
apiVersion: platform.acme.io/v1alpha1 kind: Database metadata: name: orders-db namespace: checkout spec: engine: postgres sizeGb: 50 highAvailability: true # and then, because it is a first-class API object: # kubectl get db -n checkout # kubectl explain database.spec # kubectl describe db orders-db # kubectl wait --for=condition=Ready db/orders-db --timeout=10m
“Before, ‘give me a database’ meant a ticket, a two-week wait and a 200-line YAML file I copied from someone who left. Now it is six lines and a pull request. I don’t know what happens underneath — StatefulSet, RDS, magic — and that is the deal I signed up for.”
CRDs versus the alternatives
The exam may ask you to pick the right extension mechanism, so know the shortlist.
| Mechanism | What it is | Choose it when… |
|---|---|---|
| CustomResourceDefinition | A new kind served by the built-in API server | The default answer — schema, RBAC and kubectl, no server to run. |
| Aggregated API server | Your own API server registered via an APIService | Custom storage, non-standard verbs or very high churn. Far more work; rare. |
| ConfigMap “API” | Free-form config blobs read by an app | Almost never — no schema, validation, status or typed RBAC. An anti-pattern. |
| Helm chart / Kustomize base | Client-side templating of standard manifests | You want to package resources, not create an API. No reconciliation or status. |
Helm and Kustomize generate YAML; a CRD plus controller owns an outcome. Templating answers “what files should I apply?”; a platform API answers “what do I want to exist, and who keeps it that way?” Both belong in a platform — see Configuration Management — but only one survives a 3am node failure.
Infrastructure provisioning with Kubernetes
☺ Like you’re 10: The same trick that makes pods can make cloud databases, networks and even whole new clusters — because to Kubernetes they are all just objects with a robot attached.
Once the API is extensible, nothing restricts it to workloads. Point a controller at a cloud provider’s API and a Kubernetes object can represent an S3 bucket, a managed Postgres, a VPC — or an entire cluster. The claim behind this competency: Kubernetes becomes a universal control plane, so infrastructure gets the same reconciliation, RBAC, audit trail and GitOps workflow as applications.
One-shot IaC versus a continuously reconciling control plane
Traditional infrastructure as code — Terraform, OpenTofu, Pulumi, CloudFormation — is run-to-completion: write code, plan, apply, and the tool records what it made in a state file. Between runs nothing watches, so a security group changed in the console drifts silently. A Kubernetes control plane stores desired state as API objects and runs the loop forever.
| Dimension | Classic IaC (Terraform / OpenTofu / Pulumi) | Kubernetes control plane (e.g. Crossplane) |
|---|---|---|
| Execution model | One-shot plan then apply | Continuous reconciliation by an in-cluster controller |
| Desired state lives in | HCL or program code in a repo | API objects in etcd (usually synced from Git) |
| Actual state tracked in | A state file, with its locking problems | The resource’s .status; the cloud is re-observed each loop |
| Drift | Detected only on the next run | Corrected automatically, continuously |
| Access control | Pipeline credentials, tool-specific policy | Kubernetes RBAC, admission control, audit log |
| Preview before change | Excellent — terraform plan is a real diff | Weaker; you lean on staging and policy checks |
Neither wins outright: a fair question asks which model corrects drift without human action (the control plane) or which gives a reviewable plan before applying (classic IaC). Deep dive: IaC & Control Planes.
Crossplane — cloud resources as Kubernetes objects
Crossplane is the CNCF project most associated with this competency, and its vocabulary is worth memorising. A Provider is an installable package of controllers for one cloud, configured by a ProviderConfig holding credentials. A Managed Resource is a Kubernetes object representing one real cloud resource — spec.forProvider is what you want, status.atProvider what the cloud reports. A CompositeResourceDefinition (XRD) declares your own higher-level API; a Composition expands it into Managed Resources. Developers get a Database; the platform team decides that means an RDS instance, a subnet group and a firewall rule.
# Platform team, once: define the API and hide the plumbing.
apiVersion: apiextensions.crossplane.io/v1
kind: CompositeResourceDefinition
metadata:
name: xdatabases.platform.acme.io
spec:
group: platform.acme.io
names: { kind: XDatabase, plural: xdatabases }
claimNames: { kind: Database, plural: databases } # the namespaced, developer-facing face
versions:
- name: v1alpha1
served: true
referenceable: true
schema:
openAPIV3Schema:
type: object
properties:
spec:
type: object
properties:
sizeGb: { type: integer }
region: { type: string }
# Developer, per app: the whole request surface they ever see.
---
apiVersion: platform.acme.io/v1alpha1
kind: Database
metadata: { name: orders-db, namespace: checkout }
spec:
sizeGb: 50
region: eu-west-1Version note. The example uses apiextensions.crossplane.io/v1, where a cluster-scoped composite resource (XR) is fronted by a namespaced claim. Crossplane v2 adds apiextensions.crossplane.io/v2, in which composite resources are namespaced by default and claims are no longer supported; v1 remains available for existing setups. CNPA tests the idea — a platform-owned API composed into real cloud resources — not the API version, so learn the shape and check the Crossplane docs for whichever release you run.
Cluster API — clusters as declarative objects
The same idea one level up. Cluster API (CAPI) makes a cluster itself a set of custom resources — Cluster, MachineDeployment, MachineSet, Machine — reconciled by controllers in a management cluster that provision and repair workload clusters. Note the echo of Deployment → ReplicaSet → Pod: scaling a node pool is editing a replica count. Know the concept, fleet management by reconciliation; skip the CLI flags.
When a cloud resource is a Kubernetes object, kubectl delete can delete a production database. Managed resources carry deletion policies and controllers use finalizers for exactly this reason — but the blast radius of a careless RBAC grant is now measured in lost data, not restarted pods. Hence the security and conformance domain.
The Kubernetes operator pattern for integration
☺ Like you’re 10: An operator is a robot that knows how to look after one specific thing — like a zookeeper who only handles penguins, and knows every penguin trick.
An operator combines the two halves you have just met: a CRD (the API) plus a controller (the behaviour), packaging the operational knowledge of a human expert as software. Produce that one-sentence definition and you have the competency. The name comes from the human role it automates — whoever held the runbook for upgrading that database at 2am.
What an operator adds beyond “install it”
An operator’s value is not Day-1 creation; Helm can install things. It is Day-2 operations encoded in a loop: upgrades in the correct order, scheduled backups and verified restores, failover, volume resizing, credential rotation. The community Operator Capability Levels give a five-rung maturity ladder worth recognising by name.
| Level | Name | What the operator can do |
|---|---|---|
| 1 | Basic Install | Provisions the application and its configuration. |
| 2 | Seamless Upgrades | Upgrades the app and itself, safely and in order. |
| 3 | Full Lifecycle | Backups, restores, failover, scaling — real Day-2 work. |
| 4 | Deep Insights | Emits metrics, alerts and workload analysis. |
| 5 | Auto Pilot | Auto-scales, auto-tunes and auto-remediates, no human needed. |
Ownership, cleanup and reporting — the three mechanics to know
Three mechanisms come up constantly and are easy marks. Owner references: an operator stamps each child with a reference to its parent, wiring them into garbage collection — delete the parent and every child is swept up. Finalizers: a string in metadata.finalizers blocking real deletion until the operator finishes its last chore (final snapshot, deprovision the cloud resource); while one is present, a delete only sets deletionTimestamp. Status conditions: the standard type/status/reason/message array (Ready, Progressing, Degraded) that dashboards and kubectl wait read.
Operators as integration adapters
The curriculum says “Operator Pattern for Integration,” and that word is the tell. An operator’s target need not live in the cluster: because the loop is just “read desire, observe reality, act,” the reality can be a cloud API, a SaaS product, a DNS zone or a secrets vault. That is how a platform presents one interface over a messy estate — External Secrets Operator syncs Vault into Kubernetes Secrets, cert-manager renews ACME certificates before expiry, Crossplane providers reconcile cloud accounts.
Recognise the frameworks: Kubebuilder (Go, on controller-runtime — the canonical path), Operator SDK (same Go core plus no-code Helm and Ansible flavours), Kopf (Python), Metacontroller (webhook-only), and OLM, which installs and upgrades operators themselves.
“The bit that made it click: I ran kubectl get certificate and it just worked — same verbs, same output shape, same RBAC as pods. cert-manager gave me no portal and no CLI to learn. It gave the cluster a new word, and my existing tools already spoke it.”
Serving the API: admission, RBAC and the request path
☺ Like you’re 10: Before your request is written down it walks through a short security line: who are you, are you allowed, let me tidy that form, is it valid?
Extending the API means knowing how a request is served, because that path is where a platform enforces its rules.
The five stops
Authentication establishes who is calling. Authorization (RBAC) decides whether that identity may perform this verb on this resource in this namespace — custom resources use exactly the same RBAC as built-ins, so you grant create on databases.platform.acme.io as you would on deployments. Mutating admission may change the object; schema validation checks it against your CRD’s OpenAPI schema; and validating admission — webhooks, ValidatingAdmissionPolicy using CEL, or Kyverno / OPA Gatekeeper — may reject it. Only then does the object reach etcd, the watch fire, and the operator wake up.
Why this makes self-service safe
That chain is the safety story the CNPA blueprint keeps circling. A developer gets RBAC on one narrow kind — databases in their own namespace — and nothing else. The schema constrains what they can ask for; admission policy constrains it further; the operator holds the powerful cloud credentials. That is self-service without handing out administrative access, and why platform APIs and platform security are two views of one design.
Designing platform APIs developers actually adopt
☺ Like you’re 10: A good new word is short, honest, and does what it says. A bad one hides something you still need to know.
The exam is knowledge-based, but it does test judgement about abstraction — the judgement covered by Platform as a Product. Two failure modes account for most bad platform APIs.
Leaky and over-thick abstractions
An abstraction is a promise, and every promise can leak. Wrap Postgres in a Database kind, and the day a developer hits a Postgres-specific limit they must understand both your abstraction and the thing underneath — worse than raw configuration. Keep the resource thin over well-understood primitives, and don’t abstract what your team cannot operate. The opposite failure is just as common: a Database with ninety optional fields is not an abstraction, it is the original YAML wearing a hat.
Versioning is a promise
Publishing a CRD is publishing an API contract other people’s repositories depend on. Hence versions carry served and storage flags and support conversion: serve v1alpha1 and v1beta1 at once while consumers migrate. Start at v1alpha1 to signal instability, and treat a breaking change like one to a public library.
Say these out loud from memory. One: CRD versus CR. Two: what a CRD gives you and what it does not. Three: the definition of an operator, in one sentence. Four: why level-triggered beats edge-triggered. Five: one difference between Terraform and Crossplane. If any answer takes more than ten seconds, reread that section — those five sentences are most of this domain’s 12%.
Foxy: So a CRD creates the database? I applied one and nothing happened.
Mira: A CRD only teaches the cluster the word Database. It validates your object beautifully — and has no idea what a database is.
Recon: BEEP. That’s my job. CRD plus me equals operator. Watch, diff, act — then again, forever.
Nutty: Wait — I can kubectl get my RDS instance?
Mira: That’s Crossplane. Same loop, different reality on the far end.
Gizmo: Or — hear me out — one giant ConfigMap and a cron job. Ship it Friday. 🤑
Timmy: No schema, no validation, no status, no typed RBAC, no audit trail. You reinvented the API server, badly.
Dot: I only care that kubectl get db works and something tells me when it’s Ready.
What you must be able to state on exam day
☺ Like you’re 10: Here is the short list to keep in your head walking into the room.
CNPA is a knowledge-based, multiple-choice exam — not a hands-on lab like CKA, CKAD, CKS or the performance-based CNPE — so the win condition is crisp recall, not kubectl speed. It will not ask you to author a CRD under time pressure; it will ask you to recognise what one does. Rehearse until boring: the loop is observe, diff, act, repeat, and level-triggered; .spec is desire from the user, .status observation from the controller; a CRD is the definition, a CR the instance; a CRD gives storage, schema, RBAC and kubectl but no behaviour; an operator is a CRD plus a controller encoding operational knowledge; classic IaC applies once, a control plane reconciles continuously.
Distinctions the question writers love
Watch for near-miss pairs: CRD versus CR; declarative versus imperative; level- versus edge-triggered; CRD versus aggregated API server; Helm templating versus a reconciling API; controller versus operator (every operator is a controller, not the reverse); mutating versus validating admission; management versus workload cluster. When two answers both look plausible, one of these is usually the point.
Where to go deeper
Back to the domain map on the CNPA hub, or the wider ladder on Certifications. The CNPE track goes deeper: Platform APIs, CRDs & Operators for the mechanics, IaC & Control Planes for the provisioning comparison, Crossplane and Cluster API for the tools, Kubebuilder for building an operator, Self-Service & Golden Paths for the product side. Neighbouring domains: Core Fundamentals, Continuous Delivery, Observability, Security & Conformance, IDPs & Developer Experience and Measuring your Platform.
To make it stick rather than merely readable: build one for real in the Platform API & Self-Service labs — a CRD with schema, printer columns and a /status subresource, then a controller to drive it. Then drill recall with Practice — Platform APIs & Self-Service (hands-on CNPE-style tasks, deeper than CNPA needs but excellent for cementing the loop), the flashcards, the quiz, and finally the weighted CNPA mock exam, in which this domain supplies roughly one question in eight.
Official sources worth an hour
The Kubernetes docs on custom resources, CustomResourceDefinitions, the operator pattern and controllers cover this domain almost exactly; then skim docs.crossplane.io and the Cluster API book. Exam logistics are set by the Linux Foundation and change without notice. As published on the official CNPA page at the time of writing: an online, proctored, multiple-choice exam of 120 minutes, priced at USD 250 standalone, including one free retake and twelve months of exam eligibility, with the certification valid for two years. The pass mark is published: the Linux Foundation’s Multiple Choice Exam FAQ requires 75% or above, and gives the CNPA 120 minutes as its one named exception to the 90 that other multiple-choice exams get. What is not published is a fixed question count — distrust any study site that quotes one, this one included. Confirm every figure on the official CNPA certification page before you book. The domain names, weights and competencies on this page come from the CNCF’s published CNPA exam curriculum; the six weights sum to 100%.
1. What are the three steps of a reconciliation loop, and what does “level-triggered” mean? 2. Who writes .spec and who writes .status? 3. You apply a CRD and then a custom resource, and nothing is provisioned. What is missing? 4. Define an operator in one sentence. 5. Name two differences between Terraform’s model and Crossplane’s. 6. What do owner references, finalizers and status conditions each do? 7. Why is RBAC on a custom resource safer than granting a developer the operator’s own permissions?
Check your answers
- Observe desired state, diff against actual state, act to close the gap — then repeat forever. Level-triggered means comparing current level to target on every pass rather than reacting once to an event, which is what makes it self-healing.
.specis written by the user and states desired state;.statusis written by the controller and reports observed state.- A controller. A CRD only registers the kind — storage, schema validation, defaulting, RBAC and
kubectlsupport, but no behaviour. With nothing watching it, the object just sits in etcd. - An operator is a custom resource (CRD) plus a controller encoding the operational knowledge of a human expert — install, upgrade, back up, fail over — applied continuously through a reconciliation loop.
- Any two of: Terraform is one-shot
plan/apply, Crossplane reconciles continuously; Terraform tracks reality in a state file, Crossplane re-observes the cloud intostatus.atProvider; Terraform detects drift only on the next run; Crossplane inherits Kubernetes RBAC and audit; Terraform gives a better pre-apply diff. - Owner references mark children so Kubernetes garbage-collects them with the parent. Finalizers block deletion until the controller finishes external cleanup. Status conditions report readiness in the standard
type/status/reason/messageform thatkubectl waitunderstands. - The developer gets only
create/geton one narrow kind in their namespace, constrained further by schema and admission policy, while the operator holds the powerful credentials and acts on their behalf — self-service without administrative access.