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.