The Complete Guide to Kubernetes Operators in 2026: Building Custom Controllers
Kubernetes Operators became the standard for extending Kubernetes in 2025-2026, allowing you to encode domain knowledge into the control plane. Instead of running manual scripts for complex deployments, Operators watch custom resources and take action.
Here's the practical guide.
What is an Operator
Kubernetes: Manages pods, services, deployments
Operator: Manages custom resources (your app's specific state)
Example: Prometheus Operator watches Prometheus CRD and automatically configures monitoring. You don't manually create the monitoring pods — the Operator does it.
Custom Resource Definition
# crd.yaml
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: myapps.example.com
spec:
group: example.com
names:
kind: MyApp
plural: myapps
scope: Namespaced
versions:
- name: v1
served: true
storage: true
schema:
openAPIV3Schema:
type: object
properties:
spec:
type: object
properties:
replicas:
type: integer
image:
type: string
Simple Operator in Go
package main
import (
"context"
"fmt"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/runtime"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
)
type MyAppReconciler struct {
client.Client
Scheme *runtime.Scheme
}
func (r *MyAppReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
fmt.Printf("Reconciling MyApp: %s\n", req.Name)
// Fetch the MyApp resource
// Create/Update Deployment based on spec
// Return ctrl.Result{} to requeue
return ctrl.Result{}, nil
}
func (r *MyAppReconciler) SetupWithManager(mgr *ctrl.Manager) error {
return ctrl.NewControllerManagedBy(mgr).
For(&MyApp{}). // Watch MyApp resources
Owns(&appsv1.Deployment{}). // Also watch Deployments we create
Complete(r)
}
controller-runtime Setup
func main() {
scheme := runtime.NewScheme()
appsv1.AddToScheme(scheme)
corev1.AddToScheme(scheme)
mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{
Scheme: scheme,
})
if err != nil {
panic(err)
}
if err := (&MyAppReconciler{
Client: mgr.GetClient(),
Scheme: mgr.GetScheme(),
}).SetupWithManager(mgr); err != nil {
panic(err)
}
mgr.Start(context.Background())
}
RBAC for Operators
# role.yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: myapp-operator
rules:
- apiGroups: ["example.com"]
resources: ["myapps"]
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
- apiGroups: ["apps"]
resources: ["deployments"]
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
Status Updates
func (r *MyAppReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
myapp := &examplecomv1.MyApp{}
if err := r.Get(ctx, req.NamespacedName, myapp); err != nil {
return ctrl.Result{}, client.IgnoreNotFound(err)
}
// Update status
myapp.Status.Phase = "Running"
myapp.Status.Replicas = *deployment.Spec.Replicas
if err := r.Status().Update(ctx, myapp); err != nil {
return ctrl.Result{}, err
}
return ctrl.Result{}, nil
}
This article contains affiliate links. If you sign up through the links above, I may earn a commission at no additional cost to you.
Ready to Build Your Online Business?
Get started with Systeme.io for free — All-in-one platform for building your online business with AI tools.
Top comments (0)