Kubernetes Operators: CRD + Controller
| Field | Value |
|---|---|
| Type | Skill Resource |
| Source | ~/.copilot/skills/platform/references/kubernetes-operator.md |
| Description | Not 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
- Use me for / Don’t use me for
- Picking a framework
- Designing the CRD
- Reconcile, finalizers, webhooks
- Testing the failure modes
- Self-rubric
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
| Framework | When |
|---|---|
| Kubebuilder (Go) | Default choice. Full control, idiomatic Go, the P3 stack’s language. |
| Metacontroller | Declarative, fast prototyping — no Go build step. |
| Crossplane | Wrapping 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 → v1upgrade 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{}orRequeueAfter. - 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:rbacmarkers — never*.
Testing the failure modes
envtestfor unit-level reconcile tests.kuttlfor 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:rbacmarkers. - Events emitted for user-visible state changes.
-
kuttlorenvtestcovers create / update / delete / failure. -
scripts/check_manifests.shclean on any rendered manifests.
References
- Kubebuilder book
- controller-runtime
- Kubernetes API conventions
- CloudNativePG (canonical example)
- Operator best practices
- Crossplane compositions
scripts/check_manifests.sh <dir>—kubeconform -strict(if installed), no:latestimage tags, every Deployment/StatefulSet declaresresources:.