The CKAD spends most of its two hours on the resources you already know — Pods, Deployments, Services, ConfigMaps. Then a task hands you a cluster with a kind you’ve never seen and asks you to create one. No documentation link, no example manifest, just a resource type like CronTab or Backup and a clock ticking down. That task is testing one specific objective: discover and use resources that extend Kubernetes (Custom Resource Definitions and Operators).
This objective sits in the Application Environment, Configuration and Security domain — about 25% of the exam, the single largest slice. Most candidates over-study ConfigMaps and Secrets here and skip CRDs entirely, then freeze when a custom resource appears. The good news: you are almost never asked to author a CRD or write a controller. You are asked to discover what already exists on the cluster and create an instance of it. Both are mechanical once you know the three commands that reveal any resource’s shape. This guide covers what a CRD actually is, the Operator pattern that usually accompanies it, and the exact kubectl discovery workflow that turns an unfamiliar custom resource into a two-minute task.
For the broader domain, pair this with the ConfigMaps and Secrets guide and the SecurityContext and ServiceAccounts guide; for the full blueprint, start with the CKAD exam guide for 2026 and the CKAD exam domains breakdown.
What a Custom Resource Definition Actually Is
Kubernetes ships with a fixed set of built-in resource types: Pod, Deployment, Service, ConfigMap, and so on. Each is a kind that the API server understands, stores in etcd, and exposes through a REST endpoint. A Custom Resource Definition (CRD) is how you teach the API server a new kind without recompiling it.
When you apply a CRD, the API server does two things immediately:
- It registers a new REST endpoint — for example
/apis/stable.example.com/v1/namespaces/default/crontabs. - It starts accepting
kubectl get,create,apply,delete, andeditfor that new kind, exactly like a built-in resource.
The critical mental model for the exam: a CRD is just a schema. It defines a new type. It does not, by itself, do anything with the objects you create. A CronTab custom resource you create after applying a CRD is stored in etcd and served by the API — but nothing schedules a cron job unless a controller is watching. That controller is the other half of the story, and we’ll get to it in the Operator section.
Here is a minimal CRD. You rarely write one on the exam, but reading one fluently is what lets you create instances correctly:
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
# name MUST be <plural>.<group>
name: crontabs.stable.example.com
spec:
group: stable.example.com
scope: Namespaced # or Cluster
names:
plural: crontabs
singular: crontab
kind: CronTab # the value you put in `kind:` on an instance
shortNames:
- ct
versions:
- name: v1
served: true # is this version reachable via the API?
storage: true # exactly one version must be the storage version
schema:
openAPIV3Schema:
type: object
properties:
spec:
type: object
properties:
cronSpec:
type: string
image:
type: string
replicas:
type: integer
Every field you need to build a valid instance is in that block. The kind under names is what goes in your instance’s kind:. The group plus a versions[].name combine into the apiVersion. The spec.properties describe exactly which fields are allowed. Read those three things and you can construct a correct custom resource without ever seeing an example.
The custom resource instance
Given the CRD above, an instance (a “custom resource” or CR) looks like any other Kubernetes object:
apiVersion: stable.example.com/v1 # group/version from the CRD
kind: CronTab # names.kind from the CRD
metadata:
name: my-cron-object
namespace: default
spec:
cronSpec: "* * * * */5"
image: my-cron-image:latest
replicas: 3
Apply it the way you apply everything else:
kubectl apply -f crontab.yaml
kubectl get crontabs # or: kubectl get ct (shortName)
kubectl describe crontab my-cron-object
That is the entire happy path for the CKAD’s “create a custom resource” task. The only hard part is discovering the four facts you need — apiVersion, kind, scope, and the allowed spec fields — on a cluster you didn’t build.
The Discovery Workflow: Three Commands That Reveal Any Resource
This is the single most valuable skill for the CRD objective. When a task says “create a Widget in the apps namespace with size: large,” you do not guess. You interrogate the cluster.
Step 1 — list every CRD and resource type
# List all Custom Resource Definitions installed on the cluster
kubectl get crd
# List every resource type the API server knows, custom ones included,
# showing the API group and whether it's namespaced
kubectl api-resources
kubectl api-resources is the workhorse. It prints the short name, API group, namespaced/cluster scope, and kind for everything — built-ins and CRDs side by side. Filter it to find your target fast:
kubectl api-resources | grep -i widget
kubectl api-resources --api-group=stable.example.com
The output tells you the exact plural name to use in kubectl get, the API group, and — crucially — the NAMESPACED column (true or false), which tells you whether your instance needs a namespace.
Step 2 — read the schema with kubectl explain
kubectl explain works on custom resources exactly as it does on Pods, because the CRD’s openAPIV3Schema is published to the API’s OpenAPI document:
kubectl explain crontab
kubectl explain crontab.spec
kubectl explain crontab.spec.cronSpec
kubectl explain crontab --recursive # dump the entire field tree at once
--recursive is the exam power-move: one command prints every field path in the resource, so you can copy field names verbatim instead of guessing at capitalisation (cronSpec, not cronspec). This is the same discovery habit that saves time everywhere else on the exam — the kubectl cheat sheet collects the rest of these shortcuts.
Step 3 — copy an existing instance as a template
If any instances already exist, the fastest correct manifest is a copy of a live one:
# Dump an existing instance, strip runtime fields, edit, re-apply
kubectl get crontab my-existing-cron -o yaml > cr.yaml
Delete the status, metadata.uid, resourceVersion, creationTimestamp, and managedFields, change the name, and you have a guaranteed-valid starting point. When nothing exists yet, fall back to kubectl explain and the CRD’s schema.
Here is the whole workflow as a table you can memorise:
| Question | Command | What you learn |
|---|---|---|
| What custom types exist? | kubectl get crd | CRD names (<plural>.<group>) |
| What’s the apiVersion / scope? | kubectl api-resources | grep <kind> | Group, version, namespaced or not |
| What fields are allowed? | kubectl explain <kind> --recursive | Full field tree with types |
| Is there an example to copy? | kubectl get <plural> -o yaml | A known-good instance |
| Does my instance validate? | kubectl apply -f cr.yaml | Schema acceptance or a clear error |
Practise that sequence until it is muscle memory. On exam day you should be able to go from “I’ve never seen this kind” to a valid applied instance in under three minutes.
Operators: Why the Custom Resource Actually Does Something
A CRD alone is inert storage. The reason custom resources are useful is the Operator pattern: a CRD plus a custom controller that watches instances of that CRD and drives the real world to match their spec.
This is the same reconcile loop that powers built-in controllers. The Deployment controller watches Deployment objects and creates ReplicaSets; a database Operator watches PostgresCluster objects and creates StatefulSets, Services, Secrets, and backups. The loop never stops:
observe diff act
current state ────► desired vs actual ────► create/update/delete
▲ │
└─────────────────── watch ◄──────────────────────┘
An Operator, in other words, encodes the operational knowledge a human admin would otherwise apply by hand — “if the primary dies, promote a replica; if a Backup resource appears, run pg_dump and upload it.” You create a high-level custom resource (kind: Backup), and the Operator’s controller does the multi-step work.
What the CKAD expects you to know about Operators
You will not write a controller in Go on the exam. The Operator content is conceptual plus the same create/read mechanics as any custom resource. Be ready to:
- Explain the relationship. A CRD defines the type; the controller/Operator provides the behaviour. Creating a CR with no controller running means the object just sits in etcd — a common “why isn’t anything happening?” gotcha.
- Create a CR that an Operator consumes. Discover its schema with
kubectl explain, apply it, and confirm the Operator reacted (kubectl getthe resources it was supposed to create). - Inspect status. Many custom resources expose a
statussubresource the controller writes to.kubectl get widget -o yamland read.statusto confirm reconciliation succeeded.
# Confirm an Operator's controller is actually running
kubectl get pods -n operators
kubectl get crd | grep example.com
# Create the CR the Operator watches for, then verify it reacted
kubectl apply -f backup.yaml
kubectl get backup my-backup -o jsonpath='{.status.phase}{"\n"}'
kubectl get pods -l app=backup-job # resources the Operator created
If you create a valid custom resource and nothing happens, the near-certain cause is that no controller is watching it — either the Operator isn’t installed or its Pod is crash-looping. That diagnostic instinct is exactly the kind of thing the application troubleshooting guide drills.
CRDs vs Built-in Resources: A Quick Comparison
| Aspect | Built-in resource (e.g. Deployment) | Custom resource (via CRD) |
|---|---|---|
| Defined by | The Kubernetes API server itself | A CustomResourceDefinition object you apply |
| API group | e.g. apps/v1 | Vendor group, e.g. stable.example.com/v1 |
| Behaviour | Built-in controller ships with the cluster | Needs a separate controller/Operator |
| Validation | Compiled-in | openAPIV3Schema in the CRD |
| kubectl support | Full | Full — get, describe, explain, apply, edit |
| Discovery | kubectl explain deployment | kubectl explain <kind> (identical) |
The row that matters most for the exam: kubectl treats them identically. Every command reflex you have for Deployments — get, describe, explain, apply, -o yaml, --dry-run=client — works unchanged on custom resources. That is the whole reason the discovery workflow is so reliable.
Common CKAD Mistakes with Custom Resources
- Guessing the
apiVersion. The group and version come straight fromkubectl api-resourcesor the CRD’sspec.group+spec.versions[].name. Never invent it. - Wrong case on field names.
cronSpecandcronspecare different fields. The schema is camelCase; usekubectl explain <kind>.specto copy names exactly. - Forgetting namespace scope. Check the
NAMESPACEDcolumn. A namespaced CR needsmetadata.namespace(or the right-nflag); a cluster-scoped one must not have a namespace. - Expecting a bare CRD to do work. Applying a CRD creates a type, not behaviour. If the task expects side effects, a controller must be running.
- Skipping
--dry-run=client -o yaml. You can’t scaffold a custom resource withkubectl create <kind>the way you can a Deployment, but you can validate your hand-written manifest before applying:kubectl apply -f cr.yaml --dry-run=serverruns it through the CRD schema without persisting it.
A Realistic Exam Scenario, End to End
“The cluster has an Operator installed that manages
SiteConfigresources in thewebnamespace. Create aSiteConfignamedlandingwithreplicas: 4anddomain: example.com.”
Your keystrokes:
# 1. Confirm the type exists and learn its group + scope
kubectl api-resources | grep -i siteconfig
# siteconfigs sc web.example.com/v1 true SiteConfig
# 2. Learn the exact spec fields
kubectl explain siteconfig.spec --recursive
# 3. Write the instance from what you learned
cat <<'EOF' > sc.yaml
apiVersion: web.example.com/v1
kind: SiteConfig
metadata:
name: landing
namespace: web
spec:
replicas: 4
domain: example.com
EOF
# 4. Validate against the schema, then apply
kubectl apply -f sc.yaml --dry-run=server
kubectl apply -f sc.yaml
# 5. Confirm the Operator reconciled it
kubectl get siteconfig landing -n web -o yaml | grep -A5 status
Five steps, all mechanical, no memorised schema required. That reproducibility under time pressure is exactly what separates candidates who pass comfortably from those who run out of clock — and it only becomes automatic with reps on a real cluster.
Practising CRDs on a Live Cluster
You cannot build the discovery reflex by reading. You build it by applying a CRD, creating instances, breaking them, and reading the validation errors — over and over until kubectl explain <kind> --recursive is the first thing your hands do when they see an unfamiliar resource. Sailor.sh’s Certified Kubernetes Application Developer (CKAD) Mock Exam Bundle runs on a live, browser-based Kubernetes cluster with exam-style performance tasks, including extend-Kubernetes scenarios where you discover an installed CRD and create valid custom resources under the clock. Warm up for free with the how to practice CKAD for free guide, sequence your prep with the CKAD study plan, and connect this objective to its neighbours through the ConfigMaps and Secrets and SecurityContext and ServiceAccounts guides. If you’re also working toward the administrator exam, the Helm and Kustomize for the CKA guide covers a related packaging skill.
Frequently Asked Questions
Do I need to write a CRD from scratch on the CKAD?
Almost never. The objective is to discover and use custom resources, not author them. You should be able to read a CRD fluently — enough to build a valid instance — but the tasks center on creating custom resources on a cluster where the CRD is already installed. Reading the schema with kubectl explain is far more important than memorising CRD syntax.
What’s the difference between a CRD, a custom resource, and an Operator?
A CRD defines a new resource type (the schema). A custom resource is an instance of that type — the object you create with kubectl apply. An Operator is the CRD plus a controller that watches those instances and takes real action to make the cluster match their spec. CRD = the noun’s definition, CR = a specific noun, Operator = the verb that acts on it.
Why did nothing happen after I created my custom resource?
Because a CRD only adds storage and an API endpoint — it doesn’t add behaviour. If no controller (Operator) is watching that resource type, your object just sits in etcd. Check that the Operator’s Pod is running (kubectl get pods -A | grep -i operator) and healthy. This “silent success” is one of the most common points of confusion.
How do I find the apiVersion for a custom resource I’ve never seen?
Run kubectl api-resources | grep -i <kind>. The output shows the API group and, combined with the version, gives you the apiVersion string. You can also read spec.group and spec.versions[].name directly from the CRD with kubectl get crd <name> -o yaml.
Is kubectl explain reliable for custom resources?
Yes, as long as the CRD includes an openAPIV3Schema (required in apiextensions.k8s.io/v1). The schema is published to the cluster’s OpenAPI document, so kubectl explain <kind> and kubectl explain <kind>.spec --recursive return the same rich field descriptions you get for built-in resources. This is your most reliable discovery tool on the exam.
How much of the CKAD is CRDs and Operators?
It’s one objective within the Application Environment, Configuration and Security domain (~25% of the exam). You won’t see many CRD questions, but the ones you do see are fast points if you’ve practised the discovery workflow — and easy to lose entirely if you haven’t. See the CKAD exam domains breakdown for the full weighting.
Conclusion
Custom Resource Definitions and Operators look intimidating because they hint at a whole world of controller programming — but the CKAD keeps you firmly on the consumer side. Your job is to walk onto an unfamiliar cluster, discover what custom types are installed, read their schema, and create valid instances, all within a few minutes. Three commands do almost all of it: kubectl api-resources to find the type and its scope, kubectl explain <kind> --recursive to read the fields, and kubectl get <plural> -o yaml to copy a known-good example. Add the mental model — CRD defines a type, a controller gives it behaviour, an Operator is both — and you’ll turn the one objective most candidates skip into reliable points. Drill the discovery workflow on a live cluster until it’s reflex, and the “I’ve never seen this resource” task becomes just another manifest to apply.