Операторы и CRD: расширяем Kubernetes

Рейтинг: 48.7% · 7 голосов
Kubernetes для разработчиков: поды, деплойменты, сервисы, ingress, конфиги и отладка. Уроки по главам с обсуждением.
Ответить
Аватара пользователя
anton_k8s
Сообщения: 55
Зарегистрирован: 12 май 2026, 03:23

Операторы и CRD: расширяем Kubernetes

Сообщение anton_k8s »

Оглавление курса (48)
  1. Зачем нужен Kubernetes и из чего состоит кластер
  2. Поднимаем локальный кластер: minikube и kind
  3. Поды: базовая единица запуска
  4. Deployment и ReplicaSet: управляем репликами
  5. Service: сетевой доступ к подам
  6. ConfigMap и Secret: выносим конфигурацию
  7. Ingress: пускаем трафик снаружи
  8. Хранилище: Volumes и PersistentVolumeClaim
  9. Namespaces, requests и limits
  10. Health checks: liveness и readiness пробы
  11. Отладка: почему под не стартует
  12. Helm: пакетный менеджер для Kubernetes
  13. Базовая безопасность: RBAC и доступы
  14. Job и CronJob: разовые и периодические задачи
  15. StatefulSet и DaemonSet: stateful-нагрузки и системные агенты
  16. Стратегии обновления и планирование: rollout и rollback, graceful shutdown, nodeSelector, affinity, taints
  17. Автомасштабирование: HPA по метрикам, обзор VPA и Cluster Autoscaler
  18. Наблюдаемость: логи, метрики, events, обзор Prometheus и Grafana
  19. Безопасность глубже: securityContext, Pod Security Standards, NetworkPolicy, шифрование секретов
  20. Архитектура control plane: api-server, etcd, scheduler
  21. Узел кластера: kubelet, kube-proxy и container runtime
  22. Декларативная модель: reconciliation, контроллеры, CRD
  23. kubectl профессионально: get, describe, explain, jsonpath
  24. Под глубже: init, sidecar, lifecycle hooks и QoS
  25. Метки, селекторы и организация ресурсов
  26. Сервисы и kube-proxy глубже: типы, IPVS, EndpointSlices
  27. Ingress, ingress-контроллеры и Gateway API
  28. Секреты в кластере: шифрование, External Secrets, Vault
  29. Хранилище глубже: PV, StorageClass, CSI и StatefulSet
  30. Сеть кластера: CNI, NetworkPolicy и CoreDNS
  31. Планировщик: affinity, taints, topology spread
  32. Ресурсы, QoS и вытеснение: requests, limits, eviction
  33. Helm глубже: шаблоны, хуки, зависимости, OCI
  34. Kustomize и управление конфигурацией без шаблонов
  35. RBAC и аутентификация глубже
  36. Admission и Pod Security: контроль на входе
  37. Policy as code: Kyverno и OPA Gatekeeper
  38. GitOps: Argo CD и Flux
  39. Операторы и CRD: расширяем Kubernetes (вы здесь)
  40. Сервис-меш: Istio, Linkerd и когда он нужен
  41. Эксплуатация кластера: апгрейд, узлы, бэкап etcd
  42. Где запускать кластер: managed, self-hosted, k3s, Deckhouse
  43. Траблшутинг кластера: Pending, CrashLoop, узлы NotReady
  44. Стоимость и эффективность кластера: FinOps
  45. CI/CD в Kubernetes: сборка, деплой, прогрессивные релизы
  46. Лучшие практики и антипаттерны Kubernetes
  47. Сквозной проект и путь дальше: от манифеста до прода
  48. Деплой приложений через Argo CD: Application, sync и App-of-Apps
Боль, которую решает kubernetes operator

Ты накатил Postgres в кластер как StatefulSet. Один под, один PVC, все красиво. А потом начинается реальная эксплуатация. Нужна реплика для отказоустойчивости - руками поднимаешь второй под, настраиваешь стриминговую репликацию, прописываешь кто primary. Падает мастер - кто-то ночью должен зайти, понять что primary мертв, промоутить реплику, переключить трафик, не словив split-brain. Раз в сутки бэкап в S3, раз в неделю проверка что бэкап восстанавливается. Обновление минорной версии Postgres - rolling по репликам, потом switchover, потом мастер. Каждая из этих операций - это знания в голове конкретного DBA, записанные в лучшем случае в Confluence, который никто не читает.

Kubernetes из коробки умеет держать заданное количество подов и перезапускать упавшие. Но он не знает, что такое "primary Postgres" и что failover - это не просто перезапустить под, а сложная процедура с проверкой lag реплики. Деплоймент-контроллер тупой в хорошем смысле: ему все равно, что внутри контейнера. А база данных - это stateful-зверь с доменной логикой эксплуатации.

Operator pattern - это способ зашить эту доменную логику прямо в кластер. Оператор - это программа, которая делает то же, что делал бы опытный человек-оператор у пульта: следит за состоянием системы и приводит ее к желаемому. Отсюда и название. Ты описываешь "хочу HA-кластер Postgres на 3 узла с бэкапом в S3" одним YAML, а дальше робот-DBA крутит ручки сам - круглосуточно, без ошибок усталости.

Изображение

Два кита: CRD и контроллер

Оператор стоит на двух вещах: новый тип ресурса (CRD) и программа, которая этим типом управляет (контроллер). Разберем по отдельности, потому что путать их - классическая ошибка новичка.

CustomResourceDefinition (CRD) - это способ научить Kubernetes API новому существительному. Из коробки API знает Pod, Service, Deployment, ConfigMap. CRD добавляет, например, PostgresCluster или Certificate. После того как CRD зарегистрирован, ты можешь делать

Код: Выделить всё

kubectl get postgresclusters
так же, как

Код: Выделить всё

kubectl get pods
. Это полноправный объект в etcd, со своим apiVersion, валидацией схемы, версионированием. Сам по себе CRD - это просто новая таблица в базе. Запись в нее ничего не делает, пока ее некому обрабатывать.

Вот минимальный CRD - определение типа (не путать с экземпляром):

Код: Выделить всё

apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
  name: postgresclusters.acme.example.com
spec:
  group: acme.example.com
  names:
    kind: PostgresCluster
    plural: postgresclusters
    singular: postgrescluster
    shortNames: ["pgc"]
  scope: Namespaced
  versions:
    - name: v1
      served: true
      storage: true
      schema:
        openAPIV3Schema:
          type: object
          properties:
            spec:
              type: object
              properties:
                instances:
                  type: integer
                  minimum: 1
                version:
                  type: string
              required: ["instances"]
            status:
              type: object
              properties:
                readyInstances:
                  type: integer
Разбор ключевых полей. group + versions дают тот самый apiVersion - тут будет acme.example.com/v1. names.kind - как ресурс зовется в манифестах (PostgresCluster), plural - как в URL и в kubectl (postgresclusters). scope: Namespaced - ресурс живет в неймспейсе (альтернатива - Cluster, без привязки). served значит версия отдается API, storage: true - именно в этой версии объект физически хранится в etcd (storage-версия всегда ровно одна). Блок openAPIV3Schema - это валидация: тут apiextensions сам отбракует instances со строкой вместо числа или instances: 0, потому что minimum: 1. Это structural schema, и без нее в v1 CRD просто не создастся - так Kubernetes защищает тебя от мусора в spec.

Обрати внимание на разделение spec и status. spec - это желаемое состояние, его пишет человек. status - фактическое, его пишет контроллер. Это фундамент декларативной модели, к которому мы вернемся в конце.

Теперь экземпляр (custom resource) - вот это и пишет инженер каждый день:

Код: Выделить всё

apiVersion: acme.example.com/v1
kind: PostgresCluster
metadata:
  name: billing-db
spec:
  instances: 3
  version: "16"
Контроллер - вторая половина. Это процесс (обычно сам крутится в кластере подом в Deployment), который через informer следит за объектами PostgresCluster и при любом изменении запускает функцию Reconcile. Логика проста на словах: посмотри, что хотят (spec), посмотри, что есть на самом деле (реальные поды, сервисы, состояние репликации), и сделай разницу нулевой. Хотят 3 инстанса, а живых 2 - подними третий, настрой репликацию, обнови status.readyInstances. Это и есть reconcile loop - бесконечный цикл выравнивания.

Как работает reconcile под капотом

Тут важно понять механику, иначе оператор будет казаться магией. Контроллер не опрашивает API в цикле for. Он использует informer - это локальный кэш плюс подписка на события API-сервера через watch. Когда кто-то создал, изменил или удалил PostgresCluster, API-сервер шлет событие, informer кладет ключ объекта (namespace/name) в рабочую очередь (workqueue). Воркер достает ключ и вызывает твою Reconcile(ctx, req), где req - это просто имя объекта, а не его содержимое.

Из этого вытекают три железных правила, на которых горят все новички.

Reconcile идемпотентна. Один и тот же объект может прийти в reconcile десять раз подряд без изменений - из-за ресинка кэша, рестарта оператора, дребезга. Каждый прогон должен давать один результат. Нельзя писать "создай под" - надо писать "убедись что под существует, если нет - создай". Это и есть выравнивание, а не последовательность шагов.

Reconcile работает по уровню (level-based), а не по событию (edge-based). Тебе не говорят "instances изменился с 2 на 3". Тебе говорят "глянь на billing-db". Ты сам читаешь текущий spec и текущую реальность. Поэтому пропущенное событие не ломает систему: следующий ресинк все равно подтянет актуальное состояние. Это то, чем Kubernetes принципиально надежнее систем на очередях команд.

При ошибке - requeue с backoff. Если Reconcile вернула ошибку или попросила

Код: Выделить всё

return ctrl.Result{RequeueAfter: time.Second*30}, nil
, ключ вернется в очередь. controller-runtime сам делает экспоненциальный backoff на ошибках, чтобы не долбить упавший API. Поэтому "база еще не готова, приду позже" - это нормальный, штатный исход reconcile, а не сбой.

Готовые операторы: бери, не пиши

90% задач уже закрыты зрелыми операторами. Писать свой ради базы - почти всегда ошибка. Что брать в проде на 2026:
  • CloudNativePG (apiVersion postgresql.cnpg.io/v1, kind Cluster) - сейчас де-факто стандарт для Postgres в Kubernetes. Сам рулит репликацией, failover, бэкапом в S3 через barman, point-in-time recovery. Не требует внешних агентов вроде Patroni - логика в самом операторе.
  • CrunchyData PGO (postgres-operator.crunchydata.com/v1beta1, kind PostgresCluster) - второй сильный игрок для Postgres, более "энтерпрайзный".
  • Prometheus Operator - вводит CRD ServiceMonitor, PodMonitor, PrometheusRule. Вместо ручной правки prometheus.yml ты декларативно говоришь "собирай метрики с подов с такими лейблами", и оператор пересобирает конфиг.
  • cert-manager - CRD Certificate, Issuer, ClusterIssuer. Заказываешь TLS-сертификат как объект, оператор сам ходит в Let's Encrypt, проходит ACME-челлендж, кладет сертификат в Secret и продлевает его до истечения. Без него HTTPS в кластере - это боль вручную.
  • etcd-operator, Strimzi (Kafka), Redis Operator - тот же подход для других stateful-систем.
Где их искать - OperatorHub.io и Artifact Hub. Но важная оговорка про эксплуатацию: OperatorHub тащит за собой Operator Lifecycle Manager (OLM), который сам управляет установкой и апдейтами операторов. На многих кластерах OLM не ставят, а операторы катят через Helm-чарт или Kustomize напрямую - это проще и прозрачнее. В Yandex Managed Kubernetes и Deckhouse часть операторов (тот же cert-manager, мониторинг) ставится из коробки или из маркетплейса - проверь, прежде чем городить свое.

Практика: смотрим живой оператор

Возьмем CloudNativePG. Поставили оператор, применили манифест Cluster на 3 инстанса. Смотрим, что появилось:

Код: Выделить всё

kubectl get clusters.postgresql.cnpg.io
NAME         AGE   INSTANCES   READY   STATUS                     PRIMARY
billing-db   4m    3           3       Cluster in healthy state   billing-db-1
Разбор колонок: INSTANCES - сколько хотим (из spec), READY - сколько реально в строю (из status). STATUS - человекочитаемое состояние, которое оператор пишет сам. PRIMARY - кто сейчас мастер. Эти колонки заданы через additionalPrinterColumns в CRD - оператор сам решает, что показать в

Код: Выделить всё

kubectl get
. Под капотом он создал поды, PVC, сервисы (отдельный для записи на primary, отдельный для чтения с реплик), настроил стриминговую репликацию.

Теперь самое интересное - failover. Прибиваем primary и смотрим events объекта:

Код: Выделить всё

kubectl describe cluster billing-db
...
Events:
  Type    Reason            Age   From                Message
  Normal  PrimaryFound      30s   cloudnative-pg      Found primary billing-db-1
  Warning ConnectionFailed  12s   cloudnative-pg      Primary billing-db-1 unreachable
  Normal  PromotingReplica  10s   cloudnative-pg      Promoting billing-db-2 to primary
  Normal  SwitchoverDone    3s    cloudnative-pg      New primary is billing-db-2
Вот тут видно ценность оператора как кода. Он сам обнаружил недоступность, выбрал реплику с наименьшим lag, промоутил ее, переключил сервис записи. Ровно те шаги, что делал бы DBA ночью, только за секунды и без паники. Это reconcile в действии: реальность (primary мертв) разошлась с желаемым (нужен живой primary), оператор выровнял.

Как написать свой оператор: Kubebuilder и reconcile

Иногда готового нет - у тебя своя доменная сущность (например, "TenantNamespace", который должен создавать неймспейс, квоты, сетевые политики и сервис-аккаунт по одному объекту). Тогда пишут свой на Go через Kubebuilder или Operator SDK (внутри оба используют библиотеку controller-runtime - ту самую, что дает informer, workqueue, кэш).

Скелет создается так:

Код: Выделить всё

kubebuilder init --domain example.com --repo example.com/tenant-op
kubebuilder create api --group platform --version v1 --kind TenantNamespace
Эти команды генерят типы Go для spec/status, манифест CRD (тот самый apiextensions.k8s.io/v1) и заглушку контроллера. Тебе остается заполнить одну функцию:

Код: Выделить всё

func (r *TenantNamespaceReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
    var tn platformv1.TenantNamespace
    if err := r.Get(ctx, req.NamespacedName, &tn); err != nil {
        // объект удален - игнорируем NotFound, дочерние ресурсы уберет GC по ownerRef
        return ctrl.Result{}, client.IgnoreNotFound(err)
    }

    // desired: создаем/обновляем неймспейс, ResourceQuota, NetworkPolicy
    if err := r.ensureNamespace(ctx, &tn); err != nil {
        return ctrl.Result{}, err // ошибка -> requeue с backoff
    }

    // пишем фактическое состояние в status
    tn.Status.Ready = true
    if err := r.Status().Update(ctx, &tn); err != nil {
        return ctrl.Result{}, err
    }
    return ctrl.Result{}, nil
}
Два момента, которые отделяют рабочий оператор от учебного. Первое - ownerReference: дочерние ресурсы помечаются ссылкой на владельца, и тогда сборщик мусора Kubernetes сам удалит их при удалении родителя, без кода в reconcile. Второе - удаление через finalizer, если нужна внешняя очистка (снять бэкап, удалить запись в облаке): ставишь finalizer в metadata, и пока он там, объект не удаляется физически - reconcile получает шанс прибрать внешние ресурсы и только потом снять finalizer.

Грабли и антипаттерны из практики

Оператор как черный ящик. Главная опасность. Ты ставишь сторонний оператор и перестаешь понимать, что происходит с базой - он "сам разберется". До первого инцидента, когда failover не сработал, а ты не знаешь даже, какие поды он создает и где логи. Лечение: читай документацию CRD, делай

Код: Выделить всё

kubectl explain cluster.spec
, изучай его events и status, проверь его права в RBAC. Оператор с правами cluster-admin на твою базу - это компонент, который надо понимать не хуже самой базы.

Писать оператор там, где хватит Helm. Если задача - просто разложить набор манифестов с параметрами, тебе нужен Helm или Kustomize, а не оператор. Оператор оправдан, когда есть день-2 операции: автоматический failover, бэкап по расписанию, безопасное обновление stateful-системы, реакция на состояние. Нет день-2 логики - нет смысла в контроллере.

Неидемпотентный reconcile. Код в стиле "создать ресурс" вместо "обеспечить наличие" приводит к дублям и ошибкам AlreadyExists при повторном прогоне. Всегда Get-then-CreateOrUpdate.

Тяжелые блокирующие операции в reconcile. Reconcile должна быть быстрой. Долгие операции (ждать готовности базы 5 минут) делаются через requeue, а не через sleep в коде - иначе воркер залипает и очередь встает.

CRD без версионирования. Поменял схему в проде без conversion webhook - и старые объекты невалидны. Версии CRD (v1alpha1 -> v1) существуют именно для эволюции схемы.

Связь с декларативной моделью

Operator pattern - не отдельная фича, а логичное продолжение того, как устроен весь Kubernetes. Встроенные контроллеры (ReplicaSet, Deployment) работают ровно так же: читают spec, смотрят реальность, выравнивают. CRD плюс контроллер - это просто способ добавить свои существительные и свою логику выравнивания в тот же механизм. Ты не обходишь Kubernetes, ты расширяешь его теми же кирпичами. Поэтому custom resource ведет себя как родной: те же kubectl, RBAC, лейблы, events, тот же GitOps. Положил PostgresCluster в Git, Argo CD синхронизировал - и оператор довел базу до нужного состояния. Декларативность сверху донизу.

Мини-лаба
  • Поставь cert-manager в kind или k3s по официальному манифесту. Посмотри, какие CRD он завел:

    Код: Выделить всё

    kubectl get crd | grep cert-manager
    .
  • Сделай

    Код: Выделить всё

    kubectl explain certificate.spec
    и найди поля dnsNames, issuerRef, secretName - это и есть схема из openAPIV3Schema.
  • Создай ClusterIssuer типа selfSigned и объект Certificate. Через

    Код: Выделить всё

    kubectl describe certificate
    проследи events: как оператор выпустил сертификат и положил его в Secret.
  • Удали Secret с сертификатом. Не создавай заново - просто подожди и посмотри events: reconcile сам обнаружит расхождение и переиздаст. Это level-based выравнивание вживую.
  • Глянь под самого оператора и его логи:

    Код: Выделить всё

    kubectl -n cert-manager logs deploy/cert-manager
    . Найди строки про reconcile конкретного объекта.
Контрольные вопросы
  • В чем разница между CRD и custom resource, и почему один CRD без контроллера бесполезен?
  • Почему reconcile должна быть идемпотентной и level-based, а не реагировать на конкретное событие изменения?
  • Зачем нужны ownerReference и finalizer, и в каком случае ты не можешь обойтись одним ownerReference?
  • По каким признакам решаешь: брать готовый оператор, писать свой или вообще обойтись Helm/Kustomize?
Итог

Operator pattern зашивает знания человека-оператора в кластер: CRD дает новый тип ресурса (свое существительное вроде PostgresCluster), а контроллер в цикле reconcile выравнивает реальность под желаемое - идемпотентно и по уровню, ровно как встроенные контроллеры Kubernetes. Для типовых stateful-систем бери зрелые операторы (CloudNativePG, Prometheus Operator, cert-manager) с OperatorHub, а свой пиши на Kubebuilder только под реальные день-2 операции. И помни главное правило эксплуатации: оператор - не черный ящик, а компонент, который ты обязан понимать так же глубоко, как систему, которой он управляет.
👍3 ❤️ 🔥1 😄 🤔
Аватара пользователя
tufty100
Сообщения: 1
Зарегистрирован: 06 июн 2026, 09:53

Re: Операторы и CRD: расширяем Kubernetes

Сообщение tufty100 »

Долго не доходило, чем CRD отличается от самого ресурса, а тут на примере таблицы и записи в нее наконец щелкнуло. CRD - это тип, а PostgresCluster в моем неймспейсе - это уже экземпляр, спасибо.
👍1 ❤️ 🔥 😄 🤔
Аватара пользователя
quake24
Сообщения: 1
Зарегистрирован: 12 май 2026, 16:36

Re: Операторы и CRD: расширяем Kubernetes

Сообщение quake24 »

Поймал у себя ровно тот антипаттерн: писал в reconcile create вместо ensure и ловил AlreadyExists при каждом ресинке. Идемпотентность и requeue вместо sleep - забрал в закладки.
👍 ❤️ 🔥 😄 🤔1
Ответить
← Предыдущая глава
GitOps: Argo CD и Flux
Следующая глава →
Сервис-меш: Istio, Linkerd и когда он нужен

Все главы курса «Kubernetes: оркестрация контейнеров от основ до продакшена»

Поделиться темой: ✈ Telegram VK
Похожие запросы: minikube или kind что выбрать для локального кластера kuberneteskubectl get describe logs основные команды для работы с кластеромчем отличается deployment от pod в kubernetes простыми словамичто такое service в kubernetes и как поды находят друг другаrequests и limits в kubernetes как правильно задать ресурсы подуnamespace в kubernetes зачем нужен и как разделить ресурсы

Вернуться в «Kubernetes на практике»

Кто сейчас на конференции

Сейчас этот форум просматривают: нет зарегистрированных пользователей и 1 гость