> ## Documentation Index
> Fetch the complete documentation index at: https://notes.kodekloud.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Demo Building a Custom Resource Definition

> Walkthrough for defining and applying a Kubernetes CustomResourceDefinition for Database with OpenAPI v3 schema validation, example custom resources, and kubectl inspection

Kubernetes includes built-in types like Deployments, Services, and ConfigMaps for common workloads. Platforms often need higher-level, domain-specific abstractions — for example, a Database resource, TenantEnvironment, or MessageQueue — so users can request complex infrastructure without knowing implementation details.

CustomResourceDefinitions (CRDs) extend the Kubernetes API with your own resource types. In this walkthrough we will:

* Define a Database CRD with OpenAPI v3 schema validation.
* Apply the CRD to a cluster.
* Create example custom resources (CRs) to demonstrate validation errors and a final valid CR.
* Inspect the created resource with kubectl.

This guide assumes basic familiarity with kubectl and a running Kubernetes cluster. For more details, see the Kubernetes CRD docs: [CustomResourceDefinitions (CRDs)](https://kubernetes.io/docs/concepts/extend-kubernetes/api-extension/custom-resources/).

## CRD skeleton

Start with a minimal CRD skeleton. We'll fill in details next.

```yaml theme={null}
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
  name:
spec:
  group:
  scope:
  names:
    plural:
    singular:
    kind:
    shortNames:
      -
  versions:
    - name: v1
      served: true
      storage: true
      schema:
        openAPIV3Schema:
          type: object
          properties:
            spec:
              type: object
              required:
                -
              properties:
```

## Top-level fields explained

Use the table below as a quick reference for the main fields in the CRD manifest.

| Field | Purpose | Example |
| - | - | - |
| `metadata.name` | Fully-qualified CRD name; must be `<plural>.<group>` | `databases.platform.example.com` |
| `spec.group` | API domain (a namespace for your custom types) | `platform.example.com` |
| `spec.scope` | Resource scope: `Namespaced` or `Cluster` | `Namespaced` |
| `spec.names` | How the resource is referenced (`plural`, `singular`, `kind`, `shortNames`) | `plural: databases` |
| `spec.versions` | API versions exposed by the CRD. `served: true` accepts requests; `storage: true` is persisted to etcd (only one) | `name: v1` |
| `spec.versions[].schema.openAPIV3Schema` | OpenAPI v3 schema used for server-side validation at object create/update | See schema example below |

Notes:

* `metadata.name` must follow the convention `<plural>.<group>`; the API server enforces this.
* Only one version in `spec.versions` should have `storage: true`. That version is persisted to etcd.
* The OpenAPI v3 schema is validated by the API server when creating or updating custom resources.

OpenAPI v3 schema validation is enforced by the API server at object creation/update time. Only one version may have `storage: true` — that version is the one persisted to etcd.

## Database CRD: concrete example

We'll create a Database CRD in the `platform.example.com` group. It will be `Namespaced` and expose `databases` as the plural. The schema requires two fields in `spec`: `engine` and `size`, both validated with `enum` to limit allowed values.

```yaml theme={null}
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
  name: databases.platform.example.com
spec:
  group: platform.example.com
  scope: Namespaced
  names:
    plural: databases
    singular: database
    kind: Database
    shortNames:
      - db
      - dbs
  versions:
    - name: v1
      served: true
      storage: true
      schema:
        openAPIV3Schema:
          type: object
          properties:
            spec:
              type: object
              required:
                - engine
                - size
              properties:
                engine:
                  type: string
                  enum:
                    - postgresql
                    - mysql
                    - mariadb
                size:
                  type: string
                  enum:
                    - small
                    - medium
                    - large
                version:
                  type: string
                storage:
                  type: string
```

Apply the CRD to your cluster:

```console theme={null}
controlplane ~ ➜ kubectl apply -f database-crd.yaml
customresourcedefinition.apiextensions.k8s.io/databases.platform.example.com created
```

Confirm the new resource appears in the API:

```console theme={null}
controlplane ~ ➜ kubectl api-resources | grep platform
databases                 db,dbs    platform.example.com/v1    true        Database
```

Your custom type now shows up alongside native Kubernetes resources and can be managed with kubectl.

## Create Custom Resources (CRs) — iterate with examples

We'll walk through several CR examples to illustrate typical validation errors and then the successful CR.

1. Missing `metadata.name`\
   If `metadata.name` is omitted, kubectl will fail before schema validation because resource names are required.

```yaml theme={null}
# cr.yaml (invalid - missing metadata.name)
apiVersion: platform.example.com/v1
kind: Database
metadata: {}
spec:
  engine: mongo
```

Applying it:

```console theme={null}
controlplane ~ ➜ kubectl apply -f cr.yaml
error: error when retrieving current configuration of:
Resource: "platform.example.com/v1, Resource=databases", GroupVersionKind: "platform.example.com/v1, Kind=Database"
Name: "", Namespace: "default"
from server for: "cr.yaml": resource name may not be empty
```

2. Unsupported `engine` value\
   Provide a name but use an engine value not allowed by the CRD schema (e.g., `mongodb`). The API server rejects this with a schema validation error.

```yaml theme={null}
# cr.yaml (invalid - unsupported engine)
apiVersion: platform.example.com/v1
kind: Database
metadata:
  name: db-order
  namespace: platform-apis
spec:
  engine: mongodb
```

Applying it:

```console theme={null}
controlplane ~ ➜ kubectl apply -f cr.yaml
The Database "db-order" is invalid: spec.engine: Unsupported value: "mongodb": supported values: "postgresql", "mysql", "mariadb"
```

3. Missing required `size` field\
   Change `engine` to a supported value but omit the required `size` field; the API server will report that `size` is required.

```yaml theme={null}
# cr.yaml (invalid - missing required field `size`)
apiVersion: platform.example.com/v1
kind: Database
metadata:
  name: db-order
  namespace: platform-apis
spec:
  engine: mariadb
```

Applying it:

```console theme={null}
controlplane ~ ➜ kubectl apply -f cr.yaml
The Database "db-order" is invalid: spec.size: Required value
```

4. Final valid CR\
   Add the required `size` and any optional fields (like `version` and `storage`) to create a valid resource.

```yaml theme={null}
# cr.yaml (valid)
apiVersion: platform.example.com/v1
kind: Database
metadata:
  name: db-order
  namespace: platform-apis
spec:
  engine: mariadb
  size: medium
  version: "15"
  storage: 50Gi
```

Apply the valid CR:

```console theme={null}
controlplane ~ ➜ kubectl apply -f cr.yaml
database.platform.example.com/db-order created
```

Verify the resource using the short name:

```console theme={null}
controlplane ~ ➜ kubectl get db -n platform-apis
NAME        AGE
db-order    15s
```

Describe the resource to inspect its fields:

```console theme={null}
controlplane ~ ➜ kubectl describe database db-order -n platform-apis
Name:         db-order
Namespace:    platform-apis
Labels:       ` <none> `
Annotations:  ` <none> `
API Version:  platform.example.com/v1
Kind:         Database
Metadata:
  Creation Timestamp: 2026-04-05T20:54:33Z
  Generation:        1
  Resource Version:  5192
  UID:               f92637d2-5677-46d5-8eff-89dcd7f2a718
Spec:
  Engine:    mariadb
  Size:      medium
  Storage:   50Gi
  Version:   15
Events:      ` <none> `
```

(Backticks around `<none>` are used to avoid MDX interpreting angle brackets as JSX.)

## Summary and next steps

This walkthrough showed how to:

* Define a CRD with server-side OpenAPI v3 validation.
* Apply the CRD so your custom type is available via the Kubernetes API.
* Iterate on CRs and observe the API server enforcing schema constraints.

With a CRD in place, your custom resources become first-class objects in Kubernetes and can be managed with the same tools (kubectl, controllers, RBAC) as built-in resources. Next, consider:

* Writing a controller/operator to reconcile Database resources into actual backing services.
* Adding status subresources and additional validation with `conversion` or admission webhooks.
* Documenting RBAC rules for users who will create Database CRs.

References

* [CustomResourceDefinitions (CRDs) — Kubernetes Docs](https://kubernetes.io/docs/concepts/extend-kubernetes/api-extension/custom-resources/)
* [OpenAPI v3 Schema for CRD Validation](https://kubernetes.io/docs/tasks/extend-kubernetes/custom-resources/custom-resource-definitions/#validation)

> **[Watch Video](https://learn.kodekloud.com/user/courses/prep-course-certified-cloud-native-platform-engineer-cnpe/module/756ffaae-767b-4743-9724-c05d3fbf9a18/lesson/c82aa899-a70b-43ff-b920-da6222c154c1)**

> **[Practice Lab](https://learn.kodekloud.com/user/courses/prep-course-certified-cloud-native-platform-engineer-cnpe/module/756ffaae-767b-4743-9724-c05d3fbf9a18/lesson/57aa85b4-e21b-4311-8593-6489e31df10a)**


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.