Module 4.6: Custom Resource Definitions (CRDs)
Complexity:
[MEDIUM]- New to CKAD 2025, conceptual understanding importantTime to Complete: 35-45 minutes
Prerequisites: Understanding of Kubernetes resources and API structure
Learning Outcomes
Section titled “Learning Outcomes”After completing this module, you will be able to:
- Create a CustomResourceDefinition with proper schema validation and versioning
- Explain how CRDs extend the Kubernetes API and how controllers reconcile custom resources
- Deploy custom resources and interact with them using standard kubectl commands
- Debug CRD validation errors and understand the relationship between CRDs and operators
Why This Module Matters
Section titled “Why This Module Matters”Custom Resource Definitions extend Kubernetes with your own resource types. Instead of only working with Pods, Services, and Deployments, you can define resources like Database, Certificate, or BackupJob that make sense for your domain.
The CKAD exam (2025) tests:
- Understanding what CRDs are
- Working with custom resources
- Using
kubectlto interact with CRs - Recognizing Operator patterns
The Custom Forms Analogy
Kubernetes built-in resources are like standard government forms—everyone uses the same Pod form, the same Service form. CRDs are like creating your own custom form for your organization. You define what fields it has (
spec), and Kubernetes stores and validates it. Operators are like automated clerks that watch for these forms and take action when they’re submitted.
CRD Basics
Section titled “CRD Basics”What is a CRD?
Section titled “What is a CRD?”A Custom Resource Definition (CRD) tells Kubernetes about a new resource type:
apiVersion: apiextensions.k8s.io/v1kind: CustomResourceDefinitionmetadata: name: databases.example.com # plural.group formatspec: group: example.com versions: - name: v1 served: true storage: true schema: openAPIV3Schema: type: object properties: spec: type: object properties: engine: type: string size: type: string scope: Namespaced names: plural: databases singular: database kind: Database shortNames: - dbPause and predict: You see
kubectl get certificatesbeing used in a cluster. IsCertificatea built-in Kubernetes resource? How would you find out whether it’s a CRD?
What is a Custom Resource (CR)?
Section titled “What is a Custom Resource (CR)?”Once the CRD exists, you can create instances—Custom Resources:
apiVersion: example.com/v1kind: Databasemetadata: name: my-databasespec: engine: postgres size: largeCRD Components
Section titled “CRD Components”names: plural: databases # Used in URLs: /apis/example.com/v1/databases singular: database # Used in CLI: kubectl get database kind: Database # Used in YAML: kind: Database shortNames: - db # Shortcuts: kubectl get dbscope: Namespaced # Resources exist in namespaces# orscope: Cluster # Resources are cluster-wideVersions
Section titled “Versions”versions:- name: v1 served: true # API server serves this version storage: true # Store in etcd using this version (only one can be true)Schema Validation
Section titled “Schema Validation”schema: openAPIV3Schema: type: object required: ["spec"] properties: spec: type: object required: ["engine"] properties: engine: type: string enum: ["postgres", "mysql", "mongodb"] size: type: string default: "small"Working with CRDs
Section titled “Working with CRDs”View Installed CRDs
Section titled “View Installed CRDs”# List all CRDsk get crd
# Describe a CRDk describe crd certificates.cert-manager.io
# Get CRD YAMLk get crd mycrd.example.com -o yamlWorking with Custom Resources
Section titled “Working with Custom Resources”# List custom resources (once CRD exists)k get databasesk get db # Using shortName
# Describe a CRk describe database my-database
# Get CR YAMLk get database my-database -o yaml
# Delete a CRk delete database my-databaseUnderstanding Schema Validation Errors
Section titled “Understanding Schema Validation Errors”One of the main benefits of CRDs is that the Kubernetes API server validates the custom resources before accepting them. If a user tries to create a CR that doesn’t match the openAPIV3Schema defined in the CRD, the API server rejects it immediately.
For example, if our Database CRD requires engine to be one of ["postgres", "mysql", "mongodb"], and you apply this YAML:
apiVersion: example.com/v1kind: Databasemetadata: name: my-databasespec: engine: redis size: largeThe API server will reject it with a descriptive error:
$ kubectl apply -f bad-db.yamlThe Database "my-database" is invalid: spec.engine: Unsupported value: "redis": supported values: "postgres", "mysql", "mongodb"Stop and think: If a CRD does not define an
openAPIV3Schema, what happens when you submit a CR with misspelled fields likeengin: postgres? The API server will accept it blindly, but the operator watching the CR might fail to process it, making debugging much harder! Strict schema validation prevents this.
Common CRDs You’ll Encounter
Section titled “Common CRDs You’ll Encounter”cert-manager
Section titled “cert-manager”k get crd | grep cert-manager# certificates.cert-manager.io# clusterissuers.cert-manager.io# issuers.cert-manager.io
# Create a Certificatek get certificatesk describe certificate my-certPrometheus Operator
Section titled “Prometheus Operator”k get crd | grep monitoring# servicemonitors.monitoring.coreos.com# prometheusrules.monitoring.coreos.comGateway API
Section titled “Gateway API”k get crd | grep gateway# gateways.gateway.networking.k8s.io# httproutes.gateway.networking.k8s.ioOperators Pattern
Section titled “Operators Pattern”What is an Operator?
Section titled “What is an Operator?”An Operator = CRD + Controller
- CRD: Defines the “what” (custom resource structure)
- Controller: Handles the “how” (watches CRs and takes action)
┌─────────────────────────────────────────────────────────────┐│ Operator Pattern │├─────────────────────────────────────────────────────────────┤│ ││ 1. User Creates Custom Resource ││ ┌─────────────────────────────────┐ ││ │ apiVersion: example.com/v1 │ ││ │ kind: Database │ ││ │ spec: │ ││ │ engine: postgres │ ││ └─────────────────────────────────┘ ││ │ ││ ▼ ││ 2. Controller Watches for Database CRs ││ ┌─────────────────────────────────┐ ││ │ Operator Pod │ ││ │ - Sees new Database CR │ ││ │ - Creates StatefulSet │ ││ │ - Creates Service │ ││ │ - Creates Secret (password) │ ││ │ - Updates CR status │ ││ └─────────────────────────────────┘ ││ │ ││ ▼ ││ 3. Actual Resources Created ││ ┌─────────────────────────────────┐ ││ │ StatefulSet: my-database │ ││ │ Service: my-database │ ││ │ Secret: my-database-creds │ ││ └─────────────────────────────────┘ ││ │└─────────────────────────────────────────────────────────────┘Stop and think: A CRD for
Databaseexists in the cluster, and you create aDatabasecustom resource. But no actual database gets provisioned. What’s missing? A CRD alone doesn’t do anything — why not?
Why Use Operators?
Section titled “Why Use Operators?”| Benefit | Example |
|---|---|
| Abstraction | Create Database, operator handles StatefulSet, PVC, etc. |
| Automation | Operator handles backups, failover, scaling |
| Domain expertise | Operator knows how to properly configure Postgres |
| Day 2 operations | Upgrades, restores, monitoring built-in |
kubectl explain with CRDs
Section titled “kubectl explain with CRDs”# Works for CRDs too (if installed)k explain databasek explain database.speck explain certificate.spec.secretNameQuick Reference
Section titled “Quick Reference”# List CRDsk get crd
# View CRD detailsk describe crd NAME
# Work with custom resourcesk get <resource>k describe <resource> NAMEk delete <resource> NAME
# Get API resources (includes CRDs)k api-resources | grep example.com
# Check if CRD existsk get crd myresource.example.comVisualization
Section titled “Visualization”┌─────────────────────────────────────────────────────────────┐│ CRD Creates New API Endpoint │├─────────────────────────────────────────────────────────────┤│ ││ Before CRD: ││ ┌─────────────────────────────────┐ ││ │ /api/v1/pods │ ││ │ /api/v1/services │ ││ │ /apis/apps/v1/deployments │ ││ └─────────────────────────────────┘ ││ ││ After CRD (group: example.com, plural: databases): ││ ┌─────────────────────────────────┐ ││ │ /api/v1/pods │ ││ │ /api/v1/services │ ││ │ /apis/apps/v1/deployments │ ││ │ /apis/example.com/v1/databases │ ← NEW! ││ └─────────────────────────────────┘ ││ ││ kubectl commands now work: ││ $ k get databases ││ $ k describe database my-db ││ $ k delete database my-db ││ │└─────────────────────────────────────────────────────────────┘Did You Know?
Section titled “Did You Know?”-
CRDs are themselves a Kubernetes resource. The
apiextensions.k8s.io/v1group defines how to define custom resources. -
Deleting a CRD deletes all its Custom Resources. Be careful!
kubectl delete crd databases.example.comwipes all Database CRs. -
CRDs support multiple versions. You can have v1alpha1, v1beta1, and v1 served simultaneously for smooth migrations.
-
The most popular Kubernetes projects are Operators. Prometheus, cert-manager, ArgoCD, Istio—all use CRDs extensively.
Common Mistakes
Section titled “Common Mistakes”| Mistake | Why It Hurts | Solution |
|---|---|---|
| Confusing CRD with CR | CRD is definition, CR is instance | CRD = template, CR = actual resource |
| Deleting CRD accidentally | Deletes all CRs too | Double-check before deleting CRDs |
| Not checking if CRD exists | kubectl commands fail | k get crd NAME first |
| Thinking CRDs do something alone | CRDs just store data | Need controller/operator for actions |
| Wrong plural/singular usage | kubectl commands fail | Check k api-resources for correct names |
-
A team installs a
DatabaseCRD and creates a custom resourcekind: Databasewithspec.engine: postgres. Nothing happens — no StatefulSet, no Service, no PVC is created. The CRD is correctly installed (confirmed withkubectl get crd). What is wrong?Answer
A CRD only defines the schema — it tells Kubernetes how to store and validate the custom resource, but it doesn't contain any logic to act on it. What's missing is a controller (operator) that watches for Database CRs and creates the actual Kubernetes resources (StatefulSet, Service, PVC, Secret, etc.). The CRD is the "form," and the operator is the "clerk that processes the form." You need to install the database operator (a pod that runs the controller logic) alongside the CRD for anything to actually happen. -
A colleague accidentally runs
kubectl delete crd databases.example.comthinking it would just clean up the definition. The team immediately discovers that all 15 Database custom resources across 5 namespaces are gone. Why did this happen and how can you prevent it?Answer
Deleting a CRD triggers cascading deletion of all Custom Resources of that type cluster-wide. Kubernetes treats the CRD as the "owner" of all its CRs — removing the definition removes all instances. To prevent this: (1) use RBAC to restrict who can delete CRDs (they're cluster-scoped resources), (2) back up CRs before any CRD operations, (3) consider adding the `foregroundDeletion` finalizer pattern in your operator to handle cleanup gracefully. This is one of the most dangerous operations in Kubernetes because a single command can wipe data across all namespaces. -
You join a new team and need to understand what custom resources are installed in the cluster. You run
kubectl get podsand see several pods with names likecert-manager-controllerandprometheus-operator. How do you discover all CRDs and which ones are actively being used?Answer
Run `kubectl get crd` to list all installed CRDs — this shows every custom resource type available. To see which ones have actual instances, iterate through them: `kubectl get crd -o name | while read crd; do echo "$crd:"; kubectl get $(echo $crd | sed 's/customresourcedefinitions.apiextensions.k8s.io\///') -A 2>/dev/null | head -5; done`. You can also use `kubectl api-resources` to see all resources (including CRD-backed ones) and their short names. CRDs from well-known operators are easy to identify by their group names: `cert-manager.io`, `monitoring.coreos.com`, `gateway.networking.k8s.io`, etc. -
You create a CRD with
scope: Namespacedand a custom resource in theproductionnamespace. A developer in thestagingnamespace trieskubectl get databasesand sees nothing. They think the CRD is broken. What do you tell them?Answer
The CRD is working correctly. Because it's defined as `scope: Namespaced`, custom resources exist within specific namespaces — just like ConfigMaps or Secrets. The Database CR was created in `production`, so it's only visible there. The developer needs to either create a Database CR in `staging` or look in the right namespace with `kubectl get databases -n production`. If the resource should be visible cluster-wide (like a shared configuration), the CRD should use `scope: Cluster` instead, but that's a design decision with trade-offs — cluster-scoped resources can't be isolated by namespace RBAC. -
You are tasked with deploying a new
KafkaTopiccustom resource provided by another team. When you runkubectl apply -f topic.yaml, you receive the error:The KafkaTopic "orders-topic" is invalid: spec.partitions: Invalid value: 0: spec.partitions in body should be greater than or equal to 1. You check yourtopic.yamland seepartitions: 0. The developer who gave you the file insists that “0 partitions” means auto-scaling in their operator logic. Why did this happen, and how should it be resolved?Answer
The error is generated by the Kubernetes API server, not the Kafka operator. The team that authored the `KafkaTopic` CRD defined an `openAPIV3Schema` that strictly enforces `spec.partitions` to have a `minimum: 1`. Because the API server validates the custom resource against this schema before the operator even sees it, the resource is rejected at the API level. To resolve this, the CRD author must either update the CRD schema to allow `0` as a valid value (if auto-scaling is indeed a supported feature), or you must update your YAML to provide a valid partition count greater than or equal to 1.
Hands-On Exercise
Section titled “Hands-On Exercise”Task: Work with a CRD and Custom Resources.
Part 1: Create a CRD
cat << 'EOF' | k apply -f -apiVersion: apiextensions.k8s.io/v1kind: CustomResourceDefinitionmetadata: name: websites.example.comspec: group: example.com versions: - name: v1 served: true storage: true schema: openAPIV3Schema: type: object properties: spec: type: object properties: domain: type: string replicas: type: integer scope: Namespaced names: plural: websites singular: website kind: Website shortNames: - wsEOF
# Verify CRD createdk get crd websites.example.comPart 2: Create Custom Resources
cat << 'EOF' | k apply -f -apiVersion: example.com/v1kind: Websitemetadata: name: my-blogspec: domain: blog.example.com replicas: 3---apiVersion: example.com/v1kind: Websitemetadata: name: my-shopspec: domain: shop.example.com replicas: 5EOF
# List using different namesk get websitesk get websitek get wsPart 3: Inspect and Modify
# Describek describe website my-blog
# Get YAMLk get ws my-blog -o yaml
# Editk edit website my-blog# Change replicas to 2Part 4: Explore API
# Check API resourcesk api-resources | grep example.com
# Use explaink explain websiteCleanup:
k delete website my-blog my-shopk delete crd websites.example.comPractice Drills
Section titled “Practice Drills”Drill 1: List CRDs (Target: 1 minute)
Section titled “Drill 1: List CRDs (Target: 1 minute)”# List all CRDsk get crd
# Count CRDsk get crd --no-headers | wc -lDrill 2: Describe a CRD (Target: 1 minute)
Section titled “Drill 2: Describe a CRD (Target: 1 minute)”# If cert-manager or similar is installedk describe crd certificates.cert-manager.io 2>/dev/null || echo "cert-manager not installed"
# Otherwise use any CRDk get crd -o name | head -1 | xargs k describeDrill 3: Create CRD (Target: 3 minutes)
Section titled “Drill 3: Create CRD (Target: 3 minutes)”cat << 'EOF' | k apply -f -apiVersion: apiextensions.k8s.io/v1kind: CustomResourceDefinitionmetadata: name: backups.drill.example.comspec: group: drill.example.com versions: - name: v1 served: true storage: true schema: openAPIV3Schema: type: object properties: spec: type: object properties: schedule: type: string retention: type: integer scope: Namespaced names: plural: backups singular: backup kind: Backup shortNames: - bkEOF
k get crd backups.drill.example.comk delete crd backups.drill.example.comDrill 4: Create and Query CR (Target: 3 minutes)
Section titled “Drill 4: Create and Query CR (Target: 3 minutes)”# First create CRDcat << 'EOF' | k apply -f -apiVersion: apiextensions.k8s.io/v1kind: CustomResourceDefinitionmetadata: name: tasks.drill.example.comspec: group: drill.example.com versions: - name: v1 served: true storage: true schema: openAPIV3Schema: type: object properties: spec: type: object properties: priority: type: string scope: Namespaced names: plural: tasks singular: task kind: TaskEOF
# Create CRcat << 'EOF' | k apply -f -apiVersion: drill.example.com/v1kind: Taskmetadata: name: important-taskspec: priority: highEOF
# Queryk get tasksk describe task important-taskk get task important-task -o yaml
# Cleanupk delete task important-taskk delete crd tasks.drill.example.comDrill 5: Check API Resources (Target: 2 minutes)
Section titled “Drill 5: Check API Resources (Target: 2 minutes)”# List all API resourcesk api-resources
# Filter for a specific groupk api-resources | grep networking
# Show only CRD-backed resources (custom)k api-resources | grep -v "^NAME" | grep "\."Drill 6: Use kubectl explain on CRD (Target: 2 minutes)
Section titled “Drill 6: Use kubectl explain on CRD (Target: 2 minutes)”# Create a simple CRDcat << 'EOF' | k apply -f -apiVersion: apiextensions.k8s.io/v1kind: CustomResourceDefinitionmetadata: name: configs.drill.example.comspec: group: drill.example.com versions: - name: v1 served: true storage: true schema: openAPIV3Schema: type: object properties: spec: type: object properties: key: type: string value: type: string scope: Namespaced names: plural: configs singular: config kind: ConfigEOF
# Use explaink explain configk explain config.spec
# Cleanupk delete crd configs.drill.example.comDrill 7: Troubleshoot CRD Validation (Target: 4 minutes)
Section titled “Drill 7: Troubleshoot CRD Validation (Target: 4 minutes)”Task: Create a CRD with strict schema validation, attempt to create an invalid CR, observe the error, and fix it.
# 1. Create a CRD with validationcat << 'EOF' | k apply -f -apiVersion: apiextensions.k8s.io/v1kind: CustomResourceDefinitionmetadata: name: caches.drill.example.comspec: group: drill.example.com versions: - name: v1 served: true storage: true schema: openAPIV3Schema: type: object required: ["spec"] properties: spec: type: object required: ["memoryLimit"] properties: memoryLimit: type: integer minimum: 128 scope: Namespaced names: plural: caches singular: cache kind: CacheEOF
# 2. Try to apply an invalid CR (memoryLimit is a string instead of integer, and too small)cat << 'EOF' | k apply -f -apiVersion: drill.example.com/v1kind: Cachemetadata: name: bad-cachespec: memoryLimit: "64"EOF
# Notice the validation error from the API server!# error: ValidationError(Cache.spec.memoryLimit): invalid type for drill.example.com/v1.Cache.spec.memoryLimit: got "string", expected "integer"
# 3. Fix the CR by providing a valid integer >= 128cat << 'EOF' | k apply -f -apiVersion: drill.example.com/v1kind: Cachemetadata: name: good-cachespec: memoryLimit: 256EOF
# 4. Verify it was created successfullyk get cache good-cache
# 5. Cleanupk delete crd caches.drill.example.comNext Module
Section titled “Next Module”Part 4 Cumulative Quiz - Test your mastery of environment, configuration, and security topics.