πŸ“šBook Signing at KubeCon EU 2026Meet us at Booking.com HQ (Mon 18:30-21:00) & vCluster booth #521 (Tue 24 Mar, 12:30-1:30pm) β€” free book giveaway!RSVP Booking.com Event
Configuration advanced ⏱ 12 minutes K8s 1.28+

K8s Custom Resources: CRD Development

Create Kubernetes Custom Resource Definitions with schema validation, additional printer columns, subresources, and conversion webhooks.

By Luca Berton β€’ β€’ πŸ“– 5 min read

πŸ’‘ Quick Answer: CustomResourceDefinition extends the Kubernetes API with your own resource types. Define a CRD with kubectl apply, then create instances with kubectl apply. CRDs support schema validation, status subresource, additional printer columns, and versioning. Use CRDs + controllers for the operator pattern β€” automated management of complex applications.

The Problem

Kubernetes built-in resources don’t cover application-specific needs:

  • Representing a database cluster (replicas, backup schedule, version)
  • Managing certificates (issuer, renewal, domains)
  • Defining network policies at higher abstraction levels
  • Application-specific configuration as first-class Kubernetes objects

The Solution

Define a CRD

apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
  name: databases.example.com
spec:
  group: example.com
  names:
    kind: Database
    listKind: DatabaseList
    plural: databases
    singular: database
    shortNames:
    - db
    categories:
    - all                    # Shows in kubectl get all
  scope: Namespaced
  
  versions:
  - name: v1
    served: true
    storage: true
    
    schema:
      openAPIV3Schema:
        type: object
        properties:
          spec:
            type: object
            required: ["engine", "version", "replicas"]
            properties:
              engine:
                type: string
                enum: ["postgresql", "mysql", "mariadb"]
              version:
                type: string
                pattern: '^\d+\.\d+(\.\d+)?$'
              replicas:
                type: integer
                minimum: 1
                maximum: 7
              storage:
                type: object
                properties:
                  size:
                    type: string
                    pattern: '^\d+Gi$'
                  storageClass:
                    type: string
              backup:
                type: object
                properties:
                  schedule:
                    type: string
                  retention:
                    type: string
                    default: "7d"
          status:
            type: object
            properties:
              phase:
                type: string
              readyReplicas:
                type: integer
              message:
                type: string
    
    # Extra columns in kubectl get
    additionalPrinterColumns:
    - name: Engine
      type: string
      jsonPath: .spec.engine
    - name: Version
      type: string
      jsonPath: .spec.version
    - name: Replicas
      type: integer
      jsonPath: .spec.replicas
    - name: Status
      type: string
      jsonPath: .status.phase
    - name: Age
      type: date
      jsonPath: .metadata.creationTimestamp
    
    # Enable status subresource
    subresources:
      status: {}
      # scale:                  # Optional: enable kubectl scale
      #   specReplicasPath: .spec.replicas
      #   statusReplicasPath: .status.readyReplicas

Create Custom Resources

apiVersion: example.com/v1
kind: Database
metadata:
  name: production-db
  namespace: production
spec:
  engine: postgresql
  version: "16.2"
  replicas: 3
  storage:
    size: 100Gi
    storageClass: fast-ssd
  backup:
    schedule: "0 2 * * *"
    retention: "30d"
# Apply CRD first
kubectl apply -f database-crd.yaml

# Then create instances
kubectl apply -f production-db.yaml

# Use short name
kubectl get db
# NAME            ENGINE       VERSION   REPLICAS   STATUS   AGE
# production-db   postgresql   16.2      3          Ready    5m

# Describe
kubectl describe db production-db

# Delete
kubectl delete db production-db

# kubectl explain works too
kubectl explain database.spec

Status Subresource

# Status is updated separately from spec
# Controller updates status:
kubectl patch database production-db --type=merge --subresource=status \
  -p '{"status":{"phase":"Ready","readyReplicas":3,"message":"All replicas healthy"}}'

# Users update spec:
kubectl patch database production-db --type=merge \
  -p '{"spec":{"replicas":5}}'

# Status can't be changed with regular kubectl apply
# Only --subresource=status can modify .status

RBAC for Custom Resources

apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: database-operator
  namespace: production
rules:
- apiGroups: ["example.com"]
  resources: ["databases"]
  verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
- apiGroups: ["example.com"]
  resources: ["databases/status"]
  verbs: ["get", "update", "patch"]

Validation Patterns

# String validation
properties:
  name:
    type: string
    minLength: 3
    maxLength: 63
    pattern: '^[a-z][a-z0-9-]*$'

# Enum validation
  engine:
    type: string
    enum: ["postgresql", "mysql", "mariadb"]

# Number validation
  replicas:
    type: integer
    minimum: 1
    maximum: 7
    default: 1

# Nested required fields
  spec:
    type: object
    required: ["engine", "version"]

# Additional properties blocked
  spec:
    type: object
    additionalProperties: false    # Rejects unknown fields

CRD Versioning

versions:
- name: v1
  served: true
  storage: true       # Only ONE version can be storage
  schema: ...

- name: v2
  served: true
  storage: false
  schema: ...         # v2 has different/extended schema

# With conversion webhook for v1 ↔ v2
conversion:
  strategy: Webhook
  webhook:
    clientConfig:
      service:
        name: database-converter
        namespace: webhook-system
        path: /convert
      caBundle: <base64-CA>
    conversionReviewVersions: ["v1"]

List CRDs

# All CRDs in cluster
kubectl get crd
# NAME                           CREATED AT
# databases.example.com          2026-05-02
# certificates.cert-manager.io   2026-01-15

# CRD details
kubectl describe crd databases.example.com

# API resources includes CRDs
kubectl api-resources | grep example.com
# databases   db   example.com/v1   true   Database

# Delete CRD (DELETES ALL custom resources!)
kubectl delete crd databases.example.com

Common Issues

β€œno matches for kind” after CRD apply

CRD not yet established. Wait: kubectl wait --for=condition=Established crd/databases.example.com.

Validation errors on create

Schema doesn’t match. Check: kubectl explain database.spec. Ensure required fields present and types match.

Deleting CRD deletes all instances

By design. Use kubectl delete crd with extreme caution. Consider setting metadata.finalizers on CRs for protection.

Best Practices

  • Always define OpenAPI schema β€” prevents invalid resources
  • Enable status subresource β€” separates user intent (spec) from controller state (status)
  • Use additionalPrinterColumns β€” better kubectl get output
  • Set categories β€” ["all"] makes CRs appear in kubectl get all
  • Version from the start β€” plan for v1β†’v2 migration
  • RBAC on custom resources β€” don’t leave them open to everyone

Key Takeaways

  • CRDs extend the Kubernetes API with custom resource types
  • Schema validation enforces field types, required fields, and patterns
  • Status subresource separates spec (user) from status (controller)
  • Additional printer columns improve kubectl get output
  • CRDs + controllers = operator pattern for automated application management
#crd #custom-resources #api #operators #cka
Luca Berton
Written by Luca Berton

Principal Solutions Architect specializing in Kubernetes, AI/GPU infrastructure, and cloud-native platforms. Author of Kubernetes Recipes and creator of CopyPasteLearn courses.

Kubernetes Recipes book cover

Want More Kubernetes Recipes?

This recipe is from Kubernetes Recipes, our 750-page practical guide with hundreds of production-ready patterns.

Luca Berton Ansible Pilot Ansible by Example Open Empower K8s Recipes Terraform Pilot CopyPasteLearn ProteinLens