Декларативная модель: reconciliation, контроллеры, CRD

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

Декларативная модель: reconciliation, контроллеры, CRD

Сообщение 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
Ты уже умеешь катить Deployment, скейлить реплики, чинить упавшие поды. Но если остановиться и спросить "а почему вообще оно само чинится?", быстро упрёшься в стену. Ты же нигде не писал код, который перезапускает упавший под. Ты не запускал демон, который следит за числом реплик. Ты просто отдал кластеру YAML - и оно как-то живёт само. Вот это "как-то" и есть главная идея Kubernetes, ради которой он вообще существует. Не контейнеры, не сеть, не балансировщики - а декларативность и петля согласования. Понял эту главу - и весь остальной k8s перестанет быть набором магических заклинаний, станет логичной системой.

Боль, которую это решает, знакома любому, кто администрировал что-то до k8s. Раньше ты писал скрипты-инструкции: сделай ssh, поставь пакет, запусти процесс, если упал - перезапусти, если нагрузка выросла - добавь сервер. Это императивный подход - ты описываешь шаги. Проблема в том, что шаги ломаются от любого расхождения с реальностью. Сервер уже обновлён наполовину, процесс уже запущен, диск кончился на третьем шаге - и скрипт встаёт колом, потому что он не знает, в каком состоянии система сейчас. Kubernetes переворачивает это: ты описываешь не шаги, а результат. "Хочу пять реплик такого образа". А уже система сама разбирается, как из текущего состояния попасть в желаемое, сколько бы раз ты ни просил.

Желаемое состояние против текущего: spec и status

Возьми любой объект Kubernetes и открой его целиком. Почти у каждого есть два больших раздела: spec и status. Это не случайное деление, это ось всей платформы. spec - это желаемое состояние, то, что записал ты (или другой контроллер). status - это текущее наблюдаемое состояние, то, что записывает сам кластер. Ты пишешь в spec, кластер пишет в status. Это разделение - фундамент.

Посмотрим на живой Deployment:

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

kubectl get deployment web -o yaml

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

spec:
  replicas: 5
  selector:
    matchLabels:
      app: web
  template:
    metadata:
      labels:
        app: web
    spec:
      containers:
      - name: web
        image: nginx:1.27
status:
  observedGeneration: 4
  replicas: 5
  updatedReplicas: 5
  readyReplicas: 3
  availableReplicas: 3
  conditions:
  - type: Available
    status: "False"
    reason: MinimumReplicasUnavailable
Разбираем поля, это важно. В spec.replicas стоит 5 - ты хочешь пять. В status.replicas тоже 5 - столько подов реально создано. Но readyReplicas: 3 - готовы только три, два ещё поднимаются или упали по readiness-пробе. observedGeneration: 4 - контроллер увидел и обработал четвёртую версию spec (поле metadata.generation растёт при каждом изменении spec). Если бы observedGeneration отставало от generation, это значило бы, что контроллер ещё не дошёл до твоих последних правок - частая зацепка при отладке "почему мои изменения не применяются". А conditions - это структурированный журнал состояний: Available: False с reason MinimumReplicasUnavailable прямо говорит, что доступных реплик меньше порога. Когда учишься читать status, ты перестаёшь гадать и начинаешь видеть.

Изображение

Reconciliation: петля, которая сводит концы с концами

Теперь главное слово урока - reconciliation, согласование. За каждым типом объекта стоит контроллер - программа в составе kube-controller-manager (или отдельный оператор). Контроллер крутит бесконечный цикл, reconciliation loop, и на каждом обороте делает одно и то же: смотрит spec, смотрит status (то есть реальный мир), вычисляет разницу и предпринимает ровно те действия, которые сокращают эту разницу. Затем засыпает до следующего события и повторяет. Reconciliation в Kubernetes - это и есть тот самый механизм, благодаря которому кластер самовосстанавливается.

Разберём на Deployment с replicas: 3. ReplicaSet-контроллер на каждом обороте считает: сколько подов с нужным селектором живо? Если два - создаёт ещё один. Если четыре (кто-то руками добавил) - удаляет лишний. Если три - не делает ничего. Вот это "если совпало - не делает ничего" критично: контроллер не выполняет команды, он устраняет расхождение. Удали под руками - через долю секунды появится новый, потому что наблюдаемое (2) разошлось с желаемым (3). Никто не "перезапускал под" в привычном смысле; петля просто свела текущее состояние к желаемому. Это уровень контроля - level-based логика, а не edge-based. Контроллеру не важно, сколько событий он пропустил или что именно произошло; ему важно лишь текущее расхождение прямо сейчас. Поэтому он устойчив к потерянным событиям, рестартам и гонкам - в отличие от реакции "пришло событие удаления пода -> создай под", которая ломается, стоит событию потеряться.

Эта же модель объясняет важнейшее свойство: kubernetes декларативность означает, что система сходится к описанию, а не выполняет твою команду один раз. Сервер упал ночью, под уехал на другую ноду, кто-то скейлил вручную - неважно. Петля всё равно вернёт мир к тому, что записано в etcd как желаемое.

Watch и informers: откуда контроллер узнаёт об изменениях

Возникает разумный вопрос: если каждый контроллер на каждом обороте читает состояние, он же закидает API-сервер запросами? Тысячи объектов, десятки контроллеров - API-сервер ляжет от одного только опроса. Так и было бы при наивном polling. Поэтому здесь работает другая механика - watch и informers.

Контроллер не опрашивает API-сервер по кругу. Он открывает один долгоживущий HTTP-стрим - watch - на нужный тип ресурса и получает события (Added, Modified, Deleted) пушем, как только что-то меняется. Поверх watch работает informer - библиотечный компонент, который держит локальный in-memory кэш всех интересных объектов и поддерживает его в актуальном состоянии. Внутри informer'а есть Reflector: он делает первичный LIST (полный снимок), затем переключается на WATCH (поток дельт) и зеркалит всё в локальное хранилище-Store. Все чтения контроллера (Get, List) бьют в этот кэш, а не в API-сервер - вот почему сотни контроллеров не валят кластер.

Что важно для эксплуатации: informer умеет восстанавливаться. Сеть моргнула, watch-соединение порвалось - informer делает re-list и re-watch, заново синхронизируя кэш, и прячет сбой от твоего кода. Из этого вытекает практический нюанс: кэш informer'а eventually consistent, он может на доли секунды отставать от реальности. Поэтому грамотный контроллер пишет изменения в API-сервер, но при конфликте версии (resourceVersion устарел) спокойно ловит ошибку 409 Conflict и идёт на следующий оборот reconciliation - там кэш уже догонит. На события informer навешивают обработчики, которые не делают работу напрямую, а кладут ключ объекта (namespace/name) в рабочую очередь. Очередь дедуплицирует: десять событий по одному объекту схлопнутся в одну задачу reconcile. Это ещё одна причина, почему петля думает категориями "текущее расхождение", а не "обработай каждое событие".

Декларативно против императивно: kubectl apply и идемпотентность

Теперь спустимся к практике, к тому, что ты набираешь в терминале. Есть два стиля работы.

Императивный:

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

kubectl run
,

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

kubectl create deployment
,

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

kubectl scale --replicas=5
,

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

kubectl edit
. Ты отдаёшь команду-действие. Удобно для разовых операций и отладки, но состояние живёт только в кластере, нигде не зафиксировано, и повторный create на существующий объект упадёт с AlreadyExists.

Декларативный: ты держишь YAML-файлы (в git), а в кластер их вкатываешь через kubectl apply.

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

kubectl apply -f deployment.yaml
Команда kubectl apply - идемпотентная: запусти её один раз или сто раз подряд - результат одинаковый. Объекта нет - создаст. Объект есть, но отличается - подгонит под файл. Совпадает - не сделает ничего и скажет unchanged:

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

deployment.apps/web configured
deployment.apps/web unchanged
Эта идемпотентность - не случайность, она прямое следствие декларативной модели: ты описываешь желаемое, а сведение к нему - забота системы. Именно поэтому kubectl apply лежит в основе всего GitOps (Argo CD, Flux): git становится единственным источником истины о желаемом состоянии, а инструмент непрерывно делает apply, удерживая кластер в согласии с репозиторием. По сути GitOps - это reconciliation, поднятая на уровень выше: контроллер сводит кластер к манифестам из git ровно так же, как ReplicaSet сводит число подов к replicas.

Server-side apply и владение полями

Со временем у apply вылез нюанс. Старый клиентский apply хранил предыдущую версию в аннотации kubectl.kubernetes.io/last-applied-configuration и вычислял разницу на твоей машине. Это плохо работало, когда один и тот же объект меняют несколько действующих лиц - ты, HPA (он крутит replicas), оператор. Кто кого затирает - непонятно.

Решение - server-side apply (SSA, стабилен с 1.22), включается флагом:

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

kubectl apply --server-side -f deployment.yaml
Здесь сервер ведёт учёт владения полями в metadata.managedFields: для каждого поля записано, какой "field manager" им управляет.

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

metadata:
  managedFields:
  - manager: kubectl
    operation: Apply
    fieldsV1:
      f:spec:
        f:template:
          f:spec:
            f:containers: {}
  - manager: kube-controller-manager
    operation: Update
    fieldsV1:
      f:spec:
        f:replicas: {}
Читаем: образ контейнеров принадлежит тебе (manager kubectl), а spec.replicas - контроллеру (HPA через controller-manager). Теперь если ты в своём YAML тоже укажешь replicas и сделаешь apply, сервер вернёт конфликт: ты пытаешься перезаписать поле, которым владеет другой. Это спасает от вечной войны "ты выставил 3, HPA вернул 10, ты опять 3". Правильный вывод из этого: не клади replicas в манифест, если им рулит HPA - не заявляй владение тем, чем не управляешь. Конфликт можно продавить флагом

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

--force-conflicts
, но это сознательный захват поля, а не способ заткнуть ошибку.

Всё - объект API. CRD и операторы

Заметь закономерность: Pod, Service, ConfigMap, Deployment - всё это объекты с единообразным устройством (apiVersion, kind, metadata, spec, status), всё читается через API-сервер, всё подчиняется apply и watch. В Kubernetes вообще всё - объект REST API, хранящийся в etcd. Это не деталь реализации, это дизайн: единый протокол для любого ресурса. И раз так - почему бы не добавить свои типы?

Это делают Custom Resource Definitions, CRD. Ты регистрируешь новый kind, и kubernetes начинает обслуживать его как родной: тот же apply, тот же watch, тот же kubectl get. Минимальный CRD:

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

apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
  name: backups.ops.cyberlake.ru
spec:
  group: ops.cyberlake.ru
  scope: Namespaced
  names:
    plural: backups
    singular: backup
    kind: Backup
  versions:
  - name: v1
    served: true
    storage: true
    schema:
      openAPIV3Schema:
        type: object
        properties:
          spec:
            type: object
            properties:
              schedule: {type: string}
              retention: {type: integer}
После

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

kubectl apply -f
этого файла появляется новый ресурс, и можно писать

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

kubectl get backups
. Но сам по себе CRD - это только структура данных в etcd, мёртвая запись. Магии нет, пока кто-то её не читает.

Тут на сцену выходит оператор - это просто контроллер, написанный под твой CRD. Он крутит ту же самую петлю reconciliation: смотрит на spec твоего Backup, смотрит, что реально создано (CronJob, PVC, права), и сводит одно к другому. Оператор PostgreSQL так разворачивает кластер БД с репликами и бэкапами; оператор cert-manager так выпускает и продлевает TLS-сертификаты. CRD плюс оператор - это и есть способ расширить Kubernetes своей предметной логикой, не трогая ядро. В RU-реалиях это повсюду: Yandex Managed Kubernetes и VK Cloud отдают managed-кластеры, а Deckhouse целиком построен на десятках встроенных операторов; даже лёгкий k3s внутри обслуживает CRD ровно так же. CRD kubernetes и операторы превратили платформу из "запускалки контейнеров" в универсальный движок согласования для чего угодно - от баз данных до выписки сертификатов.

Типичные грабли и антипаттерны
  • Мешать императив и декларатив на одном объекте. Сделал kubectl scale руками на объекте, которым управляет apply из git - при следующем apply твой ручной скейл откатится. Источник истины должен быть один.
  • Класть в манифест поля, которыми владеет контроллер. Заявил spec.replicas рядом с активным HPA - получишь либо вечную борьбу (client-side apply), либо конфликт (SSA). Убери поле из манифеста.
  • Ждать мгновенной реакции. Reconciliation асинхронна. apply вернул "configured" - это значит "желаемое записано", а не "уже сделано". Смотри в status и conditions, а не в код возврата команды.
  • Редактировать status руками. status пишет контроллер. Твои правки он сотрёт на следующем обороте. Ты управляешь только через spec.
  • Думать, что под "перезапускает" какой-то демон. Никто не реагирует на конкретное событие падения - петля просто видит расхождение желаемого и текущего. Поймёшь это - перестанешь искать несуществующий "сервис автоперезапуска".
Мини-лаба: пощупать петлю руками

Нужен любой кластер (minikube, kind, k3s).
  • Создай деплой декларативно. Положи в web.yaml Deployment с replicas: 3, образ nginx:1.27, селектор по label app: web. Накати:

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

    kubectl apply -f web.yaml
  • Запусти apply повторно - убедись, что вышло unchanged. Это идемпотентность.
  • Открой наблюдение в одном терминале:

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

    kubectl get pods -l app=web -w
  • В другом терминале удали один под:

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

    kubectl delete pod <имя-пода>
    . Смотри, как в первом окне почти сразу появляется новый - петля свела текущее (2) к желаемому (3).
  • Посмотри расхождение в цифрах:

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

    kubectl get deploy web -o jsonpath='{.spec.replicas} / {.status.readyReplicas}'
  • Сравни управление полями:

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

    kubectl apply --server-side -f web.yaml
    , затем

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

    kubectl get deploy web --show-managed-fields -o yaml
    - найди свой manager в managedFields.
  • Зарегистрируй CRD Backup из урока, сделай apply, проверь

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

    kubectl get crd
    и

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

    kubectl explain backup.spec
    . Оператора нет, поэтому объект просто лежит в etcd - почувствуй разницу между "тип зарегистрирован" и "кто-то его согласует".
Контрольные вопросы
  • Чем spec отличается от status и кто пишет в каждое из этих полей?
  • Почему reconciliation устойчива к потерянным событиям watch - что именно сравнивает контроллер на каждом обороте?
  • Зачем нужны informers и локальный кэш, и какую проблему они снимают по сравнению с прямым опросом API-сервера?
  • Что такое managedFields в server-side apply и как они предотвращают войну за spec.replicas между тобой и HPA?
Итог

Kubernetes - это машина согласования. Ты декларируешь желаемое состояние в spec, контроллеры через watch и informers непрерывно крутят reconciliation loop и сводят наблюдаемое к желаемому, а kubectl apply делает это идемпотентно и (в server-side режиме) с честным учётом владения полями. Всё в кластере - объект API, поэтому CRD и операторы расширяют ту же модель на любую твою предметную область. Усвоишь эту петлю - и перестанешь воспринимать k8s как набор команд: увидишь единый принцип, на котором держится самовосстановление, скейлинг, апдейты и GitOps.
👍1 ❤️3 🔥1 😄 🤔
Аватара пользователя
ansibleuser
Сообщения: 1
Зарегистрирован: 14 май 2026, 04:54

Re: Декларативная модель: reconciliation, контроллеры, CRD

Сообщение ansibleuser »

Дошло наконец почему под сам поднимается после delete - это не какой-то демон-нянька, а просто петля видит что реплик стало меньше чем в spec. Раньше думал что есть отдельный сервис автоперезапуска, искал его в логах ))
👍 ❤️1 🔥 😄 🤔2
Аватара пользователя
Susyq
Сообщения: 1
Зарегистрирован: 25 май 2026, 15:45

Re: Декларативная модель: reconciliation, контроллеры, CRD

Сообщение Susyq »

А можно вопрос про managedFields - если я положил replicas в манифест по привычке и уже накатил, а потом повесил HPA, как теперь по-нормальному отдать ему владение полем? Просто убрать replicas из yaml и сделать apply хватит или надо force-conflicts?
👍1 ❤️ 🔥 😄 🤔
Ответить
← Предыдущая глава
Узел кластера: kubelet, kube-proxy и container runtime
Следующая глава →
kubectl профессионально: get, describe, explain, jsonpath

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

Поделиться темой: ✈ Telegram VK

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

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

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