A Kubernetes Operator is a software pattern and a set of tools for extending the Kubernetes API and control loop mechanism to manage complex, application-specific tasks. It allows you to encode the operational knowledge of a human operator—someone who knows how to install, configure, upgrade, backup, recover, and scale a particular application—into a piece of software that runs inside the Kubernetes cluster, continuously reconciling the desired state with the actual state of that application. In other words, an Operator turns "runbook" knowledge into Kubernetes-native automation.
Below is an extensive, deeply detailed explanation of Operators, complemented by examples at every stage to illustrate how they work in real-world scenarios.
Why Operators?
Motivation and Rationale:
- Kubernetes Resource Model: Out of the box, Kubernetes provides a set of controllers and resources for generic container orchestration—like Deployments for stateless applications and StatefulSets for stateful ones. These are great starting points, but they often only provide the basic scaffolding for running applications.
- Complex Lifecycle Management: Many sophisticated applications—such as databases (PostgreSQL, MongoDB), distributed systems (Cassandra, Kafka), or other stateful services—require more nuanced lifecycle management:
- Installation and Configuration: Installing a database might require initializing schemas, setting passwords, or configuring replica sets.
- Upgrades: Upgrading from version 1.2 to 1.3 might require a controlled rolling upgrade, schema migrations, or ensuring data consistency.
- Scaling: Scaling beyond a certain threshold might require adding shards or rebalancing data.
- Backups and Recovery: Applications may need automatic, periodic backups and automated restore procedures.
- Human Expertise as Code: Prior to Operators, cluster administrators might write manual scripts or rely on external automation to perform these tasks. Operators allow you to implement this know-how in a Kubernetes-native manner. They actively observe and reconcile the state of the system, so administrators and developers can simply declare what they want, and the Operator figures out how to get there.
Key Concepts
Custom Resource (CR): A Custom Resource is an extension of the Kubernetes API that allows you to define new resource types. For example, instead of just Deployment or Service, you might define a PostgresCluster resource type that represents a complete PostgreSQL cluster configuration.
Example:
| apiVersion: database.example.com/v1 kind: PostgresCluster metadata: name: my-postgres spec: version: 13.3 size: 3 storage: size: 20Gi backup: schedule: "0 2 * * *" # daily at 2 AM |
Applying this CR (kubectl apply -f postgrescluster.yaml) tells the Operator: "I want a PostgreSQL cluster of size 3, running version 13.3, with daily backups." The Operator then ensures that this desired state is met.
Custom Resource Definition (CRD): A CRD is what allows Kubernetes to understand new resource types. Once the CRD is installed in the cluster, the Kubernetes API server treats the defined resource like a first-class citizen. Operators rely on CRDs to introduce domain-specific APIs.
Example (CRD for PostgresCluster):
| apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: name: postgresclusters.database.example.com spec: group: database.example.com versions: – name: v1 served: true storage: true schema: openAPIV3Schema: type: object properties: spec: type: object properties: version: type: string size: type: integer storage: type: object properties: size: type: string backup: type: object properties: schedule: type: string scope: Namespaced names: plural: postgresclusters singular: postgrescluster kind: PostgresCluster shortNames: – pgc |
Controller and Reconciliation Loop: The Operator includes a controller, which continuously watches the Kubernetes API for changes to the custom resources it manages. When it detects a new PostgresCluster or an update to an existing one, it runs through a reconciliation loop:
- Read the desired state from the spec of the PostgresCluster resource.
- Compare the desired state with the current state of Pods, Services, StatefulSets, ConfigMaps, Secrets, etc.
- Take actions to move the system from current to desired state. This might involve creating new StatefulSets, updating images, changing replica counts, generating config files, or scheduling backups.
Example: If you change the version from 13.3 to 13.4 in the PostgresCluster spec:
| … spec: version: 13.4 … |
The Operator sees that the running Pods are on version 13.3 and need to be upgraded. It orchestrates a rolling upgrade, perhaps taking one Pod down at a time, running migrations if needed, and verifying that the database remains healthy.
Desired State vs. Actual State: The entire Kubernetes design is based on declarative configuration. You tell Kubernetes what you want, not how to do it. Operators take this a step further by providing a controller that knows the domain-specific "how."
Example: If your PostgresCluster says size: 3 but you currently only have 2 Pods running due to a node failure, the Operator will detect this discrepancy. It will create a new Pod, wait for it to join the cluster, and ensure the database replication is correctly set up.
Operator Lifecycle
Installation of the Operator:
- Operators are usually packaged and distributed as container images plus a set of YAML manifests that include CRDs and RBAC settings.
- You might install an Operator using kubectl apply or using a package manager like OperatorHub or Helm.
Example:
| kubectl apply -f https://example.com/operators/postgres-operator.yaml |
This might install:
- The PostgresCluster CRD.
- A Deployment running the Operator controller.
- RBAC roles that let the Operator manage related resources.
Creating and Managing Instances: Once the Operator is installed, you create instances of the new custom resource to manage your application.
Example:
| kubectl apply -f my-postgres-cluster.yaml |
The Operator's controller sees this new PostgresCluster resource and starts provisioning a StatefulSet for the PostgreSQL pods, a Service for connections, a Secret for passwords, and possibly CronJobs for backups.
Day-2 Operations (Updates, Backups, Scale): After initial setup:
- Upgrade: Change .spec.version to initiate an automatic, graceful rolling upgrade.
- Scale: Increase .spec.size to add more database replicas.
- Change Configuration: Add .spec.tuningParameters (if supported) to apply performance tweaks. The Operator might generate ConfigMaps with these parameters and roll out updates.
- Backup and Restore: If .spec.backup.enabled = true, the Operator sets up a CronJob to take backups. If you delete the cluster due to data corruption and later re-apply the CR with .spec.restoreFrom pointing to a backup, the Operator triggers a restore procedure.
Example of updating configuration:
| apiVersion: database.example.com/v1 kind: PostgresCluster metadata: name: my-postgres spec: version: 13.4 size: 5 storage: size: 50Gi backup: schedule: "0 3 * * *" # now daily at 3 AM |
With a single kubectl apply, the Operator orchestrates all these changes.
More Detailed Examples
MongoDB Operator Example: Suppose you have a MongoDBCluster CR. Initially, you set:
| apiVersion: database.example.com/v1 kind: MongoDBCluster metadata: name: my-mongo spec: replicaSet: members: 3 version: 4.4 |
The Operator:
- Creates a StatefulSet with 3 Pods running MongoDB 4.4.
- Initializes a MongoDB replica set using Kubernetes Pod DNS names.
- Creates a Service for clients to connect.
When you later modify the CR:
| … spec: replicaSet: members: 5 version: 4.4 … |
The Operator:
- Scales the StatefulSet to 5 Pods.
- Runs the necessary rs.add() commands inside MongoDB to add the new members to the replica set.
If you change the version to 4.4.1:
| … spec: version: 4.4.1 … |
The Operator:
- Performs a rolling upgrade, one Pod at a time, ensuring the cluster remains available.
- Once all are upgraded and stable, .status of the CR is updated to reflect success.
Elasticsearch Operator Example (From the Elastic Cloud on Kubernetes Operator):
You create a resource like:
| apiVersion: elasticsearch.k8s.elastic.co/v1 kind: Elasticsearch metadata: name: quickstart spec: version: 7.10.1 nodeSets: – name: default count: 3 config: node.store.allow_mmap: false |
The Operator sets up:
- 3 Elasticsearch Pods, forms a cluster, configures storage and services.
- If you change the count to 5, it adds two more Pods and joins them into the cluster.
- If you change the version to 7.11.0, it orchestrates a safe rolling upgrade.
Kafka Operator Example: A KafkaCluster CR might look like:
| apiVersion: kafka.strimzi.io/v1beta2 kind: Kafka metadata: name: my-cluster spec: kafka: version: 2.6.0 replicas: 3 resources: requests: memory: 2Gi storage: type: persistent-claim size: 100Gi zookeeper: replicas: 3 |
The Strimzi Operator:
- Sets up a 3-node Kafka cluster and a 3-node Zookeeper ensemble.
- If you scale replicas to 5, it provisions extra brokers and updates the cluster configuration.
- If you upgrade version to 2.7.0, it rolls the brokers gracefully, ensuring no downtime in message processing.
Backup and Restore Example: Consider a PostgresBackup CR managed by a
| PostgresOperator: apiVersion: database.example.com/v1 kind: PostgresBackup metadata: name: daily-backup spec: clusterName: my-postgres schedule: "0 1 * * *" |
The Operator:
- Creates a CronJob that runs every day at 1 AM.
- The CronJob might run pg_dump and upload the backup to an S3 bucket. If a disaster occurs, you might create a PostgresRestore CR:
| apiVersion: database.example.com/v1 kind: PostgresRestore metadata: name: restore-my-postgres spec: fromBackup: daily-backup restoreTo: name: my-postgres-restored |
The Operator reads this, creates a new Postgres cluster using the backed-up data, and once done, reports status back in the .status field of the PostgresRestore resource.
Building Operators
- Operator SDK: A tool that simplifies building Operators. It provides scaffolding to quickly create CRDs, Controllers, and Reconcilers in Go, Ansible, or Helm.
- Go-Based Operator: Write reconciliation logic in Go using controller-runtime libraries.
- Helm-Based Operator: Use an existing Helm chart and wrap it in an Operator that reconciles the chart when CRs change.
- Ansible-Based Operator: Use Ansible playbooks to define how to reconcile the desired state.
Example:
| operator-sdk init –domain=database.example.com –owner='YourCompany' operator-sdk create api –group=database –version=v1 –kind=PostgresCluster |
- This scaffolds code and manifests for a PostgresCluster Operator.
- Kubebuilder: Another popular framework that helps you build CRDs and Controllers in Go. Kubebuilder provides project scaffolding, code generators, and testing frameworks to accelerate Operator development.
Best Practices
- Spec and Status:
- Spec: Desired state provided by the user.
- Status: Current observed state updated by the Operator.
- Example: After creating a PostgresCluster, the Operator updates .status.conditions, .status.currentVersion, and .status.availableReplicas so that kubectl get postgresclusters my-postgres -o yaml reveals detailed information about what's actually running.
- OpenAPI Schema and Validation:
- Specify validation rules in your CRD. For instance, ensure size is a positive integer or version matches a known format.
Example:
| schema: openAPIV3Schema: properties: spec: properties: size: type: integer minimum: 1 |
- This ensures invalid specs are rejected before the Operator tries to act on them.
- Finalizers:
- Add finalizers to CRs so that when a resource is deleted, the Operator can perform cleanup actions (like deleting external storage, removing DNS entries, or gracefully shutting down services).
- Example: If you delete the PostgresCluster CR, the Operator first removes data from external storage and once cleanup is done, it removes the finalizer, allowing the resource to disappear.
- Resiliency and Idempotency:
- The reconciliation logic should be idempotent: running it multiple times should not cause unintended side effects.
- Operators should gracefully handle transient errors, retrying when necessary.
- Example: If a backup step fails due to a temporary network error, the Operator should try again rather than crash.
- Security and RBAC:
- Give the Operator only the permissions it needs.
- Use separate Service Accounts and Roles for Operators to reduce blast radius if compromised.
Operator Capability Levels
Operators can evolve in sophistication:
- Basic Install: Can install and run the application.
- Seamless Upgrades: Handles application upgrades automatically.
- Full Lifecycle: Manages configuration changes, scale operations, and backups.
- Deep Insights: Provides health checks, metrics, logging, and performance tuning suggestions.
- Autopilot: Continuously monitors the application and autonomously adjusts configuration, scale, or triggers failover without human intervention.
Example:
A Level-5 Operator for PostgreSQL might automatically detect slow queries and adjust index configurations or memory settings based on observed workload patterns.
Conclusion
A Kubernetes Operator is effectively a Kubernetes-native automation engine for complex applications. By extending the Kubernetes API with CRDs and encoding domain-specific lifecycle management into a controller, Operators make running complex, stateful, and scalable applications more predictable and less manual.
In summary:
- The Operator pattern shifts day-2 operations into the cluster's control plane.
- Users declare intent through custom resources.
- The Operator's reconciliation loop ensures actual state always aligns with desired state.
- From installation and upgrades to scaling and backups, Operators provide a single declarative interface for all lifecycle actions.
- Tools like Operator SDK and Kubebuilder streamline building these Operators, allowing developers and SREs to encode operational best practices as code.
This blend of declarative configuration, continuous reconciliation, and domain-specific intelligence is what makes Operators such a powerful and popular approach to manage complex applications on Kubernetes.