Kubernetes CRDs let you extend the Kubernetes API with your own custom object types, fundamentally changing how you manage your cluster’s state.

Let’s see this in action. Imagine you want to manage database instances directly within Kubernetes. You’d define a Database Custom Resource Definition (CRD).

apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
  name: databases.stable.example.com
spec:
  group: stable.example.com
  versions:
    - name: v1
      served: true
      storage: true
      schema:
        openAPIV3Schema:
          type: object
          properties:
            spec:
              type: object
              properties:
                engine:
                  type: string
                  enum: [mysql, postgres]
                version:
                  type: string
                storageGB:
                  type: integer
            status:
              type: object
              properties:
                phase:
                  type: string
                connectionString:
                  type: string
  scope: Namespaced
  names:
    plural: databases
    singular: database
    kind: Database
    shortNames:
    - db

Once this CRD is applied, you can create Database objects:

apiVersion: stable.example.com/v1
kind: Database
metadata:
  name: my-app-db
spec:
  engine: postgres
  version: "14"
  storageGB: 100

This Database object is now a first-class citizen in your Kubernetes cluster, just like Pods or Deployments. The Kubernetes API server understands databases.stable.example.com.

The core problem CRDs solve is enabling declarative management of anything within Kubernetes. Instead of shoehorning custom logic into existing Kubernetes objects (like using annotations on Deployments to track application-specific states), you define new, semantically meaningful objects that perfectly represent your application’s components. This aligns with Kubernetes’ philosophy of treating all cluster resources as declarative objects.

Internally, when you apply a CRD, the Kubernetes API server registers a new API endpoint for your custom resource. The etcd datastore then stores these custom objects under the specified group, version, and plural name. When you create a Database object, the API server validates it against the schema defined in the CRD and stores it. A controller (which you’ll write separately) then watches for these Database objects, reconciles their state, and performs the actual work (e.g., provisioning a PostgreSQL instance, configuring storage, and updating the status field of the Database object).

You control the shape of your custom resources through the spec field, defining the desired state. The status field is then used by controllers to report the observed state back to the user. The group, version, scope (Namespaced or Cluster), and names (plural, singular, kind) fields are crucial for API registration and discoverability. The schema field, using OpenAPI v3, enforces the structure and types of your custom resource, preventing invalid configurations from being applied.

The versions field is powerful; you can define multiple versions of your CRD simultaneously, allowing for API evolution. For example, you could add a replicaCount to spec in a v2 version while keeping v1 available, enabling a smooth migration path for users of your custom resource.

Most people don’t realize that CRDs, by default, do not come with any controllers. Applying a CRD merely teaches the Kubernetes API server about a new object type. The actual behavior — what happens when you create, update, or delete a Database object — is entirely up to you to implement in a separate controller. This controller acts as a reconciliation loop, continuously comparing the desired state in the spec with the observed state in the status and taking action to bridge any gaps.

The next concept you’ll likely encounter is writing and deploying the controller that actually acts upon your custom resources.

Want structured learning?

Take the full Kubernetes course →