Cloud Native 19 min read

Master Kubernetes Operators with Kubebuilder: Build, Deploy, and Test Your First Operator

This step‑by‑step tutorial explains what a Kubernetes Operator is, shows how to set up the development environment with Kubebuilder, walks through creating CRDs and controllers in Go, and demonstrates building, installing, and testing the operator in a local Kind cluster.

MaGe Linux Operations
MaGe Linux Operations
MaGe Linux Operations
Master Kubernetes Operators with Kubebuilder: Build, Deploy, and Test Your First Operator

What is an Operator?

Kubernetes Operators extend the native API by embedding custom business logic as code, enabling automation of tasks that Kubernetes cannot perform on its own, such as managing MySQL, Elasticsearch, or GitLab Runner instances.

Building an Operator

We use the Kubebuilder framework because it is easy to use, well‑documented, and battle‑tested.

Set up the development environment

Go v1.17.9+

Docker 17.03+

kubectl v1.11.3+

A Kubernetes v1.11.3+ cluster (Kind is recommended for local testing)

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"}

Now create a simple foo operator to illustrate the workflow.

kubebuilder init --domain my.domain --repo my.domain/tutorial

The generated project structure includes main.go (manager entry), config/ (manifests), and Dockerfile (image build).

Operator Architecture

An Operator consists of two components:

Custom Resource Definition (CRD) : defines a new Kubernetes resource type.

Controller : watches the custom resource and reconciles the actual cluster state to the desired state.

CRD and Custom Resource diagram
CRD and Custom Resource diagram
Controller flow diagram
Controller flow diagram

Create API and Controller

kubebuilder create api --group tutorial --version v1 --kind Foo
Create Resource [y/n] y
Create Controller [y/n] y

This generates api/v1/foo_types.go (CRD schema) and controllers/foo_controller.go (reconciliation logic).

package v1

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

// FooSpec defines the desired state of Foo
type FooSpec struct {
    // Name of the friend Foo is looking for
    Name string `json:"name"`
}

// FooStatus defines the observed state of Foo
type FooStatus struct {
    // Happy is true when a matching Pod is found
    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{}) }
package controllers

import (
    "context"
    corev1 "k8s.io/api/core/v1"
    "k8s.io/apimachinery/pkg/runtime"
    "k8s.io/apimachinery/pkg/types"
    ctrl "sigs.k8s.io/controller-runtime"
    "sigs.k8s.io/controller-runtime/pkg/client"
    "sigs.k8s.io/controller-runtime/pkg/handler"
    "sigs.k8s.io/controller-runtime/pkg/log"
    "sigs.k8s.io/controller-runtime/pkg/reconcile"
    "sigs.k8s.io/controller-runtime/pkg/source"
    tutorialv1 "my.domain/tutorial/api/v1"
)

type FooReconciler struct {
    client.Client
    Scheme *runtime.Scheme
}

//+kubebuilder:rbac:groups=tutorial.my.domain,resources=foos,verbs=get;list;watch;create;update;patch;delete
//+kubebuilder:rbac:groups=tutorial.my.domain,resources=foos/status,verbs=get;update;patch
//+kubebuilder:rbac:groups=tutorial.my.domain,resources=foos/finalizers,verbs=update
//+kubebuilder:rbac:groups="",resources=pods,verbs=get;list;watch

func (r *FooReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
    log := log.FromContext(ctx)
    var foo tutorialv1.Foo
    if err := r.Get(ctx, req.NamespacedName, &foo); err != nil {
        log.Error(err, "unable to fetch Foo")
        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.GetName() == foo.Spec.Name {
                friendFound = true
                break
            }
        }
    }
    foo.Status.Happy = friendFound
    if err := r.Status().Update(ctx, &foo); err != nil {
        log.Error(err, "unable to update Foo status")
        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)
}

func (r *FooReconciler) mapPodsReqToFooReq(obj client.Object) []reconcile.Request {
    var list tutorialv1.FooList
    if err := r.Client.List(context.TODO(), &list); err != nil {
        return nil
    }
    var reqs []reconcile.Request
    for _, foo := range list.Items {
        if foo.Spec.Name == obj.GetName() {
            reqs = append(reqs, reconcile.Request{NamespacedName: types.NamespacedName{Name: foo.Name, Namespace: foo.Namespace}})
        }
    }
    return reqs
}

Generate Manifests and Deploy

make manifests
# Generates CRD YAML in config/crd/bases
make install
kubectl apply -k config/crd
# CRD foos.tutorial.my.domain is created

Run the Operator

make run
# Starts the manager and the Foo controller, listening for events

Test the Operator

Create two Foo resources:

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
kubectl apply -f config/samples

Observe that the controller reconciles both resources (status initially false). Deploy a Pod named jack:

apiVersion: v1
kind: Pod
metadata:
  name: jack
spec:
  containers:
  - name: ubuntu
    image: ubuntu:latest
    command: ["sleep"]
    args: ["infinity"]
kubectl apply -f jack-pod.yaml

The controller detects the matching Pod, updates foo-01 status to true, and logs the event. Updating foo-02 spec to name: jack triggers another reconciliation, setting its status to true. Deleting the Pod resets the status to false, confirming the Operator works as expected.

For the full source code, visit the GitHub repository:

https://github.com/leovct/kubernetes-operator-tutorial

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.

KubernetesOperatorGoControllerCRDOperator SDKKubebuilder
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.