---
title: "Kubernetes Operators Tutorial"
source: Panaversity Learn Agentic AI
source_url: https://github.com/panaversity/learn-agentic-ai
licence: Apache-2.0
domain: agentic-ai
subdomain: panaversity-agentic
date_added: 2026-04-25
---

# Kubernetes Operators Tutorial

Below is a detailed tutorial on Kubernetes Operators, including their relationship with Helm, whether they can be used together, and examples using Python where applicable. This tutorial assumes you have a basic understanding of Kubernetes concepts like pods, deployments, and custom resources.

---

## What Are Kubernetes Operators?

Kubernetes Operators are a method of packaging, deploying, and managing a Kubernetes application. They extend the Kubernetes API to automate complex, application-specific tasks that would otherwise require human intervention. Operators leverage **Custom Resource Definitions (CRDs)** and **Controllers** to manage the lifecycle of applications or resources in a Kubernetes cluster.

An Operator essentially encodes operational knowledge into software, allowing Kubernetes to handle tasks like scaling, upgrades, backups, and failure recovery for specific applications (e.g., databases like PostgreSQL or message queues like Kafka). They act like a "human operator" for your application, responding to events in the cluster.

### Example Use Cases
- Managing databases (e.g., PostgreSQL, Redis).
- Automating backups or upgrades for stateful apps.
- Handling complex deployments like Kafka or Elasticsearch.

### Why Use Operators?
- **Automation**: Operators handle repetitive tasks (e.g., scaling, failover) that would otherwise require manual intervention.
- **Stateful Apps**: They’re ideal for managing applications with persistent state, which Kubernetes’ built-in controllers (e.g., Deployment) struggle with.
- **Extensibility**: Operators let you tailor Kubernetes to your specific needs.

### Core Components of an Operator
1. **Custom Resource (CR)**: A custom Kubernetes resource defined via a CRD (e.g., `PostgresCluster` for a PostgreSQL Operator).
2. **Controller**: A control loop that watches the state of the CR and reconciles it with the desired state by performing actions (e.g., creating pods, updating configurations).
3. **Operational Logic**: Application-specific logic written in a programming language like Go, Python, or others.

---

## Using a Prebuilt Kubernetes Operator

Prebuilt Operators are ready-to-use solutions created by the community or vendors to manage specific applications (e.g., databases, monitoring systems) on Kubernetes. Instead of writing your own Operator logic, you deploy an existing one and interact with it via its Custom Resources (CRs). Here’s how to do it step-by-step.

### Prerequisites
- A Kubernetes cluster (e.g., Minikube, GKE, EKS).
- `kubectl` installed and configured.
- (Optional) Helm installed if the Operator is distributed as a Helm Chart.

For this example, we’ll use the **Prometheus Operator**, a popular prebuilt Operator for managing Prometheus monitoring instances. It’s part of the CNCF ecosystem and widely adopted.

---

### Step 1: Understand the Prebuilt Operator
Before using a prebuilt Operator, review its documentation to understand:
- What it manages (e.g., Prometheus instances for monitoring).
- The Custom Resources it provides (e.g., `Prometheus`, `ServiceMonitor`).
- Installation options (e.g., Helm, raw YAML, Operator Lifecycle Manager).

For the Prometheus Operator:
- **Purpose**: Manages Prometheus, Alertmanager, and related monitoring components.
- **CRs**: `Prometheus`, `ServiceMonitor`, `Alertmanager`, etc.
- **Source**: Available via GitHub (https://github.com/prometheus-operator/prometheus-operator) or Helm.

---

### Step 2: Install the Operator
Prebuilt Operators can typically be installed in one of three ways:
1. **Raw YAML**: Apply manifests directly.
2. **Helm Chart**: Use Helm for templated deployment.
3. **Operator Lifecycle Manager (OLM)**: Use OLM on clusters like OpenShift.

#### Option 1: Install with Raw YAML
The Prometheus Operator provides a bundled YAML file for easy installation.
1. Download the latest release bundle:
   ```bash
   curl -sL https://github.com/prometheus-operator/prometheus-operator/releases/latest/download/bundle.yaml -o prometheus-operator.yaml
   ```
2. Apply it to your cluster:
   ```bash
   kubectl apply -f prometheus-operator.yaml
   ```
   This deploys the Operator’s CRDs, RBAC rules, and controller pod.

#### Option 2: Install with Helm
Many Operators, including Prometheus, are available as Helm Charts.
1. Add the Helm repository:
   ```bash
   helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
   helm repo update
   ```
2. Install the Operator:
   ```bash
   helm install prometheus-operator prometheus-community/kube-prometheus-stack --namespace monitoring --create-namespace
   ```
   This installs the Operator along with default Prometheus and Grafana instances. The `kube-prometheus-stack` chart bundles the Operator with additional resources for a full monitoring stack.

#### Verify Installation
Check that the Operator pod is running:
```bash
kubectl get pods -n monitoring
```
Look for a pod like `prometheus-operator-xxxxx`.

---

### Step 3: Use the Operator’s Custom Resources
Once the Operator is running, you interact with it by creating instances of its Custom Resources. For the Prometheus Operator, you might create a `Prometheus` CR to deploy a Prometheus instance.

#### Example: Deploy a Prometheus Instance
Create a file named `prometheus-instance.yaml`:
```yaml
apiVersion: monitoring.coreos.com/v1
kind: Prometheus
metadata:
  name: my-prometheus
  namespace: monitoring
spec:
  replicas: 2
  serviceMonitorSelector:
    matchLabels:
      app: my-app
  resources:
    requests:
      memory: "400Mi"
```
Apply it:
```bash
kubectl apply -f prometheus-instance.yaml
```

The Operator will:
- Detect the new `Prometheus` CR.
- Create the necessary pods, services, and configurations.
- Manage scaling and updates based on the `spec`.

#### Example: Monitor an Application
To monitor an application, define a `ServiceMonitor` CR. Assume you have a service labeled `app: my-app`:
```yaml
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
  name: my-app-monitor
  namespace: monitoring
  labels:
    app: my-app
spec:
  selector:
    matchLabels:
      app: my-app
  endpoints:
  - port: http
    path: /metrics
```
Apply it:
```bash
kubectl apply -f my-app-monitor.yaml
```
The Prometheus Operator will configure the Prometheus instance to scrape metrics from this service.

---

### Step 4: Validate and Interact
- Check the Prometheus pods:
  ```bash
  kubectl get pods -n monitoring
  ```
- Access the Prometheus UI (if exposed via a service or ingress):
  ```bash
  kubectl port-forward -n monitoring svc/my-prometheus 9090:9090
  ```
  Open `http://localhost:9090` in your browser.

The Operator handles scaling, updates, and recovery automatically based on the CRs you define.

---

### Step 5: Customize and Extend (Optional)
Prebuilt Operators often allow customization via their CR specs or Helm values. For example, with Helm:
1. Create a `values.yaml` file:
   ```yaml
   prometheus:
     prometheusSpec:
       replicas: 3
       resources:
         requests:
           memory: "600Mi"
   ```
2. Upgrade the installation:
   ```bash
   helm upgrade prometheus-operator prometheus-community/kube-prometheus-stack -n monitoring -f values.yaml
   ```

---

## Using Prebuilt Operators with Helm
As shown with the Prometheus Operator, Helm is a common way to deploy prebuilt Operators. Here’s why they work well together:
- **Simplified Installation**: Helm Charts package the Operator’s CRDs, RBAC, and deployment manifests into a single command.
- **Customization**: Helm’s `values.yaml` lets you tweak the Operator’s configuration without editing raw YAML.
- **Versioning**: Helm supports upgrading or rolling back Operator versions.

For example, the `kube-prometheus-stack` Helm Chart not only installs the Operator but also deploys a full monitoring stack (Prometheus, Grafana, Alertmanager), which you can customize via Helm values.

### Can You Use Them Together?
Yes, absolutely. Helm deploys the Operator, and then you use the Operator’s CRs to manage application instances. This hybrid approach leverages Helm’s deployment strengths and the Operator’s lifecycle management.

---

## Other Popular Prebuilt Operators
Here are a few other prebuilt Operators you might explore:
1. **PostgreSQL Operator by Crunchy Data**:
   - Install: Available via Helm or YAML.
   - CR: `PostgresCluster`.
   - Use: Manages PostgreSQL clusters with replication and backups.
2. **Rook (Ceph Operator)**:
   - Install: Helm or YAML.
   - CR: `CephCluster`.
   - Use: Manages distributed storage on Kubernetes.
3. **Kafka Operator by Strimzi**:
   - Install: Helm or YAML.
   - CR: `Kafka`.
   - Use: Deploys and manages Apache Kafka clusters.

To use any of these, follow a similar process: install the Operator, then create its CRs based on its documentation.

---

## Troubleshooting Tips
- **Operator Not Responding**: Check pod logs (`kubectl logs <operator-pod> -n <namespace>`).
- **CR Not Working**: Ensure the CRD is installed (`kubectl get crd`) and the spec matches the Operator’s expectations.
- **Helm Issues**: Use `helm get manifest <release-name>` to debug generated manifests.

---

## Conclusion
Using a prebuilt Operator involves installing it (often with Helm or YAML) and then defining its Custom Resources to manage your application. The Prometheus Operator example demonstrates how this works in practice, and Helm enhances the process by simplifying deployment and customization. Whether you’re managing monitoring, databases, or storage, prebuilt Operators save time by encoding expert operational knowledge into reusable software.


---

## How Operators Work

1. **Define a CRD**: You create a CRD to extend the Kubernetes API with a new resource type (e.g., `MyApp`).
2. **Implement a Controller**: The controller watches for changes to instances of the CR (e.g., `myapp-1`) and ensures the cluster state matches the desired state defined in the CR.
3. **Deploy the Operator**: The Operator runs as a pod in the cluster, continuously monitoring and managing resources.

For example, a PostgreSQL Operator might:
- Watch for a `PostgresCluster` CR.
- Create pods for the database instances.
- Handle replication, backups, and scaling based on the CR’s specification.

---

## Building a Simple Operator with Python

Let’s create a basic Operator using Python and the `kopf` framework (Kubernetes Operator Pythonic Framework). This Operator will manage a custom resource called `SimpleApp`.

### Prerequisites
- A Kubernetes cluster (e.g., Minikube or a cloud provider).
- Python 3.8+ installed.
- `kubectl` configured to interact with your cluster.
- Install dependencies:
  ```bash
  pip install kopf kubernetes
  ```

### Step 1: Define the Custom Resource Definition (CRD)
Create a file named `crd.yaml`:
```yaml
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
  name: simpleapps.example.com
spec:
  group: example.com
  names:
    kind: SimpleApp
    listKind: SimpleAppList
    plural: simpleapps
    singular: simpleapp
  scope: Namespaced
  versions:
    - name: v1
      served: true
      storage: true
      schema:
        openAPIV3Schema:
          type: object
          properties:
            spec:
              type: object
              properties:
                replicas:
                  type: integer
                image:
                  type: string
```

Apply it to your cluster:
```bash
kubectl apply -f crd.yaml
```

### Step 2: Write the Operator Logic in Python
Create a file named `operator.py`:
```python
import kopf
import kubernetes.client
from kubernetes.client.rest import ApiException

# Handler for when a SimpleApp resource is created or updated
@kopf.on.create('example.com', 'v1', 'simpleapps')
@kopf.on.update('example.com', 'v1', 'simpleapps')
def create_or_update_fn(spec, name, namespace, **kwargs):
    api = kubernetes.client.AppsV1Api()
    
    # Extract spec fields
    replicas = spec.get('replicas', 1)
    image = spec.get('image', 'nginx:latest')

    # Define the Deployment object
    deployment = {
        'apiVersion': 'apps/v1',
        'kind': 'Deployment',
        'metadata': {
            'name': name,
            'namespace': namespace,
        },
        'spec': {
            'replicas': replicas,
            'selector': {'matchLabels': {'app': name}},
            'template': {
                'metadata': {'labels': {'app': name}},
                'spec': {'containers': [{'name': name, 'image': image}]}
            }
        }
    }

    try:
        # Check if the deployment exists
        api.read_namespaced_deployment(name, namespace)
        # Update if it exists
        api.patch_namespaced_deployment(name, namespace, deployment)
        return {'message': f'Deployment {name} updated'}
    except ApiException as e:
        if e.status == 404:
            # Create if it doesn’t exist
            api.create_namespaced_deployment(namespace, deployment)
            return {'message': f'Deployment {name} created'}
        raise

# Handler for when a SimpleApp resource is deleted
@kopf.on.delete('example.com', 'v1', 'simpleapps')
def delete_fn(name, namespace, **kwargs):
    api = kubernetes.client.AppsV1Api()
    try:
        api.delete_namespaced_deployment(name, namespace)
        return {'message': f'Deployment {name} deleted'}
    except ApiException as e:
        if e.status != 404:
            raise

if __name__ == '__main__':
    kopf.run()
```

### Step 3: Deploy the Operator
1. Package the Operator as a Docker image:
   ```Dockerfile
   FROM python:3.9-slim
   WORKDIR /app
   COPY requirements.txt .
   RUN pip install -r requirements.txt
   COPY operator.py .
   CMD ["kopf", "run", "operator.py", "--verbose"]
   ```
   ```bash
   # requirements.txt
   kopf==1.36.2
   kubernetes==29.0.0
   ```

2. Build and push the image:
   ```bash
   docker build -t my-simple-operator:latest .
   docker push my-simple-operator:latest
   ```

3. Deploy the Operator to Kubernetes:
   ```yaml
   apiVersion: apps/v1
   kind: Deployment
   metadata:
     name: simple-operator
   spec:
     replicas: 1
     selector:
       matchLabels:
         app: simple-operator
     template:
       metadata:
         labels:
           app: simple-operator
       spec:
         containers:
         - name: operator
           image: my-simple-operator:latest
   ```
   Apply it:
   ```bash
   kubectl apply -f operator-deployment.yaml
   ```

### Step 4: Test the Operator
Create a `SimpleApp` resource:
```yaml
apiVersion: example.com/v1
kind: SimpleApp
metadata:
  name: my-app
  namespace: default
spec:
  replicas: 2
  image: nginx:latest
```
Apply it:
```bash
kubectl apply -f simpleapp.yaml
```

Check the deployment:
```bash
kubectl get deployments
```
You should see a deployment named `my-app` with 2 replicas running `nginx:latest`.

---

## Relationship Between Operators and Helm

### What Is Helm?
Helm is a package manager for Kubernetes that simplifies the deployment of applications using **Charts**. A Helm Chart is a collection of pre-configured Kubernetes resource manifests (e.g., deployments, services) bundled with templating and values files for customization.

### Operators vs. Helm
- **Helm**: Focuses on templating and deploying static manifests. It’s great for initial deployment but doesn’t manage the application lifecycle post-deployment.
- **Operators**: Actively manage applications by watching CRs and responding to changes, failures, or scaling needs.

### Can They Be Used Together?
Yes, Helm and Operators can complement each other:
1. **Deploying Operators with Helm**: Many Operators are distributed as Helm Charts. For example, you can use Helm to install the Operator’s CRD, controller deployment, and RBAC rules, then use the Operator’s CRs to manage the application.
2. **Hybrid Approach**: Use Helm to deploy static components (e.g., a web frontend) and an Operator to manage dynamic components (e.g., a database).

#### Example: Deploying the Above Operator with Helm
1. Create a Helm Chart structure:
   ```bash
   helm create simple-operator-chart
   ```
2. Replace `templates/deployment.yaml` with the Operator deployment:
   ```yaml
   apiVersion: apps/v1
   kind: Deployment
   metadata:
     name: {{ .Release.Name }}-operator
   spec:
     replicas: 1
     selector:
       matchLabels:
         app: {{ .Release.Name }}-operator
     template:
       metadata:
         labels:
           app: {{ .Release.Name }}-operator
       spec:
         containers:
         - name: operator
           image: {{ .Values.image.repository }}:{{ .Values.image.tag }}
   ```
3. Update `values.yaml`:
   ```yaml
   image:
     repository: my-simple-operator
     tag: latest
   ```
4. Install the Operator:
   ```bash
   helm install my-operator ./simple-operator-chart
   ```

Now, Helm deploys the Operator, and the Operator manages `SimpleApp` resources.

---

## When to Use Operators, Helm, or Both?
- **Helm Alone**: For simple, static applications with minimal lifecycle management (e.g., a stateless web app).
- **Operators Alone**: For complex, stateful applications requiring active management (e.g., databases, queues).
- **Helm + Operators**: When you want to simplify Operator deployment and manage both static and dynamic components.

---

## Conclusion

Kubernetes Operators extend the platform’s capabilities by automating application-specific tasks using CRDs and controllers. Python, with frameworks like `kopf`, makes it accessible to build Operators, as shown in the example. Helm, while not a replacement for Operators, can streamline their deployment and work alongside them in a hybrid setup. By combining the two, you can leverage Helm’s packaging power and Operators’ lifecycle management for robust Kubernetes workflows.

---

## Kubernetes Operators and Dapr (Distributed Application Runtime)

Kubernetes Operators and Dapr (Distributed Application Runtime) are connected in the Kubernetes ecosystem as tools that can complement each other to manage and enhance distributed applications. While they serve distinct purposes—Operators focus on automating application lifecycle management, and Dapr provides a runtime for distributed system capabilities—they can intersect in powerful ways when used together. Let’s explore their relationship in detail.

---

### What Are Kubernetes Operators?

A Kubernetes **Operator** extends Kubernetes by combining **Custom Resource Definitions (CRDs)** with custom controllers to automate the management of complex applications. Operators encode operational logic (e.g., scaling, upgrades, backups) and react to cluster events to maintain an application’s desired state.

---

### What is Dapr?

Dapr is a runtime that simplifies building distributed applications by providing **building blocks** (e.g., state management, pub/sub, service invocation) as sidecars. It abstracts distributed system complexities and integrates with Kubernetes via its control plane (e.g., `dapr-operator`, `dapr-sidecar-injector`), which manages Dapr’s runtime services.

---

### How Are Operators and Dapr Connected?

Operators and Dapr intersect in their ability to manage and enhance applications on Kubernetes, often in complementary or overlapping ways. Here’s how they connect:

#### 1. **Dapr Itself Uses an Operator**
- Dapr’s control plane includes a component called `dapr-operator`, which is a Kubernetes Operator. This Operator manages Dapr-specific custom resources, such as:
  - **`Component`**: Defines Dapr building blocks (e.g., a Redis state store or Kafka pub/sub).
  - **`Configuration`**: Specifies runtime settings (e.g., tracing, mTLS).
- The `dapr-operator` watches these CRs and configures the Dapr runtime accordingly. For example:
  ```yaml
  apiVersion: dapr.io/v1alpha1
  kind: Component
  metadata:
    name: statestore
  spec:
    type: state.redis
    version: v1
    metadata:
    - name: redisHost
      value: redis:6379
  ```
  The `dapr-operator` ensures this Redis state store is available to Dapr sidecars.

- **Connection**: Dapr leverages the Operator pattern internally to manage its own infrastructure, making it a practical example of an Operator in action.

#### 2. **Operators Can Deploy Dapr-Enabled Apps**
- A custom Operator for your application could integrate Dapr by:
  - Deploying application pods with Dapr sidecar annotations (e.g., `dapr.io/enabled: "true"`).
  - Managing Dapr `Component` resources as part of the application’s lifecycle.
- Example: An Operator for a `MyApp` CR could create a Deployment with Dapr annotations and a corresponding `Component` for state management:
  ```yaml
  apiVersion: myapp.example.com/v1
  kind: MyApp
  metadata:
    name: my-app
  spec:
    replicas: 2
    dapr:
      appId: my-app
      stateStore: redis
  ```
  The Operator’s controller would reconcile this by deploying the app with Dapr enabled and configuring a Redis `Component`.

#### 3. **Dapr Can Enhance Operator-Managed Apps**
- Operators typically manage complex, stateful apps (e.g., databases, message queues). Dapr’s building blocks can simplify these apps’ distributed features:
  - **State Management**: Instead of an Operator implementing custom state logic, it can delegate to Dapr’s state store.
  - **Pub/Sub**: An Operator-managed app can use Dapr for messaging without reinventing the wheel.
- Example: A PostgreSQL Operator could use Dapr’s pub/sub to notify other services of database events, offloading messaging logic to Dapr.

#### 4. **Hybrid Approach: Operator + Dapr Control Plane**
- You might deploy Dapr’s control plane (including its `dapr-operator`) and then use a custom Operator to manage your app, leveraging Dapr’s runtime. For instance:
  - Deploy Dapr via Helm or manifests.
  - Create a custom Operator that deploys your app and configures Dapr `Component` resources dynamically based on the app’s needs.

#### 5. **Overlap in Automation Goals**
- Both Operators and Dapr aim to automate and simplify. Operators focus on Kubernetes-level resource management, while Dapr focuses on application-level distributed system patterns. They can overlap when an Operator manages Dapr resources or when Dapr’s `dapr-operator` handles tasks an app-specific Operator might otherwise cover.

---

### Practical Example

Let’s see how an Operator and Dapr might work together.

#### Scenario: Managing a Stateful App with Dapr
1. **Deploy Dapr**:
   ```bash
   helm install dapr dapr/dapr --namespace dapr-system
   ```

2. **Define a Custom Resource**:
   Create a CRD for a `Guestbook` app that uses Dapr for state:
   ```yaml
   apiVersion: app.example.com/v1alpha1
   kind: Guestbook
   metadata:
     name: guestbook-sample
   spec:
     replicas: 2
     daprAppId: guestbook
   ```

3. **Operator Logic** (Python with Kopf, simplified):
   ```python
   import kopf
   from kubernetes import client, config

   apps_v1 = client.AppsV1Api()

   @kopf.on.create('app.example.com', 'v1alpha1', 'guestbooks')
   def create_guestbook(spec, name, namespace, **kwargs):
       # Define Deployment with Dapr annotations
       deployment = {
           "apiVersion": "apps/v1",
           "kind": "Deployment",
           "metadata": {"name": f"{name}-deployment", "namespace": namespace},
           "spec": {
               "replicas": spec.get('replicas', 1),
               "selector": {"matchLabels": {"app": name}},
               "template": {
                   "metadata": {
                       "labels": {"app": name},
                       "annotations": {
                           "dapr.io/enabled": "true",
                           "dapr.io/app-id": spec.get('daprAppId', name)
                       }
                   },
                   "spec": {
                       "containers": [{"name": "guestbook", "image": "nginx:latest"}]
                   }
               }
           }
       }
       apps_v1.create_namespaced_deployment(namespace, deployment)

       # Define a Dapr Component for state
       dapr_component = {
           "apiVersion": "dapr.io/v1alpha1",
           "kind": "Component",
           "metadata": {"name": f"{name}-state", "namespace": namespace},
           "spec": {
               "type": "state.in-memory",
               "version": "v1",
               "metadata": []
           }
       }
       client.CustomObjectsApi().create_namespaced_custom_object(
           group="dapr.io", version="v1alpha1", namespace=namespace,
           plural="components", body=dapr_component
       )
   ```

4. **Apply and Verify**:
   ```bash
   kubectl apply -f guestbook-sample.yaml
   kubectl get pods
   ```
   The Operator deploys a `guestbook-sample-deployment` with a Dapr sidecar and an in-memory state store managed by Dapr.

- **Result**: The Operator handles the app’s lifecycle, while Dapr provides distributed features (e.g., state persistence).

---

### Key Points of Connection

- **Dapr’s Operator**: Dapr uses an Operator (`dapr-operator`) to manage its own resources, showing Operators as a core part of Dapr’s architecture.
- **Enhancement**: Dapr can offload distributed system tasks from custom Operators, simplifying their logic.
- **Deployment**: A custom Operator can integrate Dapr into an app’s workflow by managing sidecar annotations and components.
- **Separation of Concerns**: Operators focus on Kubernetes resource orchestration; Dapr focuses on runtime application logic.

---

### Are They Competitors?

Not directly. Operators and Dapr address different layers:
- **Operators**: Manage Kubernetes resources and application lifecycle.
- **Dapr**: Provides runtime abstractions for distributed systems.

However, there’s potential overlap if an Operator reimplements Dapr-like features (e.g., state management). Using Dapr can reduce this redundancy, letting Operators focus on orchestration while Dapr handles runtime concerns.

---

### Conclusion

Operators and Dapr connect through their shared goal of simplifying Kubernetes application management. Dapr’s own `dapr-operator` is an Operator, and custom Operators can leverage Dapr to enhance apps with distributed capabilities. Together, they enable a powerful workflow: Operators for deployment and lifecycle automation, Dapr for runtime distributed system features—making them a natural fit for complex, stateful applications on Kubernetes.
