Skip to content

Kubernetes Operators: CRD + Controller

FieldValue
TypeSkill Resource
Source~/.copilot/skills/platform/references/kubernetes-operator.md
DescriptionNot specified

Source Content

Kubernetes Operators: CRD + Controller

Reach for an operator when Helm has run out of road — when a workload needs runtime behavior (rotation, failover, scheduled backup, drift correction), not just templating. An operator is a controller plus operational knowledge: the runbook, encoded as code.

Contents

Controller vs operator

Controller = pure reconciliation loop. Operator = controller + domain knowledge. State explicitly which one you’re building before writing code.

Use me for / Don’t use me for

Use for:

  • Workloads needing lifecycle automation Helm can’t express (rotation, failover, scheduled backups).
  • Bundling multiple resources behind a single high-level CRD for app teams.
  • Wrapping an external API (cloud, SaaS) as Kubernetes resources via Crossplane.
  • Extending the P3 stack (CloudNativePG, ArgoCD, Helm) with domain-specific automation.

Don’t use for:

  • Pure templating → a Helm chart, no controller (see references/helm/).
  • One-shot config → ConfigMap + init container.
  • Scheduled work → CronJob, not a custom controller.
  • CI/CD plumbing → references/github-actions.md.

“Template our microservice Helm chart” → defer to Helm, no operator needed. “Run this once a day at 3am” → CronJob, not a custom controller.

Picking a framework

FrameworkWhen
Kubebuilder (Go)Default choice. Full control, idiomatic Go, the P3 stack’s language.
MetacontrollerDeclarative, fast prototyping — no Go build step.
CrossplaneWrapping an external cloud/SaaS API as a Kubernetes resource (XRD + Composition).

Document the choice and why.

Designing the CRD

Treat the CRD as a public, long-lived API — names, versions, and validation matter more than the controller code underneath.

  • Spec (desired) and status (observed) are separated. Never mix intent with observed state.
  • Status conditions: Ready, Progressing, Degraded — set explicitly on every reconcile path.
  • printcolumn + OpenAPI validation markers on every field that matters at a glance (kubectl get).
  • Version it from day one. Plan the v1alpha1 → v1 upgrade path and whether a conversion webhook is needed before promoting.

Reconcile, finalizers, webhooks

  • Reconcile must be idempotent and safely re-entrant. Never panic; return ctrl.Result{} or RequeueAfter.
  • Finalizers for cleanup. Register on create; remove only after external resources are confirmed gone. Give every finalizer a timeout / escape hatch — never block deletion forever.
  • Webhooks where invariants matter. Validating webhook for required fields; conversion webhook before promoting v1alpha1 → v1.
  • Events for every user-visible state change.
  • RBAC is minimum-verbs via +kubebuilder:rbac markers — never *.

Testing the failure modes

  • envtest for unit-level reconcile tests.
  • kuttl for E2E: create / update / delete / failure paths.
  • Chaos-test deletion order, partial failures, and API server flakes before shipping.
  • Ship with a cosign-signed image.

Self-rubric

  • Reconcile is idempotent and safely re-entrant.
  • Status conditions (Ready / Progressing / Degraded) documented and set on every path.
  • Finalizer present if external resources are created, with a timeout.
  • RBAC is minimum-verbs via +kubebuilder:rbac markers.
  • Events emitted for user-visible state changes.
  • kuttl or envtest covers create / update / delete / failure.
  • scripts/check_manifests.sh clean on any rendered manifests.

References