Kubernetes Operators Basics
10 examples to get you started with K8s Operators - 7 basic and 3 intermediate.
Search across all documentation pages
10 examples to get you started with K8s Operators - 7 basic and 3 intermediate.
kubectl and a local cluster (kind, minikube, or Docker Desktop Kubernetes).kustomize (bundled with recent kubectl) for make deploy.go version, kubebuilder version, kubectl cluster-info.Create a module with API group webapp.example.com and a Guestbook kind.
mkdir guestbook-operator && cd guestbook-operator
kubebuilder init --domain example.com --repo example.com/guestbook-operator
kubebuilder create api --group webapp --version v1 --kind Guestbook --resource --controllerinit generates main.go, Makefile, and Dockerfile scaffold.create api adds api/v1/guestbook_types.go and internal/controller/guestbook_controller.go.go mod tidy after scaffolding to resolve controller-runtime modules.Related: CRD Design & kubebuilder Scaffolding - API markers and codegen
Use controller-gen via the project Makefile to emit install YAML.
make manifests
ls config/crd/bases/make manifests runs controller-gen with +kubebuilder markers from Go types.config/crd/bases/ and config/rbac/.kubectl apply.Related: CRD Design & kubebuilder Scaffolding - validation markers
Apply generated CRDs and RBAC to kind or minikube.
make install
kubectl get crd | grep webappmake install applies CRD bases from config/crd.guestbooks.webapp.example.com appears before starting the manager.Related: controller-runtime Manager, Client & Scheme - Scheme registration
The generated controller implements Reconcile with a typed client.
func (r *GuestbookReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
var gb webappv1.Guestbook
if err := r.Get(ctx, req.NamespacedName, &gb); err != nil {
return ctrl.Result{}, client.IgnoreNotFound(err)
}
// TODO: ensure desired child resources exist
return ctrl.Result{}, nil
}req.NamespacedName identifies the object that triggered work.client.IgnoreNotFound(err) when the CR was deleted.ctrl.Result{} means success with no forced requeue.Related: Writing a Reconciler: ctrl.Request & Reconcile Loop - requeue and errors
Wire the reconciler in main.go with the Manager's client and scheme.
if err = (&controller.GuestbookReconciler{
Client: mgr.GetClient(),
Scheme: mgr.GetScheme(),
}).SetupWithManager(mgr); err != nil {
setupLog.Error(err, "unable to create controller", "controller", "Guestbook")
os.Exit(1)
}mgr.GetClient() is a caching client safe for reads inside reconcile.SetupWithManager registers watches and connects the workqueue.Related: controller-runtime Manager, Client & Scheme - Manager lifecycle
Start the operator against your kubeconfig with leader election disabled for dev.
make runmake run executes go run ./cmd/main.go with envtest-free cluster access.kubectl apply -f config/samples/.Related: Leader Election, Metrics & Operator Observability - health and metrics endpoints
Use the generated sample manifest to trigger reconciliation.
apiVersion: webapp.example.com/v1
kind: Guestbook
metadata:
name: guestbook-sample
namespace: default
spec:
frontendSize: 2kubectl apply -f config/samples/webapp_v1_guestbook.yaml
kubectl get guestbook guestbook-sample -o yamlconfig/samples/ after create api..status once you implement them.spec and confirm the reconciler runs again (level-driven).Related: Watches, Predicates, Owner References & Finalizers - event filtering
Tie a child Deployment lifecycle to the parent Guestbook CR.
dep := &appsv1.Deployment{ObjectMeta: metav1.ObjectMeta{
Name: gb.Name + "-frontend", Namespace: gb.Namespace,
}}
if err := controllerutil.SetControllerReference(&gb, dep, r.Scheme); err != nil {
return ctrl.Result{}, err
}SetControllerReference requires the parent to have a controller flag set.Related: Watches, Predicates, Owner References & Finalizers - GC and watches
Block CR removal until external cleanup finishes.
if !controllerutil.ContainsFinalizer(&gb, finalizerName) {
controllerutil.AddFinalizer(&gb, finalizerName)
return ctrl.Result{}, r.Update(ctx, &gb)
}
if !gb.DeletionTimestamp.IsZero() {
// teardown external resources, then:
controllerutil.RemoveFinalizer(&gb, finalizerName)
return ctrl.Result{}, r.Update(ctx, &gb)
}Update.Related: Watches, Predicates, Owner References & Finalizers - teardown safety
Test reconcile logic without a live cluster using envtest.
make testtestEnv := &envtest.Environment{
CRDDirectoryPaths: []string{filepath.Join("..", "..", "config", "crd", "bases")},
}
cfg, err := testEnv.Start()suite_test.go.make test runs go test with CRDs loaded into a local control plane.Related: Operator Testing with envtest & fake client - fake client patterns
Stack versions: This page was written for Go 1.26.x (Green Tea GC default, go fix modernizers - verify patch at build), chi (latest - verify at build), gin (latest - verify at build), echo (latest - verify at build), google.golang.org/grpc (latest - verify at build), sigs.k8s.io/controller-runtime (latest - verify at build), kubebuilder (latest - verify at build), tinygo (latest - verify board targets at build), wazero (latest - verify at build), and golangci-lint (latest - verify linter set at build).
Reviewed by Chris St. John·Last updated Jul 18, 2026