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.
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/tutorialThe 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.
Create API and Controller
kubebuilder create api --group tutorial --version v1 --kind Foo
Create Resource [y/n] y
Create Controller [y/n] yThis 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 createdRun the Operator
make run
# Starts the manager and the Foo controller, listening for eventsTest 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/samplesObserve 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.yamlThe 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
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
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.
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.
