What is CRD ?
A CRD (Custom Resource Definition) is a Kubernetes feature that lets you teach your cluster about new types of resources. Instead of just using standard Kubernetes objects like Pods or Deployments, a CRD lets you define your own custom objects (like a Database or VirtualService) so you can manage them natively.In plain terms, a CRD acts as a blueprint or schema.
It tells the Kubernetes system: "Here is a new kind of object I want to create, and here are the rules/fields it accepts."
Real-World Examples
- cert-manager: Uses a CRD to allow you to easily request and manage SSL/TLS Certificates inside your cluster
- ArgoCD: Uses a CRD to define Applications, letting Kubernetes handle the continuous deployment of your code
- Istio: Uses CRDs to define network routing rules like VirtualService
- Prometheus Operator: Prometheus, ServiceMonitor, PodMonitor
What is CR ?
A CR (Custom Resource) is the actual instance or object created from a CRD blueprint.
If the CRD (Custom Resource Definition) is the code that defines a new cookie cutter, the CR (Custom Resource) is the actual cookie you make with it.
A Real-World Example
Prometheus Operator : You cannot create a monitoring rule until the cluster knows what it is.
- The CRD: The operator installs a blueprint called ServiceMonitor
- The CR: You write a YAML file using kind: ServiceMonitor filled with your specific app details.
- When you run
kubectl apply -f my-file.yamlyou have just created a CR.
Don't talk, just show
STEP 1: Deploying CRD
CRD => crd.yaml
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: greetings.example.com
spec:
group: example.com
versions:
- name: v1
served: true
storage: true
schema:
openAPIV3Schema:
type: object
properties:
spec:
type: object
properties:
name:
type: string
scope: Namespaced
names:
plural: greetings
singular: greeting
kind: Greeting
shortNames:
- gr
Line by line:
- group: example.com — namespaces your API so it doesn't clash with built-in Kubernetes types. Full API path becomes example.com/v1.
- schema.openAPIV3Schema — this is the validation rule. It says a Greeting's spec can have a name field, must be a string. If you try to apply a CR with spec.name: 5 (a number), the API server rejects it before it even reaches your operator.
- names.plural/singular/kind — how kubectl refers to it (kubectl get greetings, kubectl get gr).
- scope: Namespaced — these objects live inside a namespace, like Pods (as opposed to cluster-scoped, like Nodes).
Apply it
kubectl apply -f crd.yaml
kubectl get crd greetings.example.com
STEP 2: Creating CR
CR => my-greeting.yaml
apiVersion: example.com/v1
kind: Greeting
metadata:
name: hello-user
spec:
name: user
Don't apply this yet — apply it after the operator is running, so you can watch it react live.
What is Operator ?
An Operator is the brains or the "worker drone" behind a CRD.
If the CRD is the instruction manual and the CR is the order form, the Operator is the human worker who reads the form and actually builds the product.
In plain terms, an Operator is a custom script or program running inside your cluster that automates complex human tasks. It replaces the need for an engineer to manually log in, run commands, and fix broken applications.
The Loop: How It Thinks
An Operator functions using a continuous Reconciliation Loop. It constantly repeats three basic steps, thousands of times a day:
- Observe: Check the current state of the cluster (What is actually running?).
- Analyze: Check the desired state from your CR (What did the user ask for?).
- Act: Fix any differences (If something is broken or missing, fix it).
A Real-World Scenario
Imagine you deploy a CR asking for a kind: Database with 3 replicas.
- Day 1 (Creation): The Operator reads your CR. It realizes 0 databases exist. It automatically creates 3 database pods and provisions cloud storage.
- Day 3 (Accident): A hardware failure kills one of your database pods.
- The Operator's Action: The Operator notices the count dropped from 3 to 2. Without you waking up at 2:00 AM, it instantly spins up a new pod, attaches the existing storage, and resynchronises the data.
Operator vs. Controller
You will often hear the terms "Controller" and "Operator" used interchangeably, but there is a slight difference:
- Controller: A general Kubernetes background process (e.g., keeping standard Pods running).
- Operator: A specific type of controller that packages human operational knowledge for a specific tool (like knowing exactly how to back up a PostgreSQL database or upgrade Prometheus without losing data).
STEP 3: Building an operator
rbac.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
name: greeting-operator
namespace: default
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: greeting-operator-role
rules:
- apiGroups: ["example.com"]
resources: ["greetings"]
verbs: ["get", "list", "watch", "patch"]
- apiGroups: [""]
resources: ["configmaps"]
verbs: ["create", "delete", "get", "list", "watch"]
- apiGroups: ["apiextensions.k8s.io"]
resources: ["customresourcedefinitions"]
verbs: ["get", "list", "watch"]
- apiGroups: [""]
resources: ["events"]
verbs: ["create", "patch"]
- apiGroups: [""]
resources: ["namespaces"]
verbs: ["list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: greeting-operator-binding
subjects:
- kind: ServiceAccount
name: greeting-operator
namespace: default
roleRef:
kind: ClusterRole
name: greeting-operator-role
apiGroup: rbac.authorization.k8s.io
- ServiceAccount — an identity for the Pod to act as (instead of using your personal kubeconfig identity).
- ClusterRole — a set of permissions. Notice it mirrors exactly what your Python code does: watch greetings, create/delete configmaps, plus events (kopf logs Kubernetes Events for visibility) and reading the CRD itself (kopf checks this at startup).
- ClusterRoleBinding — glues the ServiceAccount to the ClusterRole. Without this, the ServiceAccount exists but has zero permissions — RBAC is deny-by-default.
operator.py
import kopf
import kubernetes.client as k8s
@kopf.on.create('example.com', 'v1', 'greetings')
def create_greeting(spec, name, namespace, logger, **kwargs):
person_name = spec.get('name', 'World')
message = f"Hello, {person_name}! This ConfigMap was created by the operator."
logger.info(f"Reconciling Greeting '{name}': creating ConfigMap")
api = k8s.CoreV1Api()
configmap = k8s.V1ConfigMap(
metadata=k8s.V1ObjectMeta(name=f"{name}-greeting-cm"),
data={"message": message}
)
api.create_namespaced_config_map(namespace=namespace, body=configmap)
return {"message": "ConfigMap created"}
@kopf.on.delete('example.com', 'v1', 'greetings')
def delete_greeting(name, namespace, logger, **kwargs):
logger.info(f"Greeting '{name}' deleted — cleaning up ConfigMap")
api = k8s.CoreV1Api()
api.delete_namespaced_config_map(name=f"{name}-greeting-cm", namespace=namespace)
What each part means, plain English:
- import kopf — this library does all the heavy lifting: opening a watch connection to the API server, filtering events for your CRD, retrying on failure. Without it, you'd hand-write hundreds of lines of client-go-equivalent boilerplate.
- @kopf.on.create('example.com', 'v1', 'greetings') — this decorator says: "call this function whenever a new Greeting object appears." This is the reconcile loop's trigger — kopf watches, and when it sees a create event matching this group/version/plural, it calls your function.
- spec, name, namespace — kopf automatically hands you the CR's spec, its name, and its namespace. No manual parsing of raw JSON.
- Inside the function: read spec.name (what the user wants) → build a message → create a ConfigMap (the "act" step of reconcile).
- @kopf.on.delete(...) — the cleanup counterpart. When someone deletes the Greeting, this fires and deletes the ConfigMap it created — this is exactly the pattern cert-manager uses to clean up secrets, or your earlier Website example would use to clean up Deployments.
- return {"message": "ConfigMap created"} — kopf automatically writes this into the CR's .status field. This is how kubectl get greetings -o yaml shows live operator state, not just your original spec.
Dockerfile
FROM python:3.11-slim
RUN pip install kopf kubernetes
COPY operator.py /operator.py
CMD ["kopf", "run", "/operator.py", "--namespace=default"]
Build and load image in minikube
docker build -t greeting-operator:1.0.0 .
minikube image load greeting-operator:1.0.0
deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: greeting-operator
namespace: default
spec:
replicas: 1
selector:
matchLabels:
app: greeting-operator
template:
metadata:
labels:
app: greeting-operator
spec:
serviceAccountName: greeting-operator
containers:
- name: operator
image: greeting-operator:1.0.0
imagePullPolicy: Never
- serviceAccountName: greeting-operator — this is what makes the Pod use the permissions from step 1, instead of the default (near-zero-permission) ServiceAccount.
- imagePullPolicy: Never — tells Kubernetes "don't try to pull this from a registry, it's already local." Without this, it'll try to pull from Docker Hub and fail, since we never pushed it anywhere.
Apply everything if not applied except cr
kubectl apply -f crd.yaml
kubectl apply -f rbac.yaml
kubectl apply -f deployment.yaml
Verification
kubectl get pods -l app=greeting-operator
kubectl logs -f deployment/greeting-operator
STEP : Trigger
In another terminal:
kubectl apply -f my-greeting.yaml
kubectl logs -f deployment/greeting-operator # watch it react
kubectl get cm/hello-user-greeting-cm -o yaml
Sample Output
kubectl logs -f deployment/greeting-operator
[2026-07-01 15:26:53,077] kopf._core.engines.a [INFO ] Initial authentication has been initiated.
[2026-07-01 15:26:53,078] kopf.activities.auth [INFO ] Activity 'login_via_client' succeeded.
[2026-07-01 15:26:53,078] kopf._core.engines.a [INFO ] Initial authentication has finished.
[2026-07-01 15:27:56,418] kopf.objects [INFO ] [default/hello-user] Reconciling Greeting 'hello-user': creating ConfigMap
[2026-07-01 15:27:56,437] kopf.objects [INFO ] [default/hello-user] Handler 'create_greeting' succeeded.
[2026-07-01 15:27:56,437] kopf.objects [INFO ] [default/hello-user] Creation is processed: 1 succeeded; 0 failed.
[2026-07-01 15:27:56,441] kopf.objects [WARNING ] [default/hello-user] Merge-patching finished with inconsistencies: (('remove', ('status',), {'create_greeting': {'message': 'ConfigMap created'}}, None),)
kubectl get cm/hello-user-greeting-cm -o yaml
apiVersion: v1
data:
message: Hello, user! This ConfigMap was created by the operator.
kind: ConfigMap
metadata:
creationTimestamp: "2026-07-01T15:27:56Z"
name: hello-user-greeting-cm
namespace: default
resourceVersion: "78755"
uid: 184008f8-39d8-4484-baf1-d1256c66bb93
Top comments (0)