A practical demonstration of a Kubernetes operator that manages a custom WebApp resource. This example showcases the core concepts of Custom Resource Definitions (CRDs) and the reconciliation loop pattern.
A Kubernetes operator is a software extension that uses Custom Resources to manage applications and their components. Operators follow Kubernetes principles, notably the control loop pattern, to automate operational tasks that would otherwise require human intervention.
Key Concept: Operators codify human operational knowledge into software that automatically manages complex applications.
A CRD extends the Kubernetes API to include custom resource types. In this example, we define a WebApp resource.
File: crd/webapp-crd.yaml
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: webapps.example.com
spec:
group: example.com
names:
kind: WebApp
plural: webappsWhat it does:
- Defines a new resource type
WebAppin theexample.comAPI group - Specifies the schema (what fields are allowed)
- Enables validation (e.g., replicas must be 1-10)
- Defines status subresource for tracking application state
Once the CRD is installed, you can create instances of the custom resource.
File: examples/nginx-webapp.yaml
apiVersion: example.com/v1
kind: WebApp
metadata:
name: nginx-app
spec:
image: nginx:1.25
replicas: 3
port: 80This is a declarative specification: "I want an nginx web app with 3 replicas."
The operator continuously watches for changes to WebApp resources and reconciles the actual state with the desired state.
File: main.go - See the Reconcile() function
┌─────────────────────────────────────────────────┐
│ User creates/updates WebApp CR │
└─────────────────┬───────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────┐
│ 1. Operator watches for WebApp events │
└─────────────────┬───────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────┐
│ 2. Reconcile() function is triggered │
│ - Fetch the WebApp resource │
└─────────────────┬───────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────┐
│ 3. Check if Deployment exists │
│ - If not, create it │
│ - If yes, check if it matches spec │
└─────────────────┬───────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────┐
│ 4. Check if Service exists │
│ - If not, create it │
└─────────────────┬───────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────┐
│ 5. Update WebApp status │
│ - Available replicas count │
│ - Ready condition │
└─────────────────┬───────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────┐
│ 6. Requeue after 30s │
│ (Loop continues) │
└─────────────────────────────────────────────────┘
- Level-Triggered, Not Edge-Triggered: The operator doesn't just react to changes; it continuously ensures the actual state matches desired state
- Idempotent: Running reconciliation multiple times has the same effect as running it once
- Eventually Consistent: The system converges to the desired state over time
- Self-Healing: If someone manually deletes a Deployment, the operator recreates it
The heart of any operator is the Reconcile() function. Here's what happens in our implementation:
func (r *WebAppReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
// STEP 1: Fetch the WebApp resource
webapp := &WebApp{}
err := r.Get(ctx, req.NamespacedName, webapp)
// STEP 2: Check if Deployment exists, create if not
deployment := &appsv1.Deployment{}
err = r.Get(ctx, types.NamespacedName{Name: webapp.Name, Namespace: webapp.Namespace}, deployment)
if errors.IsNotFound(err) {
// Create deployment
dep := r.deploymentForWebApp(webapp)
err = r.Create(ctx, dep)
}
// STEP 3: Reconcile drift - ensure Deployment matches spec
if *deployment.Spec.Replicas != webapp.Spec.Replicas {
deployment.Spec.Replicas = &webapp.Spec.Replicas
err = r.Update(ctx, deployment)
}
// STEP 4: Create Service if needed
// ... similar logic
// STEP 5: Update status
webapp.Status.AvailableReplicas = deployment.Status.AvailableReplicas
err = r.Status().Update(ctx, webapp)
// STEP 6: Requeue to check again later
return ctrl.Result{RequeueAfter: 30 * time.Second}, nil
}Notice the SetControllerReference() calls:
controllerutil.SetControllerReference(webapp, dep, r.Scheme)This creates an owner reference, meaning:
- The Deployment is "owned" by the WebApp
- If the WebApp is deleted, Kubernetes automatically deletes the Deployment (garbage collection)
- This establishes the parent-child relationship
func (r *WebAppReconciler) SetupWithManager(mgr ctrl.Manager) error {
return ctrl.NewControllerManagedBy(mgr).
For(&WebApp{}). // Primary resource to watch
Owns(&appsv1.Deployment{}). // Trigger reconciliation when owned Deployments change
Owns(&corev1.Service{}). // Trigger reconciliation when owned Services change
Complete(r)
}This tells the operator to trigger reconciliation when:
- A WebApp resource changes
- A Deployment owned by a WebApp changes
- A Service owned by a WebApp changes
operator/
├── crd/
│ └── webapp-crd.yaml # Custom Resource Definition
├── examples/
│ ├── nginx-webapp.yaml # Example CR: nginx application
│ └── simple-webapp.yaml # Example CR: hello-app
├── main.go # Operator implementation
├── go.mod # Go dependencies
├── Dockerfile # Container image for operator
├── Makefile # Convenience commands
└── README.md # This file
- Kubernetes cluster (minikube, kind, or any cluster)
- kubectl configured
- Go 1.21+ (for local development)
kubectl apply -f crd/webapp-crd.yamlVerify:
kubectl get crd webapps.example.comgo mod download
go run main.goThe operator will connect to your Kubernetes cluster and start watching for WebApp resources.
In another terminal:
kubectl apply -f examples/nginx-webapp.yaml# Watch the WebApp status
kubectl get webapps -w
# Check what the operator created
kubectl get deployments
kubectl get services
kubectl get podsTry these experiments to see reconciliation in action:
# 1. Scale the replicas
kubectl patch webapp nginx-app -p '{"spec":{"replicas":5}}' --type=merge
# 2. Manually delete the deployment (operator will recreate it)
kubectl delete deployment nginx-app
# 3. Check the status
kubectl describe webapp nginx-app- Automation: Codify operational knowledge (backups, upgrades, scaling)
- Consistency: Same operational practices across environments
- Kubernetes-Native: Leverage existing Kubernetes primitives
- Declarative: Users specify "what" they want, operator figures out "how"
- Prometheus Operator: Manages Prometheus monitoring deployments
- Strimzi: Manages Apache Kafka clusters
- MySQL Operator: Handles database provisioning, backups, failover
- Cert-Manager: Automates TLS certificate management
Build an operator when:
- Application requires complex operational knowledge
- Need to automate Day 2 operations (upgrades, backups, recovery)
- Managing stateful applications
- Need to extend Kubernetes with domain-specific abstractions
- Operator SDK: Full framework with scaffolding tools (Go, Ansible, Helm)
- KubeBuilder: Go-focused framework, used by Operator SDK under the hood
- controller-runtime: Lower-level library (what we used in this example)
- Basic Install: Automated application provisioning
- Seamless Upgrades: Patch and minor version upgrades
- Full Lifecycle: App lifecycle, storage, networking
- Deep Insights: Metrics, alerts, log processing
- Auto Pilot: Horizontal/vertical scaling, auto-config tuning, abnormality detection
Our example is at level 1-2.
Operators can include validating and mutating webhooks:
- Validating: Reject invalid WebApp configurations before they're stored
- Mutating: Set default values automatically
When running multiple operator replicas:
- Only one actively reconciles (the leader)
- Others wait to take over if leader fails
- Ensures only one operator makes changes at a time
Allow operators to perform cleanup before resource deletion:
if webapp.ObjectMeta.DeletionTimestamp.IsZero() {
// Add finalizer
controllerutil.AddFinalizer(webapp, "example.com/finalizer")
} else {
// Resource being deleted, perform cleanup
if controllerutil.ContainsFinalizer(webapp, "example.com/finalizer") {
// Cleanup logic here (e.g., delete external resources)
controllerutil.RemoveFinalizer(webapp, "example.com/finalizer")
}
}We use Kubernetes-standard conditions to report state:
status:
conditions:
- type: Ready
status: "True"
reason: DeploymentReady
message: "Deployment has 3/3 replicas available"
lastTransitionTime: "2024-01-18T10:30:00Z"Q: What's the difference between an operator and a controller? A: All operators are controllers, but not all controllers are operators. Operators are controllers that use CRDs to manage application-specific resources with domain knowledge.
Q: What happens if the operator crashes? A: When it restarts, it will reconcile all existing WebApp resources and bring them to the desired state. The reconciliation loop is level-triggered, not edge-triggered.
Q: How do you handle concurrent updates? A: Kubernetes uses optimistic locking with resource versions. If two updates conflict, one will fail with a conflict error and should retry.
Q: How do you test operators? A: Use envtest (runs a local control plane), unit tests for reconciliation logic, and end-to-end tests in real clusters.
Q: What about performance with many resources? A: Use caching, selective watching (predicates), and rate limiting. controller-runtime includes a built-in cache.
Q: How do you handle upgrades of the CRD itself? A: CRDs support versioning. You can have multiple versions (v1alpha1, v1beta1, v1) simultaneously. Use conversion webhooks to translate between versions. The stored version is what's persisted in etcd.
# Delete example resources
kubectl delete -f examples/
# Delete CRD (this also deletes all WebApp resources)
kubectl delete -f crd/webapp-crd.yaml- Operator SDK: https://sdk.operatorframework.io/
- KubeBuilder Book: https://book.kubebuilder.io/
- controller-runtime: https://github.com/kubernetes-sigs/controller-runtime
- Operator Hub: https://operatorhub.io/ (browse existing operators)
This example demonstrates:
- ✅ Custom Resource Definition (CRD) to extend Kubernetes
- ✅ Reconciliation loop pattern for continuous state management
- ✅ Creating and managing child resources (Deployment, Service)
- ✅ Status reporting and conditions
- ✅ Owner references for garbage collection
- ✅ Real-world operator pattern using controller-runtime
The operator pattern is powerful because it allows you to encode operational expertise into software that runs continuously, ensuring your applications stay healthy and properly configured without manual intervention.