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"`
}
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)
}
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
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!![]()