Kubernetes Operators a CRDs — custom controllers v praxi
Kubernetes sa často opisuje ako "platforma pre platformy". Toto tvrdenie stojí na jednoduchom, ale mocnom nápade: ak vám nestačí to, čo K8s poskytuje hotové, môžete si rozšíriť jeho API o vlastné typy objektov (CustomResourceDefinitions, CRDs) a vlastné reconciliation controllery (Operators). Vaša aplikácia sa tým stane first-class K8s citizen — deklaratívne ju opisujete YAML-om a Kubernetes ju udržiava v požadovanom stave.
Operator pattern vznikol v 2016 v CoreOS a za posledných 10 rokov sa z exotickej techniky stal štandard. PostgreSQL, Prometheus, Kafka, Argo CD, Cert-Manager — všetko Operators. Ak dnes produktizujete stateful aplikáciu pre K8s, Operator je expected delivery vehicle.
Tento článok vysvetľuje, ako sa Operator stavia od nuly — s Go a controller-runtime, s pomocou kubebuilder alebo operator-sdk, s lifecycle managementom cez OLM, a s pragmatickými odporúčaniami pre produkciu.
Prečo Operator
Klasický deployment modeluje stateless service — container, replicas, nejaké secrets. Scale a rollout riadi Deployment controller ktorý je built-in. Ale čo ak vaša aplikácia je:
- Stateful (databáza, message broker) a potrebuje pokročilý lifecycle (backup, failover, version upgrade s migration)
- Distribuovaná (PostgreSQL cluster s primary + replicas), kde jednotlivé role nie sú zameniteľné
- Rozšiaruje K8s o vlastnú abstrakciu (Cert-Manager's
Certificate, Prometheus'sServiceMonitor, Istio'sVirtualService) - Multi-tenant s per-tenant konfiguráciou riadenou z vyššej vrstvy
Pre takéto prípady klasické Deployment + Service + Helm chart dosiahnu hranicu. Operator vám dá deklaratívny CRD, ktorý tieto detaily zabaľuje do jedného resource-u:
# Namiesto 15+ objektov, 1 CRD:
apiVersion: postgresql.acid.zalan.do/v1
kind: postgresql
metadata:
name: analytics-db
spec:
teamId: "analytics"
numberOfInstances: 3
postgresql:
version: "16"
volume:
size: 500Gi
Operator sa postará o vytvorenie podov s Patroni (primary election), PVC pre data, Service pre primary + replicas, PodDisruptionBudget, backup schedules, WAL-E archival, version upgrades.
Anatómia Operatora
Operator = CRD(s) + Controller(s).
┌─── user ───┐ ┌─── cluster ───┐
│ kubectl │ │ kube-apiserver│
│ apply │──── POST CR ─────►│ │
└────────────┘ │ ┌──────────┐ │
│ │ etcd │ │
│ └────┬─────┘ │
└───────┼────────┘
│ watch
▼
┌────────────────────────┐
│ Operator Pod │
│ ┌──────────────────┐ │
│ │ Controller │ │
│ │ ┌────────────┐ │ │
│ │ │ Reconcile │ │ │
│ │ │ loop │ │ │
│ │ └──────┬─────┘ │ │
│ └─────────┼────────┘ │
└────────────┼───────────┘
│ create/update
▼
┌────────────────────────┐
│ Managed resources │
│ (Pods, Services, PVC) │
└────────────────────────┘
Reconcile loop — srdce operator-a — funguje takto:
- Watchuje Custom Resource (CR) cez K8s informer.
- Pri každej zmene (alebo periodicky) dostane Request s
namespace/name. - Fetch-uje aktuálny stav CR a porovnáva s požadovaným (spec).
- Vytvorí / updatne / zmaže child resources (Pod, Service, ConfigMap) tak, aby realita zodpovedala spec-u.
- Updatne
statusv CR (napr.conditions,readyReplicas).
Kľúčová vlastnosť reconcile-u: idempotencia. Musí fungovať správne, ak ho spustíte 10× za sekundu — musí skončiť v rovnakom stave.
CRDs — rozširujeme K8s API
Pred operatoron je CRD. Minimálny príklad:
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: backends.example.sk
spec:
group: example.sk
scope: Namespaced
names:
plural: backends
singular: backend
kind: Backend
shortNames: [be]
versions:
- name: v1
served: true
storage: true
schema:
openAPIV3Schema:
type: object
properties:
spec:
type: object
required: [image, replicas]
properties:
image:
type: string
pattern: "^[a-zA-Z0-9.\\-/:]+$"
replicas:
type: integer
minimum: 1
maximum: 100
default: 1
resources:
type: object
properties:
cpu:
type: string
default: "100m"
memory:
type: string
default: "128Mi"
status:
type: object
properties:
phase:
type: string
enum: [Pending, Ready, Failed]
readyReplicas:
type: integer
conditions:
type: array
items:
type: object
properties:
type: {type: string}
status: {type: string}
lastTransitionTime: {type: string, format: date-time}
subresources:
status: {}
scale:
specReplicasPath: .spec.replicas
statusReplicasPath: .status.readyReplicas
additionalPrinterColumns:
- name: Image
type: string
jsonPath: .spec.image
- name: Replicas
type: integer
jsonPath: .spec.replicas
- name: Ready
type: integer
jsonPath: .status.readyReplicas
- name: Phase
type: string
jsonPath: .status.phase
Kľúčové časti:
- group + names — definuje ako
kubectl get backendsfunguje - versions — každé API má verzie (v1, v1beta1), musíte mať presne jednu
storage: true - schema (openAPIV3Schema) — K8s validuje manifesty proti tejto schéme (silne odporúčané, bez nej sa akákoľvek hlúposť uloží do etcd)
- subresources —
statusoddelí update spec od update status (RBAC + generation increment) - scale — umožní
kubectl scale backend my-be --replicas=5 - additionalPrinterColumns — čo zobrazí
kubectl get backends
Po kubectl apply tohto CRD môžete vytvoriť:
apiVersion: example.sk/v1
kind: Backend
metadata:
name: checkout
spec:
image: registry.example.sk/checkout:v2.3.1
replicas: 3
resources:
cpu: "500m"
memory: "512Mi"
A kubectl get be uvidí tabuľku s Image, Replicas, Ready, Phase.
Ale bez Operator-a sa nič nestane — CR zostane v etcd a tým to končí. Potrebujeme controller.
Stavba Operator-a — controller-runtime + kubebuilder
controller-runtime je Go knižnica (subprojekt kubernetes-sigs), ktorá zjednodušuje písanie controllerov. kubebuilder je CLI scaffolding tool postavený nad controller-runtime — generuje boilerplate pre CRD, webhook, typescafe manifesty.
Setup projektu
mkdir backend-operator && cd backend-operator
go mod init example.sk/backend-operator
kubebuilder init --domain example.sk --repo example.sk/backend-operator
kubebuilder create api --group apps --version v1 --kind Backend \
--resource --controller
Vygenerovaná štruktúra:
backend-operator/
├── api/
│ └── v1/
│ ├── backend_types.go # CRD structs
│ └── zz_generated_deepcopy.go
├── cmd/
│ └── main.go # entry point
├── config/
│ ├── crd/bases/apps.example.sk_backends.yaml # generated CRD manifest
│ ├── default/
│ ├── manager/manager.yaml
│ └── rbac/
├── internal/
│ └── controller/
│ └── backend_controller.go # reconcile loop
├── Makefile
└── PROJECT
CR type definícia
// api/v1/backend_types.go
package v1
import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/api/resource"
)
type BackendSpec struct {
//+kubebuilder:validation:Required
Image string `json:"image"`
//+kubebuilder:validation:Minimum=1
//+kubebuilder:validation:Maximum=100
//+kubebuilder:default=1
Replicas int32 `json:"replicas,omitempty"`
//+kubebuilder:default={cpu: "100m", memory: "128Mi"}
Resources Resources `json:"resources,omitempty"`
}
type Resources struct {
CPU resource.Quantity `json:"cpu,omitempty"`
Memory resource.Quantity `json:"memory,omitempty"`
}
type BackendStatus struct {
Phase string `json:"phase,omitempty"`
ReadyReplicas int32 `json:"readyReplicas,omitempty"`
Conditions []metav1.Condition `json:"conditions,omitempty"`
}
//+kubebuilder:object:root=true
//+kubebuilder:subresource:status
//+kubebuilder:subresource:scale:specpath=.spec.replicas,statuspath=.status.readyReplicas
//+kubebuilder:printcolumn:name="Image",type=string,JSONPath=`.spec.image`
//+kubebuilder:printcolumn:name="Replicas",type=integer,JSONPath=`.spec.replicas`
//+kubebuilder:printcolumn:name="Ready",type=integer,JSONPath=`.status.readyReplicas`
//+kubebuilder:printcolumn:name="Phase",type=string,JSONPath=`.status.phase`
type Backend struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Spec BackendSpec `json:"spec,omitempty"`
Status BackendStatus `json:"status,omitempty"`
}
//+kubebuilder:object:root=true
type BackendList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`
Items []Backend `json:"items"`
}
func init() {
SchemeBuilder.Register(&Backend{}, &BackendList{})
}
Po make manifests kubebuilder vygeneruje z kubebuilder-komentárov kompletné CRD YAML v config/crd/bases/.
Reconcile loop
// internal/controller/backend_controller.go
package controller
import (
"context"
"fmt"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/util/intstr"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/log"
examplev1 "example.sk/backend-operator/api/v1"
)
type BackendReconciler struct {
client.Client
Scheme *runtime.Scheme
}
//+kubebuilder:rbac:groups=example.sk,resources=backends,verbs=get;list;watch;create;update;patch;delete
//+kubebuilder:rbac:groups=example.sk,resources=backends/status,verbs=get;update;patch
//+kubebuilder:rbac:groups=apps,resources=deployments,verbs=get;list;watch;create;update;patch;delete
//+kubebuilder:rbac:groups="",resources=services,verbs=get;list;watch;create;update;patch;delete
func (r *BackendReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
logger := log.FromContext(ctx)
// 1. Fetch the Backend CR
var backend examplev1.Backend
if err := r.Get(ctx, req.NamespacedName, &backend); err != nil {
if errors.IsNotFound(err) {
return ctrl.Result{}, nil
}
return ctrl.Result{}, err
}
// 2. Reconcile Deployment (create or update)
dep := &appsv1.Deployment{
ObjectMeta: metav1.ObjectMeta{
Name: backend.Name,
Namespace: backend.Namespace,
},
}
op, err := ctrl.CreateOrUpdate(ctx, r.Client, dep, func() error {
if err := ctrl.SetControllerReference(&backend, dep, r.Scheme); err != nil {
return err
}
replicas := backend.Spec.Replicas
dep.Spec.Replicas = &replicas
dep.Spec.Selector = &metav1.LabelSelector{
MatchLabels: map[string]string{"app": backend.Name},
}
dep.Spec.Template = corev1.PodTemplateSpec{
ObjectMeta: metav1.ObjectMeta{
Labels: map[string]string{"app": backend.Name},
},
Spec: corev1.PodSpec{
Containers: []corev1.Container{{
Name: "backend",
Image: backend.Spec.Image,
Resources: corev1.ResourceRequirements{
Limits: corev1.ResourceList{
"cpu": backend.Spec.Resources.CPU,
"memory": backend.Spec.Resources.Memory,
},
},
Ports: []corev1.ContainerPort{{ContainerPort: 8080}},
}},
},
}
return nil
})
if err != nil {
logger.Error(err, "unable to reconcile Deployment")
return ctrl.Result{}, err
}
logger.Info("Deployment reconciled", "operation", op)
// 3. Reconcile Service
svc := &corev1.Service{
ObjectMeta: metav1.ObjectMeta{
Name: backend.Name,
Namespace: backend.Namespace,
},
}
_, err = ctrl.CreateOrUpdate(ctx, r.Client, svc, func() error {
if err := ctrl.SetControllerReference(&backend, svc, r.Scheme); err != nil {
return err
}
svc.Spec.Selector = map[string]string{"app": backend.Name}
svc.Spec.Ports = []corev1.ServicePort{{
Port: 80,
TargetPort: intstr.FromInt(8080),
}}
return nil
})
if err != nil {
return ctrl.Result{}, err
}
// 4. Update status
backend.Status.ReadyReplicas = dep.Status.ReadyReplicas
if dep.Status.ReadyReplicas == backend.Spec.Replicas {
backend.Status.Phase = "Ready"
} else {
backend.Status.Phase = "Pending"
}
if err := r.Status().Update(ctx, &backend); err != nil {
return ctrl.Result{}, err
}
return ctrl.Result{}, nil
}
func (r *BackendReconciler) SetupWithManager(mgr ctrl.Manager) error {
return ctrl.NewControllerManagedBy(mgr).
For(&examplev1.Backend{}).
Owns(&appsv1.Deployment{}).
Owns(&corev1.Service{}).
Complete(r)
}
Kľúčové momenty:
SetControllerReference— nastaví owner reference. Keď Backend zmizne, garbage collector K8s automaticky zmaže Deployment + Service.CreateOrUpdate— idempotentný helper, fetch-ne objekt, aplikuje mutator funkciu, vytvorí alebo updatne..Owns(&appsv1.Deployment{})— Watchuje aj Deploymenty (keď sa zmenia, reconcile sa prepočíta).r.Status().Update— subresource update, neinkrementujegeneration.
Spúšťanie lokálne
# Install CRDs do cluster-a
make install
# Spustiť operator lokálne (proti kubeconfigu)
make run
Operator beží ako local process, pripojí sa na K8s API, watch-uje Backend resources, reconcile-uje. Pre development ideálne — rýchly feedback.
Deploy do klastera
# Build image, push do registry
make docker-build docker-push IMG=ghcr.io/example-sk/backend-operator:v0.1.0
# Deploy operator pod
make deploy IMG=ghcr.io/example-sk/backend-operator:v0.1.0
Operator teraz beží ako pod v backend-operator-system namespace-e.
operator-sdk vs kubebuilder
Oba nástroje stavajú na controller-runtime. Historicky:
- kubebuilder — SIG Cluster Lifecycle project, pôvodný a minimalistickejší
- operator-sdk — Red Hat/CoreOS, pridal podporu pre Ansible a Helm based operators
operator-sdk a kubebuilder zostávajú samostatnými projektmi, ale zdieľajú controller-runtime ako spoločnú knižnicu a koordinujú vývoj — výsledkom je, že Go-based operators napísané v jednom alebo druhom CLI-e sú dnes štruktúrne takmer identické. Rozdiely:
operator-sdkešte vyrába Ansible Operators (reconciliation cez Ansible playbook) a Helm Operators (Helm chart → CR). Vhodné pre jednoduché use-case-y, ale obmedzené.operator-sdkmá Operator Lifecycle Manager (OLM) integráciu hlbšiu — generovanieClusterServiceVersion(CSV) manifestov.
V 2026 odporúčanie: kubebuilder pre nové Go operators, operator-sdk ak už používate OLM alebo potrebujete Ansible/Helm variant.
Operator Capability Levels
Operator Framework (operatorframework.io) definuje 5 úrovní maturity operatora:
| Level | Čo to znamená |
|---|---|
| I. Basic Install | Automatizuje inštaláciu a konfiguráciu |
| II. Seamless Upgrades | Zvláda version upgrades bez zásahu |
| III. Full Lifecycle | App lifecycle management — backups, restore, failover |
| IV. Deep Insights | Metriky, alerting, log aggregation |
| V. Auto Pilot | Auto-scaling, auto-healing, anomaly detection |
Väčšina operators v produkcii je na Level II-III. Level V je rare a typicky commercial (např. PGO od Crunchy Data pre PostgreSQL).
Pragmatické odporúčanie: nestavajte Level V od nuly. Level II (deploy + upgrade) pokryje 80% use-case-ov a nie je to mesiace práce.
Finalizers — lifecycle hooks
Keď user urobí kubectl delete backend my-be, Kubernetes nastaví deletionTimestamp a zmaže. Ale čo ak potrebujete vykonať akciu pred zmazaním (backup, remove external DNS record, notify external system)?
Finalizers sú strings v metadata.finalizers. Kým tam je aspoň jeden, objekt sa nefyzicky nezmaže — len má deletionTimestamp. Váš operator musí:
- Pri prvom reconcile pridať finalizer:
metadata.finalizers += ["example.sk/backend-finalizer"]. - Keď je
deletionTimestamp != nil, spustiť cleanup logiku. - Po úspešnom cleanup-e finalizer odstrániť.
- K8s garbage collector fyzicky zmaže objekt.
const backendFinalizer = "example.sk/backend-finalizer"
func (r *BackendReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
var backend examplev1.Backend
if err := r.Get(ctx, req.NamespacedName, &backend); err != nil {
return ctrl.Result{}, client.IgnoreNotFound(err)
}
// Deletion path
if !backend.DeletionTimestamp.IsZero() {
if controllerutil.ContainsFinalizer(&backend, backendFinalizer) {
// Cleanup: backup DB, remove DNS record, etc.
if err := r.cleanupExternal(ctx, &backend); err != nil {
return ctrl.Result{}, err
}
controllerutil.RemoveFinalizer(&backend, backendFinalizer)
if err := r.Update(ctx, &backend); err != nil {
return ctrl.Result{}, err
}
}
return ctrl.Result{}, nil
}
// Normal path — ensure finalizer set
if !controllerutil.ContainsFinalizer(&backend, backendFinalizer) {
controllerutil.AddFinalizer(&backend, backendFinalizer)
if err := r.Update(ctx, &backend); err != nil {
return ctrl.Result{}, err
}
}
// ... rest of reconcile ...
return ctrl.Result{}, nil
}
Footgun: ak odstránite Operator bez toho, aby ste najprv vyčistili CRs s finalizers, CRs zostanú "zombie" v etcd. kubectl delete --ignore-not-found nepomôže. Musíte manuálne kubectl patch backend my-be --type=merge -p '{"metadata":{"finalizers":null}}'. Preto: undeploy operator-a vždy AŽ po cleanup-e CRs.
Webhooks — validation a mutation
Okrem reconcile loop-u operator často potrebuje admission webhooks pre:
- Defaulting — user pošle minimálny manifest, webhook doplní defaulty
- Validation — komplexná validácia mimo možností CRD schema (napr. krížové validácie medzi fields)
- Conversion — konverzia medzi API verziami (v1beta1 ↔ v1)
kubebuilder scaffold:
kubebuilder create webhook --group example.sk --version v1 \
--kind Backend --defaulting --programmatic-validation
Generovaný webhook handler:
func (r *Backend) Default() {
if r.Spec.Replicas == 0 {
r.Spec.Replicas = 1
}
}
// Note: v controller-runtime v0.15+ webhook signatúra vracia aj admission.Warnings
func (r *Backend) ValidateCreate() (admission.Warnings, error) {
if !strings.HasPrefix(r.Spec.Image, "registry.example.sk/") {
return nil, fmt.Errorf("image must be from registry.example.sk/")
}
return nil, nil
}
Webhooks sa servuje cez TLS — kubebuilder generuje certs cez cert-manager alebo samotné.
Operator Lifecycle Manager (OLM)
OLM je meta-operator, ktorý rieši inštaláciu a version management samotných operatorov. Používajú ho hlavne OpenShift a v kubernetes-only klastroch je optional.
Kľúčové koncepty:
- ClusterServiceVersion (CSV) — metadata o operatorovi (version, owned CRDs, permissions, install method)
- CatalogSource — zdroj operatorov (napr. OperatorHub.io, interný registry)
- Subscription — user request "install X operator from catalog Y"
- InstallPlan — plán inštalácie (resolve dependencies)
Pre samostatné operators typicky OLM nepotrebujete — make deploy stačí. Ale ak distribuujete operator pre širokú publikum, OperatorHub.io (založené na OLM) je štandardný distribution channel.
Testing Operator-a
1. Unit testy
Controller logika v internal/controller/backend_controller_test.go. controller-runtime poskytuje envtest — spúšťa testovaciu kópiu kube-apiserver a etcd lokálne, bez potreby full K8s cluster-a.
var _ = Describe("Backend controller", func() {
It("should create Deployment when Backend is created", func() {
ctx := context.Background()
backend := &examplev1.Backend{
ObjectMeta: metav1.ObjectMeta{Name: "test-be", Namespace: "default"},
Spec: examplev1.BackendSpec{
Image: "registry.example.sk/test:v1",
Replicas: 2,
},
}
Expect(k8sClient.Create(ctx, backend)).Should(Succeed())
Eventually(func() bool {
dep := &appsv1.Deployment{}
err := k8sClient.Get(ctx, types.NamespacedName{
Name: "test-be", Namespace: "default",
}, dep)
return err == nil && *dep.Spec.Replicas == 2
}, time.Second*10, time.Millisecond*250).Should(BeTrue())
})
})
2. E2E testy
kubebuilder + chainsaw (open-source test framework pre K8s operators). Definujete test ako YAML s krokmi apply, wait, assert.
apiVersion: chainsaw.kyverno.io/v1alpha1
kind: Test
metadata:
name: backend-basic-install
spec:
steps:
- name: install
try:
- apply:
file: ./backend.yaml
- assert:
resource:
apiVersion: apps/v1
kind: Deployment
metadata:
name: checkout
- name: scale
try:
- script:
content: |
kubectl scale backend checkout --replicas=5
- sleep: 10s
- assert:
resource:
apiVersion: apps/v1
kind: Deployment
metadata:
name: checkout
spec:
replicas: 5
3. Chaos tests
Pre Level III+ operatorov — čo sa stane, keď zmiznú pods, keď padne primary, keď sa rozdelí cluster? chaos-mesh alebo litmus simulujú tieto scenáre. Viac v článku o Chaos Engineering.
Produkčné odporúčania
1. Leader election
Dva replica Operator-y by spustili reconcile dvakrát — potenciálne závody a konflikty. controller-runtime podporuje leader election cez ConfigMap/Lease — len jeden replica aktívny, ostatné standby.
mgr, err := ctrl.NewManager(cfg, ctrl.Options{
LeaderElection: true,
LeaderElectionID: "backend-operator.example.sk",
LeaderElectionNamespace: "backend-operator-system",
})
2. Metriky
Prometheus metriky automatická cez controller-runtime. Kľúčové:
controller_runtime_reconcile_total{controller="backend", result="success|error"}controller_runtime_reconcile_errors_totalcontroller_runtime_reconcile_time_seconds_bucketworkqueue_depth
Alert na workqueue_depth rastúci = reconcile je pomalší než prichádzajú events.
3. Rate limiting
Default controller-runtime rate-limiter: exponential backoff (max 1000s per item). Pri chybe neustále nespúšťa reconcile.
4. Multi-version support
Ak plánujete CRD version evolution, majte conversion webhook od začiatku. v1beta1 → v1 konverzia na serveri — inak clients musia manuálne migrovať.
5. RBAC — principle of least privilege
kubebuilder generuje permissive RBAC (* na všetky resources v group). Zužte manuálne po stabilizácii CRD.
6. Graceful shutdown
SIGTERM → operator ukončí beha reconcile-u, uvoľní leader election lease, skončí. Default v controller-runtime.
7. Observability
Loguj namespace/name pri každom reconcile-e. Použite log.FromContext(ctx) — controller-runtime injektuje req do kontextu automaticky.
Anti-patterns
- Reconcile loop s nevracaním
ctrl.Result{}aleboerror— framework si myslí, že všetko OK. Chyba sa "spapá". - Veľký one-shot reconcile — funkcia ktorá robí 100 vecí v riadku bez mezi-stavov. Pri chybe nevieš kde si skončil. Preferuj malé, idempotentné kroky +
conditionsv status-e. - Updatovanie spec-u z controller-a — porušenie princípu (user defines desired, controller observes). Ak potrebujete mutovať spec, použite defaulting webhook.
- Žiadny finalizer na resources, ktoré vyžadujú external cleanup — pri
kubectl deletezostane "leak" na externom systéme. - CRD bez schema validation — user pošle garbage YAML, uloží sa, operator padne. Vždy definujte openAPIV3Schema.
- Reconcile čaká na external I/O synchronne dlhšie ako ~1s — blokuje ostatné reconciles. Preferuj
requeue afterak čakáte na external system. owner referencesnenastavené — keď user zmaže CR, managed resources zostanú orfans.- Bez status-u — user nevie, či operator pracuje alebo je zaseknutý.
conditionsje povinné. - Operator je single-replica bez leader election — HA nie je.
- Operator run-uje
kubectlsubprocessy — použi K8s client library (controller-runtime).kubectlv pode je anti-pattern.
Zhrnutie
Operators sú dnes štandardný spôsob distribúcie "K8s-aware" aplikácií. CRD rozširuje K8s API, controller drží realitu v súlade so spec poľom.
Pre jednoduché stateless aplikácie Helm chart často stačí — nepúšťajte sa do Operator-a bez dôvodu.
Pre stateful aplikácie s lifecycle operations (backup, failover, upgrade) je Operator jediná rozumná cesta. kubebuilder + controller-runtime sú v 2026 de-facto tooling; operator-sdk je v podstate wrapper nad tým istým.
Priority pri stavbe:
- Idempotentný reconcile (nikdy "assume state", vždy observe + adjust).
- Schema validation na CRD (nič nie je "príliš striktné" pre user input).
- Status + conditions (users musia vedieť čo sa deje).
- Finalizers ak máte external state.
- Metriky + leader election od dňa jedna.
S týmto základom je Operator prakticky naveky stabilný.
Ďalšie čítanie
- kubebuilder book — oficiálny tutorial
- controller-runtime docs
- Operator Pattern — K8s docs
- Operator SDK
- OperatorHub.io — distribúcia
- Operator Capability Levels
- chainsaw — E2E testing
- envtest — local testing