Operators: controllers with expert knowledge
Kubernetes knows how to keep 3 Pods running. It does not know how to fail over a PostgreSQL primary, take a backup, or upgrade Kafka without losing messages. An Operator teaches it: you add your own kind of object, like PostgresCluster, plus a controller that knows how to run that software the way an experienced engineer would.
The idea: a robot ops engineer
Imagine the person on your team who knows the database inside out: how to set it up, add a replica, back it up at night, switch to a replica when the primary dies. An operator is that knowledge written as code, running in the cluster, awake 24/7.
Step 1: a new kind of object (CRD)
A CustomResourceDefinition tells the API server: "from now on, accept objects of kind BlogSite, and here is what they may contain". Click the parts:
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: blogsites.example.com
spec:
group: example.com
names:
kind: BlogSite
plural: blogsites
shortNames: [blog]
scope: Namespaced
versions:
- name: v1
served: true
storage: true
schema:
openAPIV3Schema:
type: object
properties:
spec:
type: object
properties:
version: { type: string }
replicas: { type: integer, minimum: 1 }
domain: { type: string }
subresources:
status: {}
group
Your own API group, usually a domain you own, so names never clash with other tools.
names
The new kind and how kubectl calls it. After applying, kubectl get blogsites and kubectl get blog just work.
scope
Namespaced (like Deployments) or Cluster (like Nodes).
schema
Which fields are allowed and their types. The API server rejects a BlogSite with replicas: "lots" before your controller ever sees it.
status
Gives the object a separate status part that only the controller writes, like READY on a Deployment.
A CRD alone does nothing; it only teaches the API server to store your objects. Like a Deployment with no Deployment controller, it is just data.
Step 2: using your new kind
What you write
apiVersion: example.com/v1
kind: BlogSite
metadata:
name: my-blog
spec:
version: "1.4"
replicas: 2
domain: blog.example.comWhat kubectl shows
$ kubectl apply -f my-blog.yaml
blogsite.example.com/my-blog created
$ kubectl get blog
NAME VERSION READY AGE
my-blog 1.4 2/2 40sIt behaves like a built-in object: RBAC, labels, kubectl describe, GitOps tools all work with it. (The VERSION and READY columns come from additionalPrinterColumns in the CRD.)
Step 3: the controller that makes it real
The operator's controller watches BlogSites and, for each one, makes sure the right objects exist. For my-blog that might be the six Deployments and Services from chapter 17, an Ingress for the domain, and a ConfigMap. Change version: "1.5" and the operator rolls every service forward in the right order. Delete the BlogSite and owner references (chapter 45) clean everything up.
One short object in, a whole running app out. That is the payoff.
An operator at work
A database operator handling a bad night, step by step:
PostgresCluster orders-db instances: 3 orders-db-1 primary node-1 orders-db-2 replica node-2 orders-db-3 replica node-3Normal: one primary takes writes, two replicas copy it.
node-1 ✗ orders-db-1 unreachableThe primary's machine dies at 3 a.m.
operator: primary lost operator: orders-db-2 is the most up-to-date replica → promote it operator: point Service orders-db-rw at orders-db-2Plain Kubernetes would only restart a Pod. The operator knows the database rule: promote the best replica, then move the "write" Service.
operator: create a new replica orders-db-4, copy data from orders-db-2 status: instances 3/3 ready, primary orders-db-2Back to 3 healthy members. Your app, which always connects to
orders-db-rw, saw a few seconds of errors and nothing more. Nobody was woken up.
spec: imageName: postgres:16.4 # was 16.3You change one line.
operator: update replica orders-db-3 … ready operator: update replica orders-db-4 … readyReplicas first, one at a time.
operator: switch primary to orders-db-3 (already upgraded) operator: update old primary orders-db-2 as a replicaThen a planned, quick switchover, so the primary is never down for long. A plain StatefulSet rolling update knows nothing about primaries.
Operators you will meet
| Operator | Custom resources | Does |
|---|---|---|
| CloudNativePG | Cluster, Backup | PostgreSQL with failover, backups, upgrades |
| Prometheus Operator | Prometheus, ServiceMonitor | Runs Prometheus and configures what it scrapes |
| cert-manager | Certificate, ClusterIssuer | Gets and renews TLS certificates (chapter 27) |
| Strimzi | Kafka, KafkaTopic | Runs Apache Kafka clusters |
$ kubectl get crds
NAME CREATED AT
certificates.cert-manager.io 2026-03-02T10:14:00Z
clusters.postgresql.cnpg.io 2026-03-02T10:20:31Z
servicemonitors.monitoring.coreos.com 2026-03-02T10:21:05Z
Most are installed with one Helm chart, and many are listed on OperatorHub.io.
How smart is an operator?
Operators range from "installs the thing" to "runs it better than you". A common five-step scale:
| Level | Can |
|---|---|
| 1 · Basic install | Create the app from a custom resource |
| 2 · Upgrades | Move to a new version safely |
| 3 · Full lifecycle | Backups, restores, failover |
| 4 · Deep insights | Metrics, alerts, useful status |
| 5 · Auto pilot | Tunes and scales itself, fixes problems on its own |
When choosing one for a database, check that it reaches at least level 3.
Building one
Kubebuilder and the Operator SDK (built on it) generate the CRD, the controller skeleton and the RBAC rules. You fill in one function: "reconcile this BlogSite". This is how most big operators are written.
kopf for Python, the Java Operator SDK, and client libraries for Node and others. Same idea: you write the reconcile function, the framework handles watching, caching and retries.
For simpler cases, tools such as Helm-based or Ansible-based operators (Operator SDK) or a plain Helm chart plus a small controller may be enough.
When not to write one
Operators are powerful but are real software to maintain. Pick the simpler tool when you can:
- Deploy the blog app's six services with different settings per environment
It is just templated YAML. Nothing needs watching once it is running.
- Run PostgreSQL with failover and nightly backups
Experts already wrote and tested one. Writing your own database operator is a huge project.
- Your company's own product needs complex, ongoing operations on every customer installation
Here the effort pays off: the knowledge only exists in your team, and it is needed continuously.
Recap
- A CRD adds a new kind of object to the API; a custom resource is one such object.
- An operator is a CRD plus a controller that knows how to run a specific piece of software.
- It handles day-2 work: upgrades, backups, failover, scaling.
- Popular ones: CloudNativePG, Prometheus Operator, cert-manager, Strimzi.
- Built with Kubebuilder / Operator SDK (Go), kopf (Python), Java Operator SDK and others.
- Use existing operators freely; write your own only when the knowledge is yours and ongoing.
Quick check
BlogSite and then a BlogSite object. kubectl get blog lists it, but no Pods appear. Why?Show answer
The CRD only lets the API server store BlogSites. Nothing acts on them until the operator's controller is installed and running.
Operators: common questions
What is the difference between a CRD and an Operator?
A CRD only adds a new kind of object to the API. An Operator is a CRD plus a controller that acts on those objects to run and manage an application.