Building Kubernetes Operators: Automating Application Management

Introduction

Managing stateful applications in Kubernetes manually can be complex. Automating application lifecycle tasks—like deployment, scaling, and failover—requires encoding operational knowledge into software. This is where Kubernetes Operators come in.

Operators extend Kubernetes by using Custom Resource Definitions (CRDs) and controllers to manage applications just like native Kubernetes resources. In this guide, we’ll build an Operator for a PostgreSQL database, automating its lifecycle management.

Step 1: Setting Up the Operator SDK

To create a Kubernetes Operator, we use the Operator SDK, which simplifies scaffolding and controller development.

Install Operator SDK

If you haven’t installed Operator SDK, follow these steps:

export ARCH=$(uname -m)
curl -LO "https://github.com/operator-framework/operator-sdk/releases/latest/download/operator-sdk_linux_${ARCH}"
chmod +x operator-sdk_linux_${ARCH}
sudo mv operator-sdk_linux_${ARCH} /usr/local/bin/operator-sdk

Verify installation:

operator-sdk version

Step 2: Initializing the Operator Project

We start by initializing our Operator project:

operator-sdk init --domain mycompany.com --repo github.com/mycompany/postgres-operator --skip-go-version-check

This command:
✅ Sets up the project structure
✅ Configures Go modules
✅ Generates required manifests

Step 3: Creating the PostgreSQL API and Controller

Now, let’s create a Custom Resource Definition (CRD) and a controller:

operator-sdk create api --group database --version v1alpha1 --kind PostgreSQL --resource --controller

This generates:
api/v1alpha1/postgresql_types.go → Defines the PostgreSQL resource structure
controllers/postgresql_controller.go → Implements the logic to manage PostgreSQL instances

Step 4: Defining the Custom Resource (CRD)

Edit api/v1alpha1/postgresql_types.go to define the PostgreSQLSpec and PostgreSQLStatus:

package v1alpha1

import (
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

// PostgreSQLSpec defines the desired state
type PostgreSQLSpec struct {
	Replicas  int    `json:"replicas"`
	Image     string `json:"image"`
	Storage   string `json:"storage"`
}

// PostgreSQLStatus defines the observed state
type PostgreSQLStatus struct {
	ReadyReplicas int `json:"readyReplicas"`
}

// PostgreSQL is the Schema for the PostgreSQL API
type PostgreSQL struct {
	metav1.TypeMeta   `json:",inline"`
	metav1.ObjectMeta `json:"metadata,omitempty"`

	Spec   PostgreSQLSpec   `json:"spec,omitempty"`
	Status PostgreSQLStatus `json:"status,omitempty"`
}
Click Here to Copy Go Language

Register this CRD:

make manifests
make install

Step 5: Implementing the Controller Logic

Edit controllers/postgresql_controller.go to define how the Operator manages PostgreSQL:

package controllers

import (
	"context"

	databasev1alpha1 "github.com/mycompany/postgres-operator/api/v1alpha1"
	appsv1 "k8s.io/api/apps/v1"
	corev1 "k8s.io/api/core/v1"
	ctrl "sigs.k8s.io/controller-runtime"
	"sigs.k8s.io/controller-runtime/pkg/client"
)

type PostgreSQLReconciler struct {
	client.Client
}

func (r *PostgreSQLReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
	var postgres databasev1alpha1.PostgreSQL
	if err := r.Get(ctx, req.NamespacedName, &postgres); err != nil {
		return ctrl.Result{}, client.IgnoreNotFound(err)
	}

	deployment := &appsv1.Deployment{}
	if err := r.Get(ctx, req.NamespacedName, deployment); err != nil {
		// Define a new Deployment
		deployment = &appsv1.Deployment{
			ObjectMeta: postgres.ObjectMeta,
			Spec: appsv1.DeploymentSpec{
				Replicas: int32Ptr(int32(postgres.Spec.Replicas)),
				Template: corev1.PodTemplateSpec{
					Spec: corev1.PodSpec{
						Containers: []corev1.Container{{
							Name:  "postgres",
							Image: postgres.Spec.Image,
						}},
					},
				},
			},
		}
		if err := r.Create(ctx, deployment); err != nil {
			return ctrl.Result{}, err
		}
	}

	return ctrl.Result{}, nil
}

func int32Ptr(i int32) *int32 {
	return &i
}

func (r *PostgreSQLReconciler) SetupWithManager(mgr ctrl.Manager) error {
	return ctrl.NewControllerManagedBy(mgr).
		For(&databasev1alpha1.PostgreSQL{}).
		Complete(r)
}
Click Here to Copy Go Language

Step 6: Deploying the Operator

Build and push the Operator container:

make docker-build docker-push IMG=mycompany/postgres-operator:latest

Apply the Operator to the cluster:

make deploy IMG=mycompany/postgres-operator:latest

Step 7: Creating a PostgreSQL Custom Resource

Once the Operator is deployed, create a PostgreSQL instance:

apiVersion: database.mycompany.com/v1alpha1
kind: PostgreSQL
metadata:
  name: my-db
spec:
  replicas: 2
  image: postgres:13
  storage: 10Gi
Click Here to Copy YAML

Apply it:

kubectl apply -f postgresql-cr.yaml

Verify the Operator has created a Deployment:

kubectl get deployments

Step 8: Testing the Operator

Check if the PostgreSQL pods are running:

kubectl get pods

Describe the Custom Resource:

kubectl describe postgresql my-db

Delete the PostgreSQL instance:

kubectl delete postgresql my-db

Conclusion

We successfully built a Kubernetes Operator to manage PostgreSQL instances automatically. By encoding operational knowledge into software, Operators:
✅ Simplify complex application management
✅ Enable self-healing and auto-scaling
✅ Enhance Kubernetes-native automation

Operators are essential for managing stateful applications efficiently in Kubernetes.

What application would you like to automate with an Operator? Drop your thoughts in the comments!👇

Leave a comment