Cloud Native 18 min read

Build Your First Kubernetes Operator with Kubebuilder: A Step‑by‑Step Guide

This tutorial explains what a Kubernetes Operator is, walks through setting up the development environment, creating a simple Foo operator with Kubebuilder, defining CRDs and controllers in Go, testing the operator on a local Kind cluster, and suggests further enhancements.

MaGe Linux Operations
MaGe Linux Operations
MaGe Linux Operations
Build Your First Kubernetes Operator with Kubebuilder: A Step‑by‑Step Guide

What is an Operator?

An Operator extends Kubernetes by embedding custom logic that automates tasks beyond the native resources, allowing engineers to encode their operational knowledge as code. It consists of a Custom Resource Definition (CRD) and a controller that continuously reconciles the desired state.

CRD and Custom Resource diagram
CRD and Custom Resource diagram
Controller operation diagram
Controller operation diagram

Building the Operator

The tutorial uses the Kubebuilder framework (built on controller‑runtime) to create a simple Foo operator. First, install the required tools and Kubebuilder:

curl -L -o kubebuilder https://go.kubebuilder.io/dl/latest/$(go env GOOS)/$(go env GOARCH) && chmod +x kubebuilder && mv kubebuilder /usr/local/bin/

Verify the installation:

kubebuilder version
Version: main.version{KubeBuilderVersion:"3.4.1", KubernetesVendor:"1.23.5", GitCommit:"d59d7882ce95ce5de10238e135ddff31d8ede026", BuildDate:"2022-05-06T13:58:56Z", GoOs:"darwin", GoArch:"amd64"}

Initialize a new project and create the API:

kubebuilder init --domain my.domain --repo my.domain/tutorial
kubebuilder create api --group tutorial --version v1 --kind Foo
# answer "y" to create the resource and controller

The generated project structure includes main.go, the config manifests, and a Dockerfile. The Foo CRD defines a spec.name field and a status.happy boolean that becomes true when a Pod with the same name exists.

package v1

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

type FooSpec struct { Name string `json:"name"` }

type FooStatus struct { Happy bool `json:"happy,omitempty"` }

//+kubebuilder:object:root=true
//+kubebuilder:subresource:status

type Foo struct { metav1.TypeMeta `json:",inline"` metav1.ObjectMeta `json:"metadata,omitempty"` Spec FooSpec `json:"spec,omitempty"` Status FooStatus `json:"status,omitempty"` }

//+kubebuilder:object:root=true

type FooList struct { metav1.TypeMeta `json:",inline"` metav1.ListMeta `json:"metadata,omitempty"` Items []Foo `json:"items"` }

func init() { SchemeBuilder.Register(&Foo{}, &FooList{}) }

The controller watches both Foo resources and Pods, updating status.happy based on whether a matching Pod exists:

func (r *FooReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
    var foo tutorialv1.Foo
    if err := r.Get(ctx, req.NamespacedName, &foo); err != nil { return ctrl.Result{}, client.IgnoreNotFound(err) }
    var podList corev1.PodList
    friendFound := false
    if err := r.List(ctx, &podList); err == nil {
        for _, pod := range podList.Items {
            if pod.Name == foo.Spec.Name { friendFound = true; break }
        }
    }
    foo.Status.Happy = friendFound
    if err := r.Status().Update(ctx, &foo); err != nil { return ctrl.Result{}, err }
    return ctrl.Result{}, nil
}

func (r *FooReconciler) SetupWithManager(mgr ctrl.Manager) error {
    return ctrl.NewControllerManagedBy(mgr).
        For(&tutorialv1.Foo{}).
        Watches(&source.Kind{Type: &corev1.Pod{}}, handler.EnqueueRequestsFromMapFunc(r.mapPodsReqToFooReq)).
        Complete(r)
}

After building the manifests ( make manifests) and installing the CRD ( make install), run the operator locally ( make run). The manager starts the Foo controller, which begins reconciling events.

Testing the Controller

Create two Foo resources and a Pod named jack:

apiVersion: tutorial.my.domain/v1
kind: Foo
metadata:
  name: foo-01
spec:
  name: jack
---
apiVersion: tutorial.my.domain/v1
kind: Foo
metadata:
  name: foo-02
spec:
  name: joe
apiVersion: v1
kind: Pod
metadata:
  name: jack
spec:
  containers:
  - name: ubuntu
    image: ubuntu:latest
    command: ["sleep"]
    args: ["infinity"]

Applying these manifests triggers reconciliation loops that set status.happy to true for the Foo whose spec.name matches the Pod. Updating the second Foo’s spec.name to jack also updates its status.

More Work

Further improvements include optimizing event filtering, refining RBAC permissions, enhancing logging, emitting Kubernetes events on updates, adding custom fields to Foo, and writing unit and end‑to‑end tests.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

OperatorGoControllerKubebuilderCustom Resource
MaGe Linux Operations
Written by

MaGe Linux Operations

Founded in 2009, MaGe Education is a top Chinese high‑end IT training brand. Its graduates earn 12K+ RMB salaries, and the school has trained tens of thousands of students. It offers high‑pay courses in Linux cloud operations, Python full‑stack, automation, data analysis, AI, and Go high‑concurrency architecture. Thanks to quality courses and a solid reputation, it has talent partnerships with numerous internet firms.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.