Перейти к содержимому

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-стека.

  1. Концепция: что такое Operator
  2. Production-deep dive: kubebuilder, controller-runtime, OLM
  3. Gotchas (10+)
  4. Real cases: cert-manager, prometheus-operator, ArgoCD
  5. Вопросы (25+)
  6. Practice
  7. Источники

Идею 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 реагирует на изменения этого ресурса и приводит реальный мир к описанному состоянию.

Принцип Kubernetes — declarative state:

  1. Пользователь говорит «хочу 3 реплики Postgres с версией 15».
  2. Controller сравнивает желаемое (Spec) и текущее (Status) состояние.
  3. Controller выполняет действия (Create/Update/Delete), чтобы их сблизить.
  4. Этот цикл повторяется бесконечно — 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, ...) │
└──────────────────────────────────────┘

Есть два способа расширить Kubernetes API:

СпособCRDAggregated 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).

CRD — это YAML, описывающий схему вашего ресурса (OpenAPI v3 + CEL).

apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: postgresclusters.db.example.com
spec:
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: Namespaced vs Cluster — на каком уровне ресурс существует.

Когда нужно эволюционировать 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.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.

Окно терминала
kubebuilder init --domain example.com --repo github.com/example/postgres-operator
kubebuilder 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.mod
api/v1/postgrescluster_types.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;all
type 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=true
type 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-схемой.

Сердце оператора:

internal/controller/postgrescluster_controller.go
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
}
cmd/main.go
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.go
func (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.

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.PodList
r.List(ctx, &pods, client.MatchingFields{"spec.nodeName": "node-42"})

Без индекса list делает full-scan кеша.

OwnerReference связывает дочерний ресурс с родительским:

if err := ctrl.SetControllerReference(&pgc, &sts, r.Scheme); err != nil {
return fmt.Errorf("set owner: %w", err)
}

Тогда:

  • При удалении pgc Kubernetes удалит и sts (foreground/background cascade).
  • Только один Controller-владелец (Controller: true).
  • Events от дочернего ресурса триггерят Owns().

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 ориентировался контроллер.

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 fields
err := r.Patch(ctx, sts, client.Apply, client.FieldOwner("pgc-operator"), client.ForceOwnership)

SSA устраняет конфликты между несколькими акторами (controller + kubectl edit).

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 + CRDs

OLM популярен в OpenShift и в «продаваемых» через OperatorHub.io операторах. Если оператор внутренний — можно ставить просто kubectl apply -f без OLM.


Gotcha 1: ⚠️ Reconcile запускается чаще, чем кажется

Заголовок раздела «Gotcha 1: ⚠️ Reconcile запускается чаще, чем кажется»

Контроллер реагирует не только на изменение Spec, но и на обновление Status, изменение Annotations, periodic resync (по умолчанию каждые 10 часов). Любая логика «делать ровно один раз» сломается. Используйте predicate.GenerationChangedPredicate{} и/или явный compare текущего и желаемого состояния.

После 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.

Owner и dependent должны быть в одном namespace (если dependent — Namespaced). Иначе garbage collector не сработает. Cluster-scoped → может быть owned by Cluster-scoped, но не Namespaced.

r.Get(ctx, ...) читает из локального информер-кэша, который может отставать от API server. Если вы только что сделали Create, последующий Get может вернуть NotFound. Решения: использовать client.New (no-cache, прямо в API), либо проектировать reconcile идемпотентно к stale-чтениям.

Параллельные Reconcile для разных объектов — нормально, для одного объекта controller-runtime гарантирует не более одного активного Reconcile (через workqueue.Forget). Но если вы делаете кросс-объектные операции вручную — нужны блокировки.

Если 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.

Если оператор крашится в цикле, lease обновляется/освобождается часто, и несколько pod’ов конкурируют за leadership с короткой задержкой. Лучше использовать LeaseDuration: 30s, RenewDeadline: 20s, RetryPeriod: 5s и отдельные probes, которые удалят неработающий pod до того, как новый pod захватит lease.

Если schema пустая (x-kubernetes-preserve-unknown-fields: true повсюду), пользователь может прислать что угодно, и Reconcile упадёт на parse’е. Всегда задавайте required, enum, pattern, minimum/maximum, либо CEL-валидации.


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.

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.

ArgoCD — GitOps-инструмент. CR Application описывает «откуда (Git URL + path) и куда (cluster + namespace) синхронизировать». Контроллер пуллит репо, сравнивает с состоянием в кластере, применяет diff. ApplicationSet генерит много Application по шаблону.

Урок: оператор может быть «control plane»-уровня, оркестрируя другие операторы.

В свежих версиях Istio оператор управляет lifecycle всего mesh: IstioOperator CR описывает компоненты (Pilot, Ingress, Egress), их версии, флаги. При изменении CR оператор делает upgrade. Подробнее в файле 34.

Два промышленных Postgres-оператора. Zalando — простой, использует Patroni для HA. Crunchy — full-featured (PGBackRest, pgBouncer, pgMonitor, TLS). Урок: чем сложнее предметная область, тем больше CRD (Backup, Restore, Replica, Connection, Pooler).


  1. Что такое Operator Pattern и зачем он нужен помимо Deployment/StatefulSet?
  2. Чем CRD отличается от Aggregated API server?
  3. Где хранятся Custom Resources?
  4. Что такое subresource status и зачем он отделён от spec?
  5. Зачем нужен subresource scale?
  6. Что такое apiextensions.k8s.io/v1?
  7. Опишите поток API request → Mutating webhook → Validation → Storage.
  8. Какие правила deprecation policy для CRDs (alpha/beta/stable)?
  9. Что такое conversion webhook?
  10. Опишите цикл Reconcile (Get → Compute → Apply → Status).
  11. В чём разница между For, Owns, Watches в SetupWithManager?
  12. Как работает field indexer и зачем он нужен?
  13. Зачем нужен GenerationChangedPredicate?
  14. Что такое OwnerReference и как срабатывает каскадное удаление?
  15. Что такое finalizer и как правильно его использовать?
  16. Чем отличается r.Update от r.Status().Update?
  17. Что такое Server-Side Apply и зачем нужен FieldOwner?
  18. Как сделать Reconcile идемпотентным?
  19. Что произойдёт, если в Reconcile сделать time.Now() в Spec дочернего ресурса?
  20. Когда controller возвращает Result{RequeueAfter: ...} vs Result{Requeue: true}?
  21. Чем kubebuilder отличается от operator-sdk?
  22. Что такое OLM и какие у него ключевые объекты (CSV, Subscription, CatalogSource)?
  23. Что такое CEL-валидации и в какой версии Kubernetes они стабильны?
  24. Что такое leader election в operator-стеке?
  25. Чем client.New отличается от mgr.GetClient()?
  26. Почему r.Get сразу после r.Create может вернуть NotFound?
  27. Как тестируется оператор через envtest?
  28. Что такое MaxConcurrentReconciles и какие риски при увеличении?
  29. Какие признаки того, что лучше Aggregated API, а не CRD?
  30. Как мигрировать CRD с v1alpha1 на v1 без даунтайма?

  1. Минимальный оператор. С помощью kubebuilder init/create api сделайте CRD Sleeper (Spec: duration), который при создании создаёт Pod, спящий N секунд, и помечает Status: Sleeping/Done.
  2. Finalizer. Расширьте Sleeper: при удалении Sleeper надо логировать в ConfigMap «удалён в HH:MM». Реализуйте finalizer с cleanup.
  3. Conversion webhook. Сделайте v1alpha1 (поле duration int) и v1 (duration string типа 5s). Реализуйте conversion.
  4. CEL-валидация. Добавьте правило «replicas нечётное», «storage size ≤ 100Gi».
  5. Print columns. Сконфигурируйте kubectl get pgc так, чтобы он показывал Version, Replicas, Ready, Age.
  6. OLM bundle. С помощью operator-sdk сгенерируйте OLM-бандл и попробуйте установить через OperatorHub в kind-кластере.
  7. Predicate с label. Сделайте, чтобы Reconcile срабатывал только если на CR есть лейбл manage=true.
  8. SSA-патчинг. Перепишите CreateOrUpdate-логику на Server-Side Apply.
  9. envtest. Напишите тесты, которые поднимают локальный API-server и проверяют, что Reconcile создаёт StatefulSet и Service.
  10. Multi-version. Добавьте v1beta1 → v1, тесты конверсии.

  1. Kubernetes Operator Pattern — официальная документация Kubernetes.
  2. kubebuilder book — основное руководство, отражает текущую структуру проекта.
  3. controller-runtime godoc — API reference.
  4. Operator SDK docs — Red Hat alternative.
  5. Custom Resource Definitions Versioning — официальный guide.
  6. CEL Validation Rules — современная валидация CRD.
  7. Programming Kubernetes (Hausenblas & Schimanski) — O’Reilly, базовая книга.
  8. Kubernetes Operators (Dobies & Wood) — O’Reilly, специально про операторы.
  9. Awesome Operators — список production-операторов.
  10. Server-Side Apply — официальный гайд.
  11. Operator Lifecycle Manager — документация OLM.
  12. Kubebuilder markers reference — список всех +kubebuilder маркеров.