Custom Resource Definitions: Extending Kubernetes the Right Way

Introduction

Kubernetes is powerful, but what if its built-in objects like Pods, Services, and Deployments aren’t enough for your application’s needs? That’s where Custom Resource Definitions (CRDs) come in!

In this post, I’ll walk you through:
✅ Why CRDs are needed
✅ How to create a CRD from scratch
✅ Implementing a custom controller
✅ Deploying and managing custom resources

Why Extend Kubernetes?

Kubernetes comes with a standard set of APIs (like apps/v1 for Deployments), but many applications require domain-specific concepts that Kubernetes doesn’t provide natively.

For example:
A database team might want a Database object instead of manually managing StatefulSets.
A security team might want a FirewallRule object to enforce policies at the cluster level.

With CRDs, you can define custom objects tailored to your use case and make them first-class citizens in Kubernetes!

Step 1: Creating a Custom Resource Definition (CRD)

A CRD allows Kubernetes to recognize new object types. Let’s create a CRD for a PostgreSQL database instance.

Save the following YAML as postgresql-crd.yaml:

apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
  name: postgresqls.mycompany.com
spec:
  group: mycompany.com
  names:
    kind: PostgreSQL
    plural: postgresqls
    singular: postgresql
  scope: Namespaced
  versions:
    - name: v1alpha1
      served: true
      storage: true
      schema:
        openAPIV3Schema:
          type: object
          properties:
            spec:
              type: object
              properties:
                databaseName:
                  type: string
                storageSize:
                  type: string
                replicas:
                  type: integer
Click Here to Copy YAML

Apply the CRD to Kubernetes

kubectl apply -f postgresql-crd.yaml

Now, Kubernetes knows about the PostgreSQL resource!

Step 2: Creating a Custom Resource Instance

Let’s create an actual PostgreSQL instance using our CRD.

Save the following YAML as postgresql-instance.yaml:

apiVersion: mycompany.com/v1alpha1
kind: PostgreSQL
metadata:
  name: my-database
spec:
  databaseName: mydb
  storageSize: "10Gi"
  replicas: 2
Click Here to Copy YAML

Apply the Custom Resource

kubectl apply -f postgresql-instance.yaml

Kubernetes now understands PostgreSQL objects, but it won’t do anything with them yet. That’s where controllers come in!

Step 3: Building a Kubernetes Controller

A controller watches for changes in custom resources and performs necessary actions.

Here’s a basic Go-based controller using controller-runtime:

package controllers

import (
	"context"
	"fmt"

	"k8s.io/apimachinery/pkg/types"
	ctrl "sigs.k8s.io/controller-runtime"
	"sigs.k8s.io/controller-runtime/pkg/client"
)

type PostgreSQLReconciler struct {
	client.Client
}

func (r *PostgreSQLReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
	fmt.Println("Reconciling PostgreSQL instance:", req.NamespacedName)

	// Fetch the PostgreSQL instance
	var pgInstance PostgreSQL
	if err := r.Get(ctx, req.NamespacedName, &pgInstance); err != nil {
		return ctrl.Result{}, client.IgnoreNotFound(err)
	}

	// Implement database provisioning logic here

	return ctrl.Result{}, nil
}

func (r *PostgreSQLReconciler) SetupWithManager(mgr ctrl.Manager) error {
	return ctrl.NewControllerManagedBy(mgr).
		For(&PostgreSQL{}).
		Complete(r)
}
Click Here to Copy Go Language

Deploying the Controller

To deploy this, we use Kubebuilder and the Operator SDK:

operator-sdk init --domain mycompany.com --repo github.com/mycompany/postgres-operator
operator-sdk create api --group mycompany --version v1alpha1 --kind PostgreSQL --resource --controller
make manifests
make install
make run

Your Kubernetes Operator is now watching for PostgreSQL objects and taking action!

Step 4: Deploying and Testing the Operator

Apply the CRD and PostgreSQL resource:

kubectl apply -f postgresql-crd.yaml
kubectl apply -f postgresql-instance.yaml

Check if the custom resource is recognized:

kubectl get postgresqls.mycompany.com

Check the controller logs to see it processing the custom resource:

kubectl logs -l control-plane=controller-manager

If everything works, your PostgreSQL resource is being managed automatically!

Conclusion: Why Use CRDs?

  • Encapsulate Business Logic: No need to manually configure every deployment—just define a custom resource, and the operator handles it.
  • Standard Kubernetes API: Developers can use kubectl to interact with custom resources just like native Kubernetes objects.
  • Automated Workflows: Kubernetes Operators can provision, update, and heal application components automatically.

By implementing Custom Resource Definitions and Operators, you extend Kubernetes the right way—without hacking it!

What are some use cases where CRDs and Operators helped your team? Let’s discuss in the comments!👇

Leave a comment