Kubernetes Operators - Introduction
At a high level, Kubernetes has one core job: continuously move the actual state of the system toward the desired state. It does this through a reconciliation-loop. Each controller watches the resources it owns and repeatedly reconciles what’s running in the cluster with what the user declared. In a typical Kubernetes cluster, many controllers run in parallel—each with its own control loop—managing different parts of the system.
All controllers typically use the watch-and-reconcile pattern. Writing a controller from scratch involves lot of repetitive code.
Kubebuilder helps with basic code generation (scaffolding) needed for building an operator. Please note an operator can be written in any of the popular languages.
A Kubernetes Operator is a custom controller that extends Kubernetes to automate the management of complex, stateful applications—handling tasks like deployment, scaling, backups, and upgrades that would normally require manual intervention. It encodes operational knowledge into software, essentially acting as an automated site reliability engineer for your application.
Kubebuilder [1] implemented in Go, is one of the standard ways to build a custom controller (often called an operator).
Let’s walk through a simple hello-world example.
Prerequisite
# Go (if you don't have it)
brew install go
# Kubebuilder - the framework
brew install kubebuilder
# Kind - runs Kubernetes locally in Docker
brew install kind
# Make sure Docker Desktop is running!
Create Local Kubernetes Cluster
kind create cluster --name dev
# Verify it works
kubectl cluster-info
Create the Project
# Create and enter project folder
mkdir ~/greeting-operator && cd ~/greeting-operator
# Initialize the project
# --domain: your API group suffix (like "apps.example.com")
# --repo: Go module path
kubebuilder init --domain example.com --repo greeting-operator
# Create an API (type 'y' for both prompts)
# --group: first part of API group ("apps")
# --version: API version ("v1")
# --kind: your resource name ("Greeting")
kubebuilder create api --group apps --version v1 --kind Greeting
What just happened?
- Created Go module with controller-runtime dependencies
- Generated api/v1/greeting_types.go - your CRD definition
- Generated internal/controller/greeting_controller.go - your logic
- Generated Makefile, Dockerfile, and Kubernetes manifests
Define Your Custom Resource
Update the api types file (greeting_types.go) with following:
package v1
import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
// =============================================================================
// SPEC: What the user provides in their YAML
// =============================================================================
type GreetingSpec struct {
// Name to greet (required)
// +kubebuilder:validation:Required
Name string `json:"name"`
// Greeting word (optional, defaults to "Hello")
// +kubebuilder:default="Hello"
Greeting string `json:"greeting,omitempty"`
}
// =============================================================================
// STATUS: What the controller reports back
// =============================================================================
type GreetingStatus struct {
// The composed message
Message string `json:"message,omitempty"`
// Whether reconciliation succeeded
Ready bool `json:"ready"`
}
// =============================================================================
// THE MAIN TYPE
// =============================================================================
// +kubebuilder:object:root=true
// +kubebuilder:subresource:status
// +kubebuilder:printcolumn:name="Message",type="string",JSONPath=".status.message"
// +kubebuilder:printcolumn:name="Ready",type="boolean",JSONPath=".status.ready"
type Greeting struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Spec GreetingSpec `json:"spec,omitempty"`
Status GreetingStatus `json:"status,omitempty"`
}
// +kubebuilder:object:root=true
type GreetingList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`
Items []Greeting `json:"items"`
}
func init() {
SchemeBuilder.Register(&Greeting{}, &GreetingList{})
}
Write the Controller Logic
Open internal/controller/greeting_controller.go and update Reconcile() with:
// Reconcile - called whenever a Greeting changes (or periodically)
//
// The goal: make reality match the desired state
// This function should be IDEMPOTENT - running it twice = same result
func (r *GreetingReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
logger := log.FromContext(ctx)
// -------------------------------------------------------------------------
// STEP 1: Fetch the Greeting resource
// -------------------------------------------------------------------------
var greeting appsv1.Greeting
if err := r.Get(ctx, req.NamespacedName, &greeting); err != nil {
// Not found = deleted, ignore
return ctrl.Result{}, client.IgnoreNotFound(err)
}
logger.Info("Reconciling", "name", greeting.Spec.Name)
// -------------------------------------------------------------------------
// STEP 2: Do our "business logic" (just build a message)
// -------------------------------------------------------------------------
message := fmt.Sprintf("%s, %s!", greeting.Spec.Greeting, greeting.Spec.Name)
// -------------------------------------------------------------------------
// STEP 3: Update the status
// -------------------------------------------------------------------------
greeting.Status.Message = message
greeting.Status.Ready = true
// Status().Update() only updates the status subresource
if err := r.Status().Update(ctx, &greeting); err != nil {
logger.Error(err, "Failed to update status")
return ctrl.Result{}, err
}
logger.Info("Reconciled successfully", "message", message)
// Return empty result = success, don't requeue
return ctrl.Result{}, nil
}
Install the CRD (customresource definition)
# Generate deep copy methods and CRD manifests
make generate
make manifests
# Install CRD into your cluster
make install
# Verify CRD is installed
kubectl get crd
# Should show: greetings.apps.example.com
Run and Test
- Terminal 1: Run the controller locally :
make run - Terminal 2: Create a Greeting resource
# Create a test resource
cat <<EOF | kubectl apply -f -
apiVersion: apps.example.com/v1
kind: Greeting
metadata:
name: my-greeting
spec:
name: "World"
greeting: "Hello"
EOF
Watch it work!
# See the custom columns we defined
kubectl get greetings
# Output:
# NAME MESSAGE READY
# my-greeting Hello, World! true
# See full details
kubectl get greeting my-greeting -o yaml
What is happening under the hood ?
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ User │ │ Kubernetes │ │ Controller │
│ │ │ API Server │ │ (your code)│
└──────┬───────┘ └──────┬───────┘ └──────┬───────┘
│ │ │
│ kubectl apply │ │
│ greeting.yaml │ │
│───────────────────────>│ │
│ │ │
│ │ "Hey, Greeting │
│ │ changed!" │
│ │───────────────────────>│
│ │ │
│ │ │ Reconcile()
│ │ │ - Get Greeting
│ │<───────────────────────│
│ │ │
│ │ │ - Build message
│ │ │
│ │ Update Status │
│ │<───────────────────────│
│ │ │
│ kubectl get greetings │ │
│───────────────────────>│ │
│ │ │
│ MESSAGE: Hello, World!│ │
│<───────────────────────│ │