Kubernetes Operators и CRD на Go
Зачем знать на Middle 3: Operator Pattern — это де-факто стандарт автоматизации stateful-приложений в Kubernetes. Cert-manager, Prometheus Operator, Istio, ArgoCD, Postgres-операторы — всё построено вокруг CRD + controller-runtime. На уровне Middle 3 / Senior от Go-инженера ожидают, что он умеет с нуля проектировать Custom Resource, реализовывать reconcile loop, обрабатывать finalizers, status-updates, и понимать жизненный цикл объектов в кластере. Это знание нужно не только для платформенных команд: любой продуктовый сервис, который пишет infrastructure-as-code или интегрируется с k8s API, требует понимания operator-стека.
Содержание
Заголовок раздела «Содержание»- Концепция: что такое Operator
- Production-deep dive: kubebuilder, controller-runtime, OLM
- Gotchas (10+)
- Real cases: cert-manager, prometheus-operator, ArgoCD
- Вопросы (25+)
- Practice
- Источники
1. Концепция
Заголовок раздела «1. Концепция»1.1 Откуда взялся Operator Pattern
Заголовок раздела «1.1 Откуда взялся Operator Pattern»Идею Operator придумали в CoreOS (2016, до поглощения Red Hat). Постановка задачи была простая: Kubernetes отлично управляет stateless-приложениями через Deployment/StatefulSet, но stateful (БД, очереди, кэши) требуют ручных операций — бэкап, рестор, апгрейд версии, failover. Operator — это software operator (человек-оператор), переложенный в код:
“An Operator is an application-specific controller that extends the Kubernetes API to create, configure, and manage instances of complex stateful applications on behalf of a Kubernetes user.”
Формула:
Operator = CRD (Custom Resource Definition) + Controller (Go-программа в кластере)CRD объявляет новый ресурс (например, PostgresCluster), Controller реагирует на изменения этого ресурса и приводит реальный мир к описанному состоянию.
1.2 Декларативная модель
Заголовок раздела «1.2 Декларативная модель»Принцип Kubernetes — declarative state:
- Пользователь говорит «хочу 3 реплики Postgres с версией 15».
- Controller сравнивает желаемое (Spec) и текущее (Status) состояние.
- Controller выполняет действия (Create/Update/Delete), чтобы их сблизить.
- Этот цикл повторяется бесконечно — reconcile loop.
┌──────────────────────────────────────┐ │ Kubernetes API server │ │ (etcd, watch streams, RBAC) │ └──────────────────────────────────────┘ ▲ │ │ Watch / Update │ Notify │ ▼ ┌──────────────────────────────────────┐ │ Controller │ │ ┌────────────────────────────────┐ │ │ │ Reconcile(ctx, req) │ │ │ │ 1. Get current state │ │ │ │ 2. Compute desired │ │ │ │ 3. Apply diff │ │ │ │ 4. Update status │ │ │ └────────────────────────────────┘ │ └──────────────────────────────────────┘ │ ▼ ┌──────────────────────────────────────┐ │ Реальные ресурсы (Pods, PVCs, ...) │ └──────────────────────────────────────┘1.3 CRD vs Aggregated API
Заголовок раздела «1.3 CRD vs Aggregated API»Есть два способа расширить Kubernetes API:
| Способ | CRD | Aggregated API server |
|---|---|---|
| Реализация | YAML manifest | Полноценный Go API-сервер |
| Хранилище | etcd k8s (через api-server) | Свой etcd / своё хранилище |
| Логика | OpenAPI / CEL validation | Любая Go-логика |
| Конверсии | Conversion webhook | Внутри сервера |
| Сложность | Низкая | Высокая (нужно содержать etcd-кворум) |
| Когда нужен | 95% случаев | Очень сложные API (metrics-server, KEDA v0) |
Все современные операторы используют CRD; Aggregated API почти исчез (раньше так делали metrics.k8s.io, custom.metrics.k8s.io).
1.4 Custom Resource Definition
Заголовок раздела «1.4 Custom Resource Definition»CRD — это YAML, описывающий схему вашего ресурса (OpenAPI v3 + CEL).
apiVersion: apiextensions.k8s.io/v1kind: CustomResourceDefinitionmetadata: name: postgresclusters.db.example.comspec: group: db.example.com names: kind: PostgresCluster plural: postgresclusters singular: postgrescluster shortNames: [pgc] categories: [databases, all] scope: Namespaced versions: - name: v1 served: true storage: true subresources: status: {} scale: specReplicasPath: .spec.replicas statusReplicasPath: .status.replicas labelSelectorPath: .status.selector additionalPrinterColumns: - name: Version type: string jsonPath: .spec.version - name: Replicas type: integer jsonPath: .status.replicas - name: Ready type: string jsonPath: .status.conditions[?(@.type=="Ready")].status - name: Age type: date jsonPath: .metadata.creationTimestamp schema: openAPIV3Schema: type: object properties: spec: type: object required: [version, replicas] properties: version: type: string enum: ["13", "14", "15", "16", "17"] replicas: type: integer minimum: 1 maximum: 9 x-kubernetes-validations: - rule: self % 2 == 1 message: "replicas must be odd" storage: type: object properties: size: type: string pattern: '^[0-9]+(Mi|Gi|Ti)$' status: type: object properties: replicas: type: integer selector: type: string conditions: type: array items: type: object properties: type: {type: string} status: {type: string} lastTransitionTime: {type: string, format: date-time} reason: {type: string} message: {type: string}Ключевые части:
versions[].served— отвечает ли API-сервер этой версии.versions[].storage— какая версия хранится в etcd (только одна).subresources.status— отделяет update метаданных от update spec’а (RBAC отдельно).additionalPrinterColumns— что увидим вkubectl get pgc.x-kubernetes-validations— CEL-выражения (доступны с 1.25+, stable с 1.29).scope: NamespacedvsCluster— на каком уровне ресурс существует.
1.5 Версионирование и conversion webhooks
Заголовок раздела «1.5 Версионирование и conversion webhooks»Когда нужно эволюционировать API, появляется несколько версий:
v1alpha1 → v1beta1 → v1 → v2alpha1Конвенция:
- alpha — может быть удалена в любой момент, не рекомендуется в проде.
- beta — gated, но требует осторожности; по правилам Kubernetes Deprecation Policy beta-фичи в API теперь по умолчанию выключаются.
- stable (v1) — гарантия совместимости 12 месяцев / 3 релизов.
storage: true всегда одна. Если CRD имеет v1alpha1 и v1, и хранится v1, то клиент, запрашивающий v1alpha1, получает результат через conversion webhook. Conversion webhook — это HTTPS endpoint, который умеет переводить объект между версиями.
2. Production-deep dive
Заголовок раздела «2. Production-deep dive»2.1 Инструменты: kubebuilder vs operator-sdk vs controller-runtime
Заголовок раздела «2.1 Инструменты: kubebuilder vs operator-sdk vs controller-runtime» ┌────────────────────────────────────────┐ │ client-go (low-level: informers, │ │ workqueues, clients) │ └────────────────────────────────────────┘ ▲ │ ┌────────────────────────────────────────┐ │ controller-runtime (manager, │ │ Reconciler interface, webhooks) │ └────────────────────────────────────────┘ ▲ │ ┌──────────────────────────────────────────────┐ │ │ ┌──────────────────┐ ┌────────────────────┐ │ kubebuilder │ │ operator-sdk │ │ (scaffolding, │ │ (Red Hat, Helm/ │ │ k8s SIG) │ │ Ansible/Go) │ └──────────────────┘ └────────────────────┘- client-go — низкий уровень: informers, listers, workqueues, restmapper. Используется крайне редко напрямую.
- controller-runtime — библиотека (sigs.k8s.io/controller-runtime), даёт абстракции Manager, Reconciler, Webhook. Это «движок».
- kubebuilder — CLI-генератор скаффолдинга поверх controller-runtime. Создаёт структуру проекта, генерит CRD из Go-структур, билдит Makefile.
- operator-sdk — CLI от Red Hat, под капотом тоже controller-runtime, плюс поддержка Helm-/Ansible-операторов (без Go). Совместим с OLM.
В 2026 году для нового Go-оператора рекомендуется kubebuilder; operator-sdk выбирают, если планируется publish через OperatorHub и интеграция с OLM.
2.2 Структура проекта kubebuilder
Заголовок раздела «2.2 Структура проекта kubebuilder»kubebuilder init --domain example.com --repo github.com/example/postgres-operatorkubebuilder create api --group db --version v1 --kind PostgresClusterЧто мы получаем:
.├── api/│ └── v1/│ ├── groupversion_info.go│ ├── postgrescluster_types.go # Go-структура (источник истины CRD)│ └── zz_generated.deepcopy.go # авто-сгенерированные DeepCopy├── cmd/│ └── main.go # bootstrap Manager├── config/│ ├── crd/ # сгенерированный CRD YAML│ ├── default/│ ├── manager/│ ├── rbac/ # сгенерированный RBAC из //+kubebuilder:rbac│ ├── samples/│ └── webhook/├── internal/│ └── controller/│ ├── postgrescluster_controller.go│ └── suite_test.go├── Dockerfile├── Makefile├── PROJECT└── go.mod2.3 Типы (Go-структуры) — источник истины
Заголовок раздела «2.3 Типы (Go-структуры) — источник истины»package v1
import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1")
// +kubebuilder:object:root=true// +kubebuilder:subresource:status// +kubebuilder:subresource:scale:specpath=.spec.replicas,statuspath=.status.replicas,selectorpath=.status.selector// +kubebuilder:printcolumn:name="Version",type=string,JSONPath=`.spec.version`// +kubebuilder:printcolumn:name="Replicas",type=integer,JSONPath=`.status.replicas`// +kubebuilder:printcolumn:name="Ready",type=string,JSONPath=`.status.conditions[?(@.type=="Ready")].status`// +kubebuilder:resource:shortName=pgc,categories=databases;alltype PostgresCluster struct { metav1.TypeMeta `json:",inline"` metav1.ObjectMeta `json:"metadata,omitempty"`
Spec PostgresClusterSpec `json:"spec,omitempty"` Status PostgresClusterStatus `json:"status,omitempty"`}
type PostgresClusterSpec struct { // +kubebuilder:validation:Enum=13;14;15;16;17 Version string `json:"version"`
// +kubebuilder:validation:Minimum=1 // +kubebuilder:validation:Maximum=9 // +kubebuilder:validation:XValidation:rule="self % 2 == 1",message="replicas must be odd" Replicas int32 `json:"replicas"`
Storage StorageSpec `json:"storage,omitempty"`}
type StorageSpec struct { // +kubebuilder:validation:Pattern=`^[0-9]+(Mi|Gi|Ti)$` Size string `json:"size"` // +optional StorageClassName *string `json:"storageClassName,omitempty"`}
type PostgresClusterStatus struct { Replicas int32 `json:"replicas,omitempty"` Selector string `json:"selector,omitempty"` Conditions []metav1.Condition `json:"conditions,omitempty"` // +optional PrimaryPod string `json:"primaryPod,omitempty"`}
// +kubebuilder:object:root=truetype PostgresClusterList struct { metav1.TypeMeta `json:",inline"` metav1.ListMeta `json:"metadata,omitempty"` Items []PostgresCluster `json:"items"`}
func init() { SchemeBuilder.Register(&PostgresCluster{}, &PostgresClusterList{})}make manifests парсит +kubebuilder: маркеры (controller-tools) и генерирует CRD YAML с правильной OpenAPI/CEL-схемой.
2.4 Reconciler
Заголовок раздела «2.4 Reconciler»Сердце оператора:
package controller
import ( "context" "fmt" "time"
appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/log"
dbv1 "github.com/example/postgres-operator/api/v1")
type PostgresClusterReconciler struct { client.Client Scheme *runtime.Scheme}
const finalizerName = "db.example.com/finalizer"
// +kubebuilder:rbac:groups=db.example.com,resources=postgresclusters,verbs=get;list;watch;create;update;patch;delete// +kubebuilder:rbac:groups=db.example.com,resources=postgresclusters/status,verbs=get;update;patch// +kubebuilder:rbac:groups=db.example.com,resources=postgresclusters/finalizers,verbs=update// +kubebuilder:rbac:groups=apps,resources=statefulsets,verbs=get;list;watch;create;update;patch;delete// +kubebuilder:rbac:groups=core,resources=services;persistentvolumeclaims;configmaps;secrets,verbs=get;list;watch;create;update;patch;delete// +kubebuilder:rbac:groups=core,resources=events,verbs=create;patch
func (r *PostgresClusterReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { logger := log.FromContext(ctx).WithValues("pgc", req.NamespacedName)
// 1. Получаем текущий объект. var pgc dbv1.PostgresCluster if err := r.Get(ctx, req.NamespacedName, &pgc); err != nil { if apierrors.IsNotFound(err) { logger.Info("resource not found, probably deleted") return ctrl.Result{}, nil } return ctrl.Result{}, fmt.Errorf("get pgc: %w", err) }
// 2. Finalizer: гарантируем cleanup перед удалением. if pgc.DeletionTimestamp.IsZero() { if !controllerutil.ContainsFinalizer(&pgc, finalizerName) { controllerutil.AddFinalizer(&pgc, finalizerName) if err := r.Update(ctx, &pgc); err != nil { return ctrl.Result{}, err } } } else { if controllerutil.ContainsFinalizer(&pgc, finalizerName) { if err := r.cleanupExternalResources(ctx, &pgc); err != nil { return ctrl.Result{}, err } controllerutil.RemoveFinalizer(&pgc, finalizerName) if err := r.Update(ctx, &pgc); err != nil { return ctrl.Result{}, err } } return ctrl.Result{}, nil }
// 3. Создаём/обновляем дочерние ресурсы. if err := r.reconcileStatefulSet(ctx, &pgc); err != nil { r.setCondition(&pgc, "Ready", metav1.ConditionFalse, "Reconciling", err.Error()) _ = r.Status().Update(ctx, &pgc) return ctrl.Result{}, err }
if err := r.reconcileService(ctx, &pgc); err != nil { return ctrl.Result{}, err }
// 4. Считаем статус. var sts appsv1.StatefulSet if err := r.Get(ctx, client.ObjectKey{Namespace: pgc.Namespace, Name: pgc.Name}, &sts); err == nil { pgc.Status.Replicas = sts.Status.ReadyReplicas pgc.Status.Selector = fmt.Sprintf("app=%s", pgc.Name)
if sts.Status.ReadyReplicas == pgc.Spec.Replicas { r.setCondition(&pgc, "Ready", metav1.ConditionTrue, "AllReplicasReady", "") } else { r.setCondition(&pgc, "Ready", metav1.ConditionFalse, "WaitingForReplicas", fmt.Sprintf("%d/%d ready", sts.Status.ReadyReplicas, pgc.Spec.Replicas)) } }
if err := r.Status().Update(ctx, &pgc); err != nil { return ctrl.Result{}, fmt.Errorf("update status: %w", err) }
// 5. Периодически пересматриваем (на случай drift). return ctrl.Result{RequeueAfter: 30 * time.Second}, nil}2.5 Manager и SetupWithManager
Заголовок раздела «2.5 Manager и SetupWithManager»mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{ Scheme: scheme, Metrics: server.Options{ BindAddress: ":8080", }, HealthProbeBindAddress: ":8081", LeaderElection: true, LeaderElectionID: "postgres-operator-leader", LeaderElectionResourceLock: "leases", Cache: cache.Options{ DefaultNamespaces: map[string]cache.Config{ "production": {}, "staging": {}, }, },})
if err = (&controller.PostgresClusterReconciler{ Client: mgr.GetClient(), Scheme: mgr.GetScheme(),}).SetupWithManager(mgr); err != nil { setupLog.Error(err, "unable to create controller") os.Exit(1)}
// internal/controller/postgrescluster_controller.gofunc (r *PostgresClusterReconciler) SetupWithManager(mgr ctrl.Manager) error { return ctrl.NewControllerManagedBy(mgr). For(&dbv1.PostgresCluster{}). Owns(&appsv1.StatefulSet{}). Owns(&corev1.Service{}). Owns(&corev1.Secret{}). WithEventFilter(predicate.Or( predicate.GenerationChangedPredicate{}, predicate.LabelChangedPredicate{}, )). WithOptions(controller.Options{ MaxConcurrentReconciles: 5, RateLimiter: workqueue.NewItemExponentialFailureRateLimiter(time.Second, 5*time.Minute), }). Complete(r)}Ключевые элементы:
For— основной тип (триггер реконсиляции).Owns— дочерние ресурсы; их события автоматически триггерят реконсиляцию владельца через OwnerReference.Watches— на чужие ресурсы (например, ConfigMap) с маппингом события вRequest.WithEventFilter / predicates— фильтры.GenerationChangedPredicateсрабатывает только при измененииspec.generation(а не статуса).MaxConcurrentReconciles— параллелизм. Стандарт — 1, но для крупных операторов поднимают до 5-10.
2.6 Predicates и Field Indexers
Заголовок раздела «2.6 Predicates и Field Indexers»Predicates — фильтры в watch:
pred := predicate.NewPredicateFuncs(func(obj client.Object) bool { return obj.GetLabels()["managed-by"] == "pgc-operator"})Field indexers — для эффективного поиска по полям:
mgr.GetFieldIndexer().IndexField(ctx, &corev1.Pod{}, "spec.nodeName", func(o client.Object) []string { return []string{o.(*corev1.Pod).Spec.NodeName} })
// Использование:var pods corev1.PodListr.List(ctx, &pods, client.MatchingFields{"spec.nodeName": "node-42"})Без индекса list делает full-scan кеша.
2.7 OwnerReference и каскадное удаление
Заголовок раздела «2.7 OwnerReference и каскадное удаление»OwnerReference связывает дочерний ресурс с родительским:
if err := ctrl.SetControllerReference(&pgc, &sts, r.Scheme); err != nil { return fmt.Errorf("set owner: %w", err)}Тогда:
- При удалении
pgcKubernetes удалит иsts(foreground/background cascade). - Только один Controller-владелец (
Controller: true). - Events от дочернего ресурса триггерят
Owns().
2.8 Status и Conditions
Заголовок раздела «2.8 Status и Conditions»Status — отдельный subresource. Update spec и update status — две разные RBAC-операции:
// Обновление spec (например, добавление finalizer).r.Update(ctx, &pgc)
// Обновление status — не трогает spec.r.Status().Update(ctx, &pgc)Conditions — стандартный Kubernetes pattern для статуса:
import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
apimeta.SetStatusCondition(&pgc.Status.Conditions, metav1.Condition{ Type: "Ready", Status: metav1.ConditionTrue, Reason: "AllReplicasReady", Message: "5/5 replicas are ready", ObservedGeneration: pgc.Generation,})ObservedGeneration показывает, на какой версии spec ориентировался контроллер.
2.9 Идемпотентность и SSA
Заголовок раздела «2.9 Идемпотентность и SSA»Reconcile должен быть идемпотентен — повторный вызов с теми же входами даёт тот же результат. Это означает использовать CreateOrUpdate / CreateOrPatch или Server-Side Apply (SSA):
op, err := ctrl.CreateOrUpdate(ctx, r.Client, sts, func() error { sts.Spec.Replicas = &pgc.Spec.Replicas sts.Spec.Template.Spec.Containers[0].Image = "postgres:" + pgc.Spec.Version return ctrl.SetControllerReference(&pgc, sts, r.Scheme)})С 1.22+ предпочтителен SSA (apply patch с field manager):
sts := &appsv1.StatefulSet{ ... }sts.SetManagedFields(nil) // SSA не любит чужие managed fieldserr := r.Patch(ctx, sts, client.Apply, client.FieldOwner("pgc-operator"), client.ForceOwnership)SSA устраняет конфликты между несколькими акторами (controller + kubectl edit).
2.10 OLM (Operator Lifecycle Manager)
Заголовок раздела «2.10 OLM (Operator Lifecycle Manager)»OLM — система установки/апгрейда/удаления операторов в кластере, разработанная в Red Hat OpenShift и используемая в OperatorHub. Объекты:
- ClusterServiceVersion (CSV) — манифест оператора (deployment, RBAC, CRDs, Conditions).
- CatalogSource — источник «бандлов» (registry с образами CSV).
- Subscription — пользователь подписался на канал (
stable,alpha). - InstallPlan — конкретный план обновления.
- OperatorGroup — на какие namespaces оператор должен смотреть.
Цикл:
CatalogSource → Subscription → InstallPlan → CSV → Deployment + RBAC + CRDsOLM популярен в OpenShift и в «продаваемых» через OperatorHub.io операторах. Если оператор внутренний — можно ставить просто kubectl apply -f без OLM.
3. Gotchas
Заголовок раздела «3. Gotchas»Gotcha 1: ⚠️ Reconcile запускается чаще, чем кажется
Заголовок раздела «Gotcha 1: ⚠️ Reconcile запускается чаще, чем кажется»Контроллер реагирует не только на изменение Spec, но и на обновление Status, изменение Annotations, periodic resync (по умолчанию каждые 10 часов). Любая логика «делать ровно один раз» сломается. Используйте predicate.GenerationChangedPredicate{} и/или явный compare текущего и желаемого состояния.
Gotcha 2: ⚠️ Status update триггерит ваш же Reconcile
Заголовок раздела «Gotcha 2: ⚠️ Status update триггерит ваш же Reconcile»После r.Status().Update(ctx, &pgc) приходит watch-event, и Reconcile запускается снова. Если внутри Reconcile вы делаете Status().Update без идемпотентности (например, увеличиваете counter), — бесконечный цикл и DDoS на api-server. Решение: обновляйте Status только при реальном изменении (deep-equal до patch’а) или через predicate.GenerationChangedPredicate.
Gotcha 3: ⚠️ Не используйте time.Now() напрямую в Spec/Status
Заголовок раздела «Gotcha 3: ⚠️ Не используйте time.Now() напрямую в Spec/Status»Если в Reconcile при создании ChildResource вы пишете time.Now() в его spec/annotation, то при следующем Reconcile значение «изменится» — это вызовет ненужный update. Используйте детерминированные значения и переустанавливайте только при реальной семантической разнице.
Gotcha 4: ⚠️ Finalizer без cleanup-логики блокирует удаление
Заголовок раздела «Gotcha 4: ⚠️ Finalizer без cleanup-логики блокирует удаление»Добавили finalizer, но забыли логику его снятия — объект застревает с DeletionTimestamp. Чтобы убрать: kubectl patch ... -p '{"metadata":{"finalizers":[]}}' --type=merge. Производство ломается. Всегда тестируйте delete-path в envtest.
Gotcha 5: ⚠️ controller-runtime cache не видит cluster-scoped ресурсы по умолчанию
Заголовок раздела «Gotcha 5: ⚠️ controller-runtime cache не видит cluster-scoped ресурсы по умолчанию»Если Manager.Cache.DefaultNamespaces ограничен namespace’ами, то r.List(ctx, &nodes) вернёт пусто. Cluster-scoped ресурсы (Node, ClusterRole) надо отдельно добавлять в кэш через cache.Options.ByObject.
Gotcha 6: ⚠️ OwnerReference между namespaces запрещён
Заголовок раздела «Gotcha 6: ⚠️ OwnerReference между namespaces запрещён»Owner и dependent должны быть в одном namespace (если dependent — Namespaced). Иначе garbage collector не сработает. Cluster-scoped → может быть owned by Cluster-scoped, но не Namespaced.
Gotcha 7: ⚠️ Maybe Stale Cache vs Live API
Заголовок раздела «Gotcha 7: ⚠️ Maybe Stale Cache vs Live API»r.Get(ctx, ...) читает из локального информер-кэша, который может отставать от API server. Если вы только что сделали Create, последующий Get может вернуть NotFound. Решения: использовать client.New (no-cache, прямо в API), либо проектировать reconcile идемпотентно к stale-чтениям.
Gotcha 8: ⚠️ Concurrency и MaxConcurrentReconciles
Заголовок раздела «Gotcha 8: ⚠️ Concurrency и MaxConcurrentReconciles»Параллельные Reconcile для разных объектов — нормально, для одного объекта controller-runtime гарантирует не более одного активного Reconcile (через workqueue.Forget). Но если вы делаете кросс-объектные операции вручную — нужны блокировки.
Gotcha 9: ⚠️ Webhook latency и FailurePolicy: Fail
Заголовок раздела «Gotcha 9: ⚠️ Webhook latency и FailurePolicy: Fail»Если admission webhook оператора отвечает медленно или падает, и failurePolicy: Fail, то API становится недоступным для всех запросов в области webhook (например, всех Pod’ов). Это уже валило целые кластеры в продакшене. Используйте failurePolicy: Ignore для не-критичных webhook’ов и timeoutSeconds: 5-10.
Gotcha 10: ⚠️ Conversion webhook должен сохранять данные
Заголовок раздела «Gotcha 10: ⚠️ Conversion webhook должен сохранять данные»При конверсии v1alpha1 ↔ v1, поля, которые есть в одной версии и отсутствуют в другой, надо сохранять в annotations (round-trip). Иначе при следующей конверсии данные потеряются. Стандартный приём — annotations типа operator.example.com/preserved-fields.
Gotcha 11: ⚠️ Leader election storm после рестарта
Заголовок раздела «Gotcha 11: ⚠️ Leader election storm после рестарта»Если оператор крашится в цикле, lease обновляется/освобождается часто, и несколько pod’ов конкурируют за leadership с короткой задержкой. Лучше использовать LeaseDuration: 30s, RenewDeadline: 20s, RetryPeriod: 5s и отдельные probes, которые удалят неработающий pod до того, как новый pod захватит lease.
Gotcha 12: ⚠️ CRD без validation === катастрофа
Заголовок раздела «Gotcha 12: ⚠️ CRD без validation === катастрофа»Если schema пустая (x-kubernetes-preserve-unknown-fields: true повсюду), пользователь может прислать что угодно, и Reconcile упадёт на parse’е. Всегда задавайте required, enum, pattern, minimum/maximum, либо CEL-валидации.
4. Real cases
Заголовок раздела «4. Real cases»4.1 cert-manager
Заголовок раздела «4.1 cert-manager»cert-manager автоматически выдаёт и обновляет TLS-сертификаты от ACME (Let’s Encrypt), HashiCorp Vault, Venafi, CA private. Использует CRDs:
Issuer/ClusterIssuer— конфигурация провайдера.Certificate— запрос на сертификат (DNS-names, secret-name).CertificateRequest— низкоуровневая абстракция.Order,Challenge— ACME-протокол.
Внутри — несколько контроллеров, каждый за свой CR. Mutating webhook добавляет default-значения, validating webhook отклоняет невалидные сертификаты.
Уроки: разделение крупного workflow на несколько CR (Certificate → CertificateRequest → Order → Challenge) даёт прозрачность через kubectl get.
4.2 prometheus-operator
Заголовок раздела «4.2 prometheus-operator»prometheus-operator управляет Prometheus, Alertmanager, ThanosRuler:
Prometheus— instance Prometheus с storage, retention.ServiceMonitor/PodMonitor/Probe— селекторы для авто-обнаружения целей.PrometheusRule— recording/alerting правила.Alertmanager/AlertmanagerConfig— конфигурация.
Когда вы создаёте ServiceMonitor, оператор регенерирует prometheus.yaml, кладёт в Secret и triggers reload Prometheus pod’а. Главный урок: операторы могут абстрагировать сложную конфигурацию в декларативные CR.
4.3 ArgoCD
Заголовок раздела «4.3 ArgoCD»ArgoCD — GitOps-инструмент. CR Application описывает «откуда (Git URL + path) и куда (cluster + namespace) синхронизировать». Контроллер пуллит репо, сравнивает с состоянием в кластере, применяет diff. ApplicationSet генерит много Application по шаблону.
Урок: оператор может быть «control plane»-уровня, оркестрируя другие операторы.
4.4 Istio (istio-operator)
Заголовок раздела «4.4 Istio (istio-operator)»В свежих версиях Istio оператор управляет lifecycle всего mesh: IstioOperator CR описывает компоненты (Pilot, Ingress, Egress), их версии, флаги. При изменении CR оператор делает upgrade. Подробнее в файле 34.
4.5 Zalando Postgres Operator vs Crunchy PGO
Заголовок раздела «4.5 Zalando Postgres Operator vs Crunchy PGO»Два промышленных Postgres-оператора. Zalando — простой, использует Patroni для HA. Crunchy — full-featured (PGBackRest, pgBouncer, pgMonitor, TLS). Урок: чем сложнее предметная область, тем больше CRD (Backup, Restore, Replica, Connection, Pooler).
5. Вопросы
Заголовок раздела «5. Вопросы»- Что такое Operator Pattern и зачем он нужен помимо Deployment/StatefulSet?
- Чем CRD отличается от Aggregated API server?
- Где хранятся Custom Resources?
- Что такое subresource
statusи зачем он отделён отspec? - Зачем нужен subresource
scale? - Что такое
apiextensions.k8s.io/v1? - Опишите поток API request → Mutating webhook → Validation → Storage.
- Какие правила deprecation policy для CRDs (alpha/beta/stable)?
- Что такое conversion webhook?
- Опишите цикл Reconcile (Get → Compute → Apply → Status).
- В чём разница между
For,Owns,Watchesв SetupWithManager? - Как работает field indexer и зачем он нужен?
- Зачем нужен
GenerationChangedPredicate? - Что такое OwnerReference и как срабатывает каскадное удаление?
- Что такое finalizer и как правильно его использовать?
- Чем отличается
r.Updateотr.Status().Update? - Что такое Server-Side Apply и зачем нужен
FieldOwner? - Как сделать Reconcile идемпотентным?
- Что произойдёт, если в Reconcile сделать
time.Now()в Spec дочернего ресурса? - Когда controller возвращает
Result{RequeueAfter: ...}vsResult{Requeue: true}? - Чем
kubebuilderотличается отoperator-sdk? - Что такое OLM и какие у него ключевые объекты (CSV, Subscription, CatalogSource)?
- Что такое CEL-валидации и в какой версии Kubernetes они стабильны?
- Что такое leader election в operator-стеке?
- Чем
client.Newотличается отmgr.GetClient()? - Почему
r.Getсразу послеr.Createможет вернуть NotFound? - Как тестируется оператор через
envtest? - Что такое
MaxConcurrentReconcilesи какие риски при увеличении? - Какие признаки того, что лучше Aggregated API, а не CRD?
- Как мигрировать CRD с v1alpha1 на v1 без даунтайма?
6. Practice
Заголовок раздела «6. Practice»- Минимальный оператор. С помощью
kubebuilder init/create apiсделайте CRDSleeper(Spec:duration), который при создании создаёт Pod, спящий N секунд, и помечает Status:Sleeping/Done. - Finalizer. Расширьте Sleeper: при удалении Sleeper надо логировать в ConfigMap «удалён в HH:MM». Реализуйте finalizer с cleanup.
- Conversion webhook. Сделайте v1alpha1 (поле
duration int) и v1 (duration stringтипа5s). Реализуйте conversion. - CEL-валидация. Добавьте правило «replicas нечётное», «storage size ≤ 100Gi».
- Print columns. Сконфигурируйте
kubectl get pgcтак, чтобы он показывал Version, Replicas, Ready, Age. - OLM bundle. С помощью
operator-sdkсгенерируйте OLM-бандл и попробуйте установить через OperatorHub в kind-кластере. - Predicate с label. Сделайте, чтобы Reconcile срабатывал только если на CR есть лейбл
manage=true. - SSA-патчинг. Перепишите CreateOrUpdate-логику на Server-Side Apply.
- envtest. Напишите тесты, которые поднимают локальный API-server и проверяют, что Reconcile создаёт StatefulSet и Service.
- Multi-version. Добавьте v1beta1 → v1, тесты конверсии.
7. Источники
Заголовок раздела «7. Источники»- Kubernetes Operator Pattern — официальная документация Kubernetes.
- kubebuilder book — основное руководство, отражает текущую структуру проекта.
- controller-runtime godoc — API reference.
- Operator SDK docs — Red Hat alternative.
- Custom Resource Definitions Versioning — официальный guide.
- CEL Validation Rules — современная валидация CRD.
- Programming Kubernetes (Hausenblas & Schimanski) — O’Reilly, базовая книга.
- Kubernetes Operators (Dobies & Wood) — O’Reilly, специально про операторы.
- Awesome Operators — список production-операторов.
- Server-Side Apply — официальный гайд.
- Operator Lifecycle Manager — документация OLM.
- Kubebuilder markers reference — список всех
+kubebuilderмаркеров.