Understanding the Operator Pattern and Building a Kubernetes Operator with Operator SDK
This article introduces the Kubernetes Operator pattern, explains its purpose for automating complex tasks, and provides a step‑by‑step tutorial on creating, configuring, and deploying a custom Operator using the Operator Framework, KubeBuilder, and Go, including code examples and deployment instructions.
Automation tasks in Kubernetes often require reacting to events that are not easily observable. The Operator pattern addresses this by extending Kubernetes with custom resources and controllers that follow the control loop principle.
Operator Pattern Overview
An Operator is a software extension for Kubernetes that uses Custom Resource Definitions (CRDs) to manage applications and their components. It leverages the control loop to monitor and reconcile resources.
What Is the Operator Pattern?
The pattern allows Kubernetes users to create their own resource controllers to automate management of application stacks. It uses CRDs such as the Strimzi Kafka example to define resources.
apiVersion: kafka.strimzi.io/v1beta2
kind: Kafka
metadata:
name: my-cluster
spec:
kafka:
version: 3.4.0
replicas: 3
listeners:
- name: plain
port: 9092
type: internal
tls: false
- name: tls
port: 9093
type: internal
tls: true
config:
offsets.topic.replication.factor: 3
transaction.state.log.replication.factor: 3
transaction.state.log.min.isr: 2
default.replication.factor: 3
min.insync.replicas: 2
inter.broker.protocol.version: "3.4"
storage:
type: jbod
volumes:
- id: 0
type: persistent-claim
size: 100Gi
deleteClaim: false
zookeeper:
replicas: 3
storage:
type: persistent-claim
size: 100Gi
deleteClaim: false
entityOperator:
topicOperator: {}
userOperator: {}The Operator can watch the resources it creates and react to changes, enabling automation such as avoiding repetitive deployments, preventing human errors, and updating configurations automatically.
Examples
1. Automate Repeated Deployments
Instead of manually deploying Prometheus, OpenTelemetry, Grafana, and Postgres for each team, define a CRD (e.g., CRD ObservabilityStack ) that the Operator uses to create all required resources.
2. Automate to Avoid Human Errors
When managing StatefulSets or other resources, an Operator can orchestrate creation, update, or deletion of dozens of resources, reducing the risk of mistakes.
3. Automate Configuration Updates
An Operator can watch for changes in APIs exposed via Nginx and automatically update configuration files across environments.
Creating a Kubernetes Operator
Use the Operator Framework and KubeBuilder. Install the SDK via Homebrew or download from GitHub releases.
brew install operator-sdk # Define platform information
export ARCH=$(case $(uname -m) in x86_64) echo -n amd64 ;; aarch64) echo -n arm64 ;; *) echo -n $(uname -m) ;; esac)
export OS=$(uname | awk '{print tolower($0)}')
# Download the binary
export OPERATOR_SDK_DL_URL=https://github.com/operator-framework/operator-sdk/releases/download/v1.28.0
curl -LO ${OPERATOR_SDK_DL_URL}/operator-sdk_${OS}_${ARCH}
chmod +x operator-sdk_${OS}_${ARCH} && sudo mv operator-sdk_${OS}_${ARCH} /usr/local/bin/operator-sdkInitialize Project
operator-sdk init --domain adaendra.org --repo github.com/adaendra/test-operatorThis generates a scaffold with common files such as Makefile, Dockerfile, and main.go .
Create API, Controller, and CRD
operator-sdk create api --group gateway --version v1alpha1 --kind MyProxy --resource --controllerThe command creates api and controllers directories. Edit myproxy_types.go to define the Spec and Status, then run make manifests && make generate .
Controller Skeleton
// SetupWithManager sets up the controller with the Manager.
func (r *MyProxyReconciler) SetupWithManager(mgr ctrl.Manager) error {
return ctrl.NewControllerManagedBy(mgr).
For(&gatewayv1alpha1.MyProxy{}).
Owns(&appsv1.Deployment{}).
Complete(r)
}RBAC permissions are declared with // +kubebuilder:rbac comments.
Reconcile Logic
The Reconcile function retrieves the custom resource, fetches the managed Deployment, creates it if missing, or updates it when necessary. Example snippets:
// Retrieve the resource
myProxy := &gatewayv1alpha1.MyProxy{}
err := r.Get(ctx, req.NamespacedName, myProxy)
if err != nil {
if errors.IsNotFound(err) {
log.Info("Resource not found. Ignoring.")
return ctrl.Result{}, nil
}
log.Error(err, "Failed to get MyProxy")
return ctrl.Result{}, err
}
// Create a new Deployment if not found
if err != nil && errors.IsNotFound(err) {
dep := r.deploymentForExample(myProxy)
err = r.Create(ctx, dep)
if err != nil {
log.Error(err, "Failed to create Deployment")
return ctrl.Result{}, err
}
return ctrl.Result{Requeue: true}, nil
}Build the Operator
Build the container image:
make docker-buildPush to a registry if needed:
make docker-pushDeploy the Operator
make install # install CRDs
make deploy # deploy the OperatorTest the Operator
Create an instance of the custom resource:
apiVersion: gateway.example.com/v1alpha1
kind: MyProxy
metadata:
name: myproxy-sample
spec:
name: totoAfter applying, an Nginx Deployment will appear, demonstrating the Operator in action.
DevOps Cloud Academy
Exploring industry DevOps practices and technical expertise.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.