Skip to main content
Sometimes you need Kubernetes to store and validate a resource that doesn’t exist yet (for example, a WebApp). Scaffolding tools can feel like a black box. The reliable, explicit solution is a CustomResourceDefinition (CRD): a Kubernetes object that teaches the API server about your new kind so it can be discovered, validated, and stored.

CRD basics

A CRD is itself a Kubernetes object and uses the same top-level fields you already know: apiVersion, kind, metadata, and spec. For CRDs:
  • apiVersion is always apiextensions.k8s.io/v1
  • kind is CustomResourceDefinition
  • metadata.name must be set to the plural form of your resource followed by a dot and the API group (see note below)
  • spec contains group, scope, names, and versions
Always set metadata.name to <plural>.<group>. For example, if your group is webapp.kodekloud.com and your plural is webapps, the metadata name must be webapps.webapp.kodekloud.com.
Minimal header example:
Quick reference: top-level CRD spec fields

Versions and schema validation

A CRD can serve multiple versions. Important rules:
  • Exactly one version must have storage: true — this is the version persisted to etcd.
  • Versions you want available via the API server should have served: true.
  • Validation and defaults are expressed using openAPIV3Schema for each version.
We’ll start with a single version v1alpha1 that is both served and storage. The following schema declares spec fields and constraints:
  • spec.image — string (required)
  • spec.replicas — integer (required), minimum 1, maximum 10
  • spec.port — integer, default 8080
Complete CRD (with validation and defaults):
When the API server reads this CRD, it will register a new endpoint at: /apis/webapp.kodekloud.com/v1alpha1 and kubectl discovery will expose the new resource types automatically.

Apply the CRD and confirm discovery

Apply the CRD manifest:
Expected creation output:
Check CRD status conditions (wait until the API server finishes registering the CRD; you should see NamesAccepted and Established):
Expected output:
Because discovery is dynamic, kubectl explain immediately recognizes your new type:
Example output:
No API server restart or plugin installation is required — discovery updates automatically.

Schema validation in action

Create a sample WebApp that violates the replicas maximum (set to 99). Save this as sample-webapp.yaml:
Apply it:
The API server will reject the resource due to schema validation. Example error:
Fix the replicas value (for example, to 3) and apply again:
Expected success output:
Now fetch the created resource and inspect the spec (pipe through jq to format). Notice the server-populated default for port:
Expected JSON:
The API server applied the default defined in the CRD schema. This demonstrates both validation and defaulting are enforced at the API level before objects are persisted to etcd.

Next steps and useful extensions

You can extend this CRD to improve UX and integration with kubectl:
  • Add status as a subresource to allow controllers to update status safely.
  • Add a scale subresource to support kubectl scale.
  • Define additionalPrinterColumns so kubectl get webapp displays meaningful columns.
Table: common kubectl checks and commands for CRDs
Two common mistakes to watch for:
  • metadata.name must be plural.group (example: webapps.webapp.kodekloud.com).
  • Exactly one version must have storage: true — that is the version persisted in etcd.
This demonstrates a complete, hand-authored CRD: names, group, versioning, schema validation, and defaults — all defined in a single YAML manifest.

Watch Video

Practice Lab