---
name: kubernetes-operator-builder
description: "Build K8s operators with Kubebuilder — CRDs, controllers, reconciliation, webhooks, RBAC. Builds Kubernetes operators with Kubebuilder / controller-runtime: CRD design, reconciliation loops, status subresource patterns, finalizers, mutating + validating webhooks, RBAC scoping, and the testing harness (envtest + Ginkgo)."
license: MIT
---

# Kubernetes Operator Builder

Scaffolds and fills in production-grade Kubernetes operators using Kubebuilder and the controller-runtime library. It handles the parts that are easy to get subtly wrong — reconciliation idempotency, status vs. spec separation, finalizer cleanup, and webhook wiring — so you end up with a controller that behaves correctly under retries and resyncs. Aimed at Go developers extending Kubernetes with custom resources, whether you're packaging an internal platform abstraction or shipping an operator to OperatorHub.

## When to use it

- You're starting a new operator and want a correct `kubebuilder init` + `kubebuilder create api` layout instead of hand-rolling one.
- You have a CRD but your `Reconcile` loop fights itself — fixing the same object every resync, or losing status on conflict.
- You need to add a mutating or validating webhook (defaulting, immutability checks) and keep the cert wiring sane.
- You're adding a finalizer to clean up external resources (cloud LBs, DB users) before the CR is deleted.

## How it works

It drives the standard toolchain: `kubebuilder init --domain`, `kubebuilder create api --group --version --kind`, and `make manifests generate` (which runs `controller-gen` to emit CRD YAML, deepcopy functions, and RBAC role rules from `// +kubebuilder:rbac` markers). Types are annotated with `// +kubebuilder:validation:` and `// +kubebuilder:subresource:status` markers. The controller is built around `ctrl.NewControllerManagedBy(mgr).For(...).Owns(...)`, with reconciler logic returning `ctrl.Result{Requeue/RequeueAfter}` and using `client.IgnoreNotFound`, `controllerutil.SetControllerReference`, and `r.Status().Update()` for the status subresource. Finalizers use `controllerutil.AddFinalizer`/`RemoveFinalizer`. Tests run under `envtest` (a real apiserver + etcd, no kubelet) with Ginkgo/Gomega.

## Example

```bash
curl -fsSL --create-dirs -o ~/.claude/skills/kubernetes-operator-builder/SKILL.md https://claudeskil.com/skills/kubernetes-operator-builder/SKILL.md

kubebuilder init --domain example.com --repo example.com/widget-operator
kubebuilder create api --group apps --version v1alpha1 --kind Widget
```

```go
func (r *WidgetReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
    var w appsv1alpha1.Widget
    if err := r.Get(ctx, req.NamespacedName, &w); err != nil {
        return ctrl.Result{}, client.IgnoreNotFound(err)
    }
    // reconcile owned Deployment, then:
    w.Status.Ready = true
    return ctrl.Result{}, r.Status().Update(ctx, &w)
}
```

## Tips & gotchas

- Always update spec and status through separate calls: mutating `.Status` and calling `r.Update()` silently drops it unless the `status` subresource is enabled and you use `r.Status().Update()`.
- Never trigger reconciles on a fixed timer when you can use `.Owns()` and watches — `RequeueAfter` is for genuine backoff/polling, not change detection.
- Make reconcilation idempotent and conflict-tolerant: a `409 Conflict` on update is normal; return the error and let the queue retry rather than looping in place.
- Re-run `make manifests` after editing RBAC markers, or your deployed `ClusterRole` won't match what the controller actually does.

## FAQ

**Kubebuilder or Operator SDK?** Operator SDK's Go path wraps Kubebuilder, so the controller-runtime patterns here apply to both. This skill targets the Kubebuilder CLI and layout directly.

**Do I need a real cluster to test?** No. `envtest` spins up a standalone apiserver and etcd, enough to test CRD validation, reconcilation, and status — but it has no scheduler or kubelet, so Pods never actually run.

**Multiple CRDs in one operator?** Yes — run `kubebuilder create api` per kind; each gets its own reconciler registered on the shared manager.
