Skip to content

CRDs & Operators

Extend Kubernetes with custom resources and build a simple operator that manages them.

Time: ~20 minutes Difficulty: Advanced

  • CustomResourceDefinitions (CRDs): teaching Kubernetes new resource types
  • Creating and managing custom resources with kubectl
  • The operator pattern: a controller that watches CRs and creates real resources
  • The reconciliation loop: desired state vs actual state
  • Printer columns, validation schemas, and status subresources

An operator is a controller that watches custom resources and makes the cluster match their desired state. In this demo:

  1. You define a Website CRD (what fields a Website has)
  2. You create Website instances (“I want a blog with 2 replicas”)
  3. The operator watches for Websites and creates Deployments + Services to match
You create: Website CR ──> Operator watches ──> Creates Deployment + Service
You update: Website CR ──> Operator notices ──> Updates Deployment
You delete: Website CR ──> Operator notices ──> Deletes Deployment + Service

Navigate to the demo directory:

Terminal window
cd demos/crds-and-operators
Terminal window
kubectl apply -f manifests/website-crd.yaml

Kubernetes now knows about the Website resource type:

Terminal window
kubectl api-resources | grep website
kubectl explain website.spec

Step 2: Create Website instances (without the operator)

Section titled “Step 2: Create Website instances (without the operator)”
Terminal window
kubectl apply -f manifests/namespace.yaml
kubectl apply -f manifests/website-samples.yaml

The resources are stored in etcd, but nothing happens yet. No pods, no services:

Terminal window
kubectl get websites -n crd-demo
kubectl get pods -n crd-demo

CRDs are just data. An operator is needed to act on them.

Terminal window
kubectl apply -f manifests/operator-rbac.yaml
kubectl apply -f manifests/operator-script.yaml
kubectl apply -f manifests/operator.yaml

Watch the operator create Deployments and Services for each Website:

Terminal window
kubectl logs -f deploy/website-operator -n crd-demo

In another terminal, check the resources it created:

Terminal window
kubectl get pods -n crd-demo
kubectl get svc -n crd-demo

You should see website-my-blog (2 replicas) and website-docs-site (1 replica).

Terminal window
kubectl port-forward svc/website-my-blog 8081:80 -n crd-demo &
kubectl port-forward svc/website-docs-site 8082:80 -n crd-demo &

Open http://localhost:8081 and http://localhost:8082.

Terminal window
kubectl patch website my-blog -n crd-demo \
--type=merge -p '{"spec":{"replicas":3}}'

Wait 10 seconds (the operator polls every 10s), then check:

Terminal window
kubectl get pods -l app=website-my-blog -n crd-demo

The operator scaled the Deployment to 3 replicas.

Terminal window
kubectl patch website my-blog -n crd-demo \
--type=merge -p '{"spec":{"title":"Updated Blog Title"}}'

Wait 10 seconds, then refresh http://localhost:8081.

manifests/
namespace.yaml # crd-demo namespace
website-crd.yaml # CRD: defines the Website resource type
website-samples.yaml # Two Website instances (my-blog, docs-site)
operator-rbac.yaml # ServiceAccount + Role + RoleBinding for the operator
operator-script.yaml # Shell script implementing the reconciliation loop
operator.yaml # Deployment running the operator

The operator is a shell script (for learning purposes). It runs a loop every 10 seconds:

  1. Lists all Website CRs in the namespace
  2. For each Website, creates or updates a Deployment with the specified replicas
  3. Creates a Service for each Website

Real operators use frameworks like:

  • Go: controller-runtime / Operator SDK
  • Python: kopf
  • Java: Java Operator SDK

They use watches (event streams) instead of polling, and they handle deletion, owner references, finalizers, and status updates properly.

  1. Create a new Website:

    Terminal window
    kubectl apply -f - <<'EOF'
    apiVersion: demo.example.com/v1
    kind: Website
    metadata:
    name: landing-page
    namespace: crd-demo
    spec:
    title: "Product Landing Page"
    replicas: 3
    color: "#E91E63"
    EOF
  2. Check that the CRD validates input:

    Terminal window
    # This should fail (replicas > 5)
    kubectl apply -f - <<'EOF'
    apiVersion: demo.example.com/v1
    kind: Website
    metadata:
    name: too-many
    namespace: crd-demo
    spec:
    title: "Won't work"
    replicas: 10
    EOF
  3. Use the short name:

    Terminal window
    kubectl get ws -n crd-demo
Terminal window
kubectl delete namespace crd-demo
kubectl delete crd websites.demo.example.com

See docs/deep-dive.md for a detailed explanation of CRD versioning, conversion webhooks, owner references, finalizers, the controller-runtime framework, and how production operators like CloudNativePG implement these patterns.

Move on to Service Types to learn the differences between ClusterIP, NodePort, LoadBalancer, and ExternalName.