🔴 Advanced

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's ServiceMonitor, Istio's VirtualService)
  • 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:

  1. Watchuje Custom Resource (CR) cez K8s informer.
  2. Pri každej zmene (alebo periodicky) dostane Request s namespace/name.
  3. Fetch-uje aktuálny stav CR a porovnáva s požadovaným (spec).
  4. Vytvorí / updatne / zmaže child resources (Pod, Service, ConfigMap) tak, aby realita zodpovedala spec-u.
  5. Updatne status v 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 backends funguje
  • 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)
  • subresourcesstatus oddelí 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, neinkrementuje generation.

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-sdk ešte vyrába Ansible Operators (reconciliation cez Ansible playbook) a Helm Operators (Helm chart → CR). Vhodné pre jednoduché use-case-y, ale obmedzené.
  • operator-sdkOperator Lifecycle Manager (OLM) integráciu hlbšiu — generovanie ClusterServiceVersion (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í:

  1. Pri prvom reconcile pridať finalizer: metadata.finalizers += ["example.sk/backend-finalizer"].
  2. Keď je deletionTimestamp != nil, spustiť cleanup logiku.
  3. Po úspešnom cleanup-e finalizer odstrániť.
  4. 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_total
  • controller_runtime_reconcile_time_seconds_bucket
  • workqueue_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

  1. Reconcile loop s nevracaním ctrl.Result{} alebo error — framework si myslí, že všetko OK. Chyba sa "spapá".
  2. 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 + conditions v status-e.
  3. Updatovanie spec-u z controller-a — porušenie princípu (user defines desired, controller observes). Ak potrebujete mutovať spec, použite defaulting webhook.
  4. Žiadny finalizer na resources, ktoré vyžadujú external cleanup — pri kubectl delete zostane "leak" na externom systéme.
  5. CRD bez schema validation — user pošle garbage YAML, uloží sa, operator padne. Vždy definujte openAPIV3Schema.
  6. Reconcile čaká na external I/O synchronne dlhšie ako ~1s — blokuje ostatné reconciles. Preferuj requeue after ak čakáte na external system.
  7. owner references nenastavené — keď user zmaže CR, managed resources zostanú orfans.
  8. Bez status-u — user nevie, či operator pracuje alebo je zaseknutý. conditions je povinné.
  9. Operator je single-replica bez leader election — HA nie je.
  10. Operator run-uje kubectl subprocessy — použi K8s client library (controller-runtime). kubectl v 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:

  1. Idempotentný reconcile (nikdy "assume state", vždy observe + adjust).
  2. Schema validation na CRD (nič nie je "príliš striktné" pre user input).
  3. Status + conditions (users musia vedieť čo sa deje).
  4. Finalizers ak máte external state.
  5. Metriky + leader election od dňa jedna.

S týmto základom je Operator prakticky naveky stabilný.


Ďalšie čítanie