Building Kubernetes Controllers with Controller Runtime and Kubebuilder
This article explains how to simplify Kubernetes controller development by using the controller-runtime library and the Kubebuilder scaffolding tool, covering manager and webhook concepts, providing full Go examples, YAML configurations, and step‑by‑step commands for building, deploying, and testing a custom Foo controller.
The previous article introduced the low‑level client‑go Informer pattern for writing Kubernetes controllers, which requires a lot of boilerplate code for leader election, webhooks, and error handling. In most cases developers can avoid these details by using the controller‑runtime library, which wraps the Informer logic and provides higher‑level abstractions.
Controller‑runtime introduces three core concepts:
Manager : a component that owns the lifecycle of one or more controllers within a single process.
Controller : created with ctrl.NewControllerManagedBy(mgr).For(&api.Foo{}).Complete(&reconciler{}) , it watches a specific CRD type and calls the Reconcile method.
Webhook : created with ctrl.NewWebhookManagedBy(mgr).For(&api.Foo{}) , it registers validation and mutation logic for the CRD.
The sample Go program below shows a minimal controller‑runtime controller that implements a simple Reconcile method, sets up a manager with leader election, registers the Foo CRD scheme, and adds both a controller and a webhook to the manager.
package main
import (
"context"
"fmt"
"os"
api "github.com/zhaohuabing/k8scontrollertutorial/pkg/custom/apis/foo/v1alpha1"
_ "k8s.io/client-go/plugin/pkg/client/auth/gcp"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/log"
"sigs.k8s.io/controller-runtime/pkg/log/zap"
)
var (
setupLog = ctrl.Log.WithName("setup")
)
type reconciler struct {
client.Client
}
func (r *reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
log := log.FromContext(ctx).WithValues("foo", req.NamespacedName)
log.V(1).Info("reconciling foo")
var foo api.Foo
if err := r.Get(ctx, req.NamespacedName, &foo); err != nil {
log.Error(err, "unable to get foo")
return ctrl.Result{}, err
}
fmt.Printf("Sync/Add/Update for foo %s\n", foo.GetName())
return ctrl.Result{}, nil
}
func main() {
ctrl.SetLogger(zap.New())
mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{LeaderElection: true, LeaderElectionID: "sample-controller", LeaderElectionNamespace: "kube-system"})
if err != nil {
setupLog.Error(err, "unable to start manager")
os.Exit(1)
}
if err = api.AddToScheme(mgr.GetScheme()); err != nil {
setupLog.Error(err, "unable to add scheme")
os.Exit(1)
}
err = ctrl.NewControllerManagedBy(mgr).
For(&api.Foo{}).
Complete(&reconciler{Client: mgr.GetClient()})
if err != nil {
setupLog.Error(err, "unable to create controller")
os.Exit(1)
}
err = ctrl.NewWebhookManagedBy(mgr).
For(&api.Foo{}).
Complete()
if err != nil {
setupLog.Error(err, "unable to create webhook")
os.Exit(1)
}
setupLog.Info("starting manager")
if err = mgr.Start(ctrl.SetupSignalHandler()); err != nil {
setupLog.Error(err, "problem running manager")
os.Exit(1)
}
}When the manager starts, it checks whether the Foo struct implements the Validator and Defaulter interfaces. If so, controller‑runtime automatically creates a webhook server. The following Go methods implement validation and mutation logic for the Foo CRD:
// Validation webhook
func (f *Foo) ValidateCreate() error {
if f.Spec.Replicas != nil && *f.Spec.Replicas < 0 {
return fmt.Errorf("replicas should be non‑negative")
}
return nil
}
func (f *Foo) ValidateUpdate(old runtime.Object) error {
if f.Spec.Replicas != nil && *f.Spec.Replicas < 0 {
return fmt.Errorf("replicas should be non‑negative")
}
return nil
}
func (f *Foo) ValidateDelete() error { return nil }
// Mutation webhook
func (f *Foo) Default() {
if f.Spec.Replicas == nil {
f.Spec.Replicas = new(int32)
*f.Spec.Replicas = 1
}
}Corresponding ValidatingWebhookConfiguration and MutatingWebhookConfiguration YAML files tell the API server to route Foo create/update/delete requests to the webhook server:
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingWebhookConfiguration
metadata:
name: foo
webhooks:
- name: foo.samplecontroller.k8s.io
clientConfig:
service:
namespace: default
name: sample-controller-webhook-server
path: /validate-samplecontroller-k8s-io-v1alpha1-foo
rules:
- apiGroups: ["samplecontroller.k8s.io"]
apiVersions: ["v1alpha1"]
resources: ["foos"]
operations: ["CREATE", "UPDATE", "DELETE"]
scope: Namespaced
sideEffects: None
admissionReviewVersions: ["v1"]
---
apiVersion: admissionregistration.k8s.io/v1
kind: MutatingWebhookConfiguration
metadata:
name: foo
webhooks:
- name: foo.samplecontroller.k8s.io
clientConfig:
service:
namespace: default
name: sample-controller-webhook-server
path: /validate-samplecontroller-k8s-io-v1alpha1-foo
rules:
- apiGroups: ["samplecontroller.k8s.io"]
apiVersions: ["v1alpha1"]
resources: ["foos"]
operations: ["CREATE"]
scope: Namespaced
sideEffects: None
admissionReviewVersions: ["v1"]While controller‑runtime reduces boilerplate, Kubebuilder further automates project scaffolding. After installing the kubebuilder CLI, the following commands generate a full project structure, CRD definitions, and controller skeletons:
kubebuilder init --project-name kubebuilderexample --domain zhaohuabing.com --repo github.com/zhaohuabing/kubebuilderexample kubebuilder create api --group samplecontroller --version v1alpha1 --kind FooDevelopers then edit api/v1alpha1/foo_types.go to add spec and status fields, run make manifests to generate the CRD YAML, and install it with make install . The controller image is built and pushed using:
make docker-build docker-push IMG=zhaohuabing/sample-controller:kubebuilderFinally, the controller is deployed to the cluster with:
make deploy IMG=zhaohuabing/sample-controller:kubebuilderSample logs show the manager starting, leader election, and the controller processing a Foo resource:
2023-04-07T06:57:46Z INFO setup starting manager
2023-04-07T06:58:10Z INFO Starting Controller {"controller":"foo","controllerGroup":"samplecontroller.zhaohuabing.com","controllerKind":"Foo"}
reconcile foo foo-sampleIn summary, the series covered three approaches to building Kubernetes controllers—raw Informer, controller‑runtime, and Kubebuilder—each offering a trade‑off between flexibility and developer convenience. Controller‑runtime provides a concise API with built‑in webhook support, while Kubebuilder adds project generation and further reduces manual setup.
References:
Kubernetes controller‑runtime (https://github.com/kubernetes-sigs/controller-runtime)
Kubebuilder quick start (https://book.kubebuilder.io/quick-start.html)
Source code for the controller‑runtime example (https://github.com/zhaohuabing/k8scontrollertutorial/tree/main/pkg/custom/controller_runtime)
Source code for the Kubebuilder example (https://github.com/zhaohuabing/kubebuilderexample)
Cloud Native Technology Community
The Cloud Native Technology Community, part of the CNBPA Cloud Native Technology Practice Alliance, focuses on evangelizing cutting‑edge cloud‑native technologies and practical implementations. It shares in‑depth content, case studies, and event/meetup information on containers, Kubernetes, DevOps, Service Mesh, and other cloud‑native tech, along with updates from the CNBPA alliance.
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.