Дайджесты новостей
Иллюстрация концепции Server-Side Apply в Kubernetes: API-сервер распределяет права владения полями манифеста и блокирует конфликты между контроллерами.

Server-Side Apply в Kubernetes: как устроен механизм разрешения конфликтов

Представьте типичную ночную аварию в продакшене: внезапный наплыв пользователей перегружает интернет-магазин, и встроенный автомасштабировщик Kubernetes (Horizontal Pod Autoscaler) оперативно увеличивает число реплик бэкенда с 3 до 25. Нагрузка стабилизируется, клиенты оформляют заказы, графики выравниваются. Но ровно через пять минут дежурный инженер выпускает хотфикс и накатывает свежий образ контейнера привычной командой kubectl apply -f deployment.yaml. Через тридцать секунд кластер падает: старый манифест в репозитории содержал строчку replicas: 3, и утилита молча затерла решение автоскейлера, оставив под шквалом запросов всего три пода.

Такие тихие диверсии годами преследовали команды эксплуатации. В распределенной системе один и тот же объект в кластере непрерывно редактируют разные участники: разработчики через GitOps-пайплайны меняют переменные окружения, сервис-меши инжектируют прокси-контейнеры, admission-вебхуки добавляют метки безопасности, а операторы баз данных правят статусы. Классический механизм применения конфигураций не имел понятия о том, кто именно отвечает за конкретную строчку в YAML. Появление Server-Side Apply (SSA) кардинально изменило правила игры, превратив хаотичную перезапись настроек в строгую систему разграничения прав.

Куда исчезают настройки при обычном apply

Чтобы понять ценность нового подхода, стоит заглянуть в изнанку исторического механизма — Client-Side Apply (CSA). Когда инженер или CI-сервер выполнял kubectl apply, вся сложная математика трехстороннего слияния (3-way merge) происходила прямо на рабочей станции или в раннере пайплайна. Утилита сравнивала три состояния: локальный файл на диске, текущий снимок объекта из базы данных etcd и специальную служебную аннотацию kubectl.kubernetes.io/last-applied-configuration.

Именно в этой аннотации пряталась главная архитектурная мина. В нее целиком упаковывался весь предыдущий JSON-манифест. Если объект разрастался — например, сложный Custom Resource с сотнями параметров или многостраничный ConfigMap, — размер аннотации легко упирался в жесткий лимит etcd (обычно 1 МБ на объект, а на практике проблемы начинались уже после 256 КБ). Если же манифест создавался через другой инструмент или аннотация случайно стиралась, клиент терял исторический контекст и не мог понять: поле удалили намеренно или его просто никогда не было в исходном файле?

Главная же проблема заключалась в отсутствии гранулярности. Клиентский apply действовал по принципу «кто последний пришел, того и конфигурация». Если сторонний контроллер изменил параметр, а разработчик просто обновил тег образа в Deployment, клиентская утилита все равно отправляла весь объект целиком, перезаписывая чужие изменения.

Как устроен паспорт манифеста: анатомия managedFields

Server-Side Apply полностью переносит логику слияния на сторону центрального компонента кластера — kube-apiserver. Вместо тяжелых и ненадежных текстовых аннотаций каждый объект Kubernetes обзавелся официальным «паспортом владения» — блоком metadata.managedFields.

Схема префиксного дерева managedFields в Kubernetes, где API-сервер разграничивает права владения ветками манифеста между контроллерами.

Теперь каждый участник, отправляющий запрос к API-серверу, обязан предъявить свое имя — идентификатор fieldManager. Это может быть ci-bot, argocd-controller, kube-controller-manager или конкретная учетная запись администратора. Сервер ведет детальный реестр: какое поле, когда, каким менеджером и в рамках какой операции было изменено.

Внутри managedFields для каждого менеджера формируется компактное префиксное дерево (trie) в формате JSON под ключом fieldsV1. Ключи с префиксом f: указывают на конкретные поля манифеста, находящиеся в собственности данного актора. Если менеджер владеет вложенным объектом, дерево разворачивается вглубь, фиксируя права на каждую отдельную ветку.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: billing-gateway
  namespace: production
spec:
  replicas: 5
  selector:
    matchLabels:
      app: billing-gateway
  template:
    metadata:
      labels:
        app: billing-gateway
    spec:
      containers:
      - name: gateway
        image: cr.internal/finance/gateway:v2.14.0
        ports:
        - containerPort: 8080
          name: http
        resources:
          requests:
            cpu: 250m
            memory: 512Mi
          limits:
            cpu: 1000m
            memory: 1Gi

В этом лаконичном манифесте четко разграничены зоны ответственности. Разработчики сервиса определяют контейнер, используемые порты и лимиты ресурсов, в то время как число реплик может быть начальной точкой, которую впоследствии подхватит контроллер автомасштабирования.

Элегантный арбитраж: как API-сервер ловит ошибку 409

В отличие от старого подхода, Server-Side Apply работает в режиме строгого арбитража. Когда к API-серверу приходит запрос с флагом --server-side, сервер сопоставляет пришедшие поля со списком уже зарегистрированных владельцев.

Если ci-deployer пытается изменить spec.replicas, но этим полем уже безраздельно владеет horizontal-pod-autoscaler, API-сервер не станет молча применять патч. Вместо этого транзакция немедленно блокируется, и клиенту возвращается статус HTTP 409 Conflict. В теле ответа сервер прямо сообщает: поле spec.replicas принадлежит другому менеджеру, операция отменена во избежание сбоя.

# Применение манифеста через Server-Side Apply с явным указанием владельца
kubectl apply -f deployment.yaml --server-side --field-manager=gitops-deployer

# Просмотр детальной карты владельцев полей объекта
kubectl get deployment billing-gateway -n production -o yaml --show-managed-fields

# Попытка стороннего вмешательства в чужие поля вызовет HTTP 409 Conflict
# Если перехват управления осознан, используется принудительная передача прав:
kubectl apply -f deployment.yaml --server-side --field-manager=sre-oncall --force-conflicts

Флаг --force-conflicts — это осознанное управленческое решение. Он сообщает кластеру: «Я понимаю, что этим полем владел другой контроллер, но текущие требования бизнеса важнее, передайте право собственности мне». После успешного выполнения API-сервер переписывает реестр managedFields, назначая новым хозяином указанного менеджера.

При этом поддерживается и совместное владение (shared ownership). Если два независимых инструмента объявляют для скалярного поля одно и то же значение, они оба записываются в соавторы. Если в будущем один из пайплайнов удалит это поле из своей конфигурации, значение не исчезнет из кластера, пока от него не откажется второй совладелец.

Ассоциативные списки и подводные камни слияния

Наибольшую сложность в декларативных системах всегда представляют массивы: список переменных окружения, перечень портов или набор аргументов запуска контейнера. Как объединить два списка, если в одном изменился порядок элементов, а в другой добавился новый пункт?

Kubernetes решает это с помощью OpenAPI v3 схемы и директивы x-kubernetes-list-type. Списки разделяются на два типа:

  1. Атомарные (atomic): список рассматривается как единое неделимое целое. Если менеджер обновляет хотя бы один элемент, весь предыдущий список заменяется новым.
  2. Ассоциативные (map или set): элементы сопоставляются по ключевым атрибутам (x-kubernetes-list-map-keys). Например, для массива контейнеров в поде ключом слияния служит name. Это позволяет разным контроллерам безопасно модифицировать соседние контейнеры в одном поде: GitOps управляет контейнером приложения gateway, а контроллер безопасности добавляет sidecar-контейнер vault-agent, не затирая соседа.

Для разработчиков собственных операторов и Custom Resource Definitions (CRD) это накладывает строгие требования: если в схеме CRD забыть разметить ключи слияния ассоциативных списков, SSA будет трактовать массив как атомарный и перезаписывать пользовательские массивы целиком.

Главное правило перехода на Server-Side Apply

При миграции на Server-Side Apply убедитесь, что GitOps-инструменты и Helm используют фиксированные имена fieldManager. Никогда не включайте в пайплайны динамические идентификаторы менеджеров (например, с добавлением номера сборки или хэша коммита), иначе каждый запуск будет создавать нового владельца и засорять реестр managedFields.

Архитектурный вердикт и готовность к продакшену

Механизм Server-Side Apply прошел долгий путь эволюции: появившись в статусе бета-версии в Kubernetes 1.16, он стал общедоступным стандартом (General Availability) начиная с версии 1.22. В актуальных релизах Kubernetes 1.28–1.32 этот функционал является фундаментальной частью ядра и включен постоянно.

Ведущие экосистемные инструменты декларативного управления — Argo CD, Flux CD, Pulumi и Helm 3 — уже поддерживают SSA в качестве основного или рекомендуемого режима доставки манифестов. Перенос арбитража на сервер не просто предотвращает конфликты конфигураций: он снижает нагрузку на сеть, уменьшает расход оперативной памяти контроллеров при расчете массивных диффов и делает совместную работу десятков автоматизированных агентов прозрачной и безопасной. Командам, до сих пор использующим классический клиентский apply, пора запланировать включение флага --server-side в качестве базового стандарта во всех сборочных линиях.