Server-Side Apply: управление ресурсами в K8s

Введение

В мире Kubernetes управление ресурсами — это нечто большее, чем просто создание, удаление или обновление объектов. Это сложный танец, в котором участвуют множество инструментов, операторов и пользователей. По мере роста инфраструктуры становится всё труднее удерживать контроль, а значит, возникает потребность в более продвинутых и зрелых подходах к управлению ресурсами и их контролю.

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

Обновление

«Как обновить ресурс?» — именно этот вопрос первым приходит в голову каждому разработчику, DevOps-инженеру или любому, кто работал с Kubernetes.

И первый ответ, который напрашивается сам собой, — UPDATE, или, в терминологии Kubernetes, APPLY.

Этот подход совершенно правильный.

Команда kubectl apply — невероятно мощный и удобный инструмент: вы просто изменяете нужную часть манифеста ресурса, применяете его, и Kubernetes делает всё остальное.

Для примера применим следующий манифест деплоймента:

> cat test-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: gpa-app
spec:
replicas: 1
selector:
matchLabels:
app: gpa-app
template:
metadata:
labels:
app: gpa-app
spec:
containers:
- name: gpa-app
image: nginx:1.21.0
ports:
- containerPort: 80
> kubectl apply -f test-deployment.yaml

Сложности начинаются, когда нам нужно автоматизировать регулярную проверку и обновление ресурса — например, инкапсулировав логику внутри микросервиса.

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

  • Любое изменение манифеста, не связанное с основной логикой микросервиса, потребует правки кода, сборки нового образа и повторного развёртывания. Это вносит ненужные простои и операционные неудобства.

  • Если манифест в кластере будет изменён вручную, эти изменения будут перезаписаны устаревшими значениями из внутреннего манифеста микросервиса при следующем цикле применения.

Более надёжное решение — реализовать логику, которая сначала получает (GET) текущее состояние манифеста ресурса, обновляет нужные поля, а затем применяет изменения обратно в кластер:

  • Выполняем команду GET, чтобы получить текущее состояние:

> kubectl get deployment gpa-app -o yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  annotations:
    ...
  creationTimestamp: "2025-08-25T23:43:37Z"
  generation: 1
  name: gpa-app
  namespace: default
  resourceVersion: "495672"
  uid: 2dec1474-988d-431b-9cf2-e8f41a624517
spec:
  replicas: 1
  ...
  selector:
    matchLabels:
      app: gpa-app
  ...
  template:
    metadata:
      creationTimestamp: null
      labels:
        app: gpa-app
    spec:
      containers:
      - image: nginx:1.21.0
        imagePullPolicy: IfNotPresent
        name: gpa-app
        ports:
        - containerPort: 80
          protocol: TCP
        ...
    ...
status:
  ...
  • Очищаем манифест и обновляем количество реплик:

> cat test-deployment-upd.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: gpa-app
spec:
replicas: 2 # новое количество реплик
selector:
matchLabels:
app: gpa-app
template:
metadata:
labels:
app: gpa-app
spec:
containers:
- name: gpa-app
image: nginx:1.21.0
ports:
- containerPort: 80
  • Применяем изменения:

> kubectl apply -f test-deployment-upd.yaml

Это очень популярное решение, которого достаточно для большинства случаев.

Однако мы говорим о больших, сложных и высоконагруженных системах, где каждый лишний запрос к API Kubernetes может обходиться дорого. В приведённом примере мы каждый раз делаем два отдельных запроса: GET, а затем APPLY.

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

Примечание

Это не касается стандартной команды kubectl apply, поскольку утилита сама выполняет валидацию применяемого манифеста, используя информацию из аннотаций ресурса. В частности, она использует аннотацию kubectl.kubernetes.io/last-applied-configuration, чтобы интеллектуально определить, что изменилось, и отправить только необходимые обновления.

Одно из наиболее очевидных решений — сначала проверить наличие изменений и применять новый манифест только в том случае, если ресурс действительно был изменён.

Подводя итог всему сказанному выше, логика реализации микросервиса или оператора для обновления ресурсов должна быть следующей:

  1. Получить текущий манифест ресурса из кластера.

  2. Изменить полученный ресурс.

  3. Сравнить изменённый ресурс с его исходным состоянием и, если он изменился, применить новый манифест.

Назовём этот подход GET-CHECK-APPLY.

Совместное управление ресурсами

Всё, что мы обсудили выше, представляет собой простое и элегантное решение для сценария, где одним ресурсом управляет один микросервис или пользователь. Но что если к изменению этого ресурса подключатся несколько участников?

Вот где кроется основная тема этой статьи: как решить проблемы совместного управления ресурсами вежливо и дипломатично.

Первый очевидный шаг — распределить ответственность за атрибуты между участниками. Например, один сервис может отвечать за отслеживание и обновление образа, а другой — за управление количеством реплик.

«service-a»:

> cat test-deployment-service-a.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: gpa-app
spec:
  ...
  template:
    ...
    spec:
      containers:
      - name: gpa-app
        image: nginx:1.21.0 # принадлежит service-A
        ...

«service-b»:

> cat test-deployment-service-b.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: gpa-app
spec:
replicas: 3 # принадлежит service-B
  ...

К сожалению, подход GET-CHECK-APPLY в этом сценарии не сработает. Поскольку он оперирует манифестом ресурса целиком, при одновременной работе нескольких сервисов может возникнуть коллизия. Конкретнее: в промежутке между шагами GET и APPLY одного сервиса другой сервис может применить свои изменения, которые затем будут перезаписаны финальным APPLY первого сервиса.

PATCH как решение для совместного управления ресурсами

Наиболее прямолинейный и очевидный способ решить проблему совместного управления ресурсами — использовать PATCH. Этот подход хорошо работает по двум основным причинам:

  • Ответственность за поля уже распределена между участниками. Используя PATCH, каждый сервис берёт на себя ответственность за определённый набор полей, предотвращая конфликты. PATCH позволяет точечно обновлять только нужные атрибуты. Вместо того чтобы обновлять весь манифест, можно отправить частичное обновление только с теми полями, которые требуется изменить. Это гораздо эффективнее и исключает перезапись изменений, сделанных другими сервисами.

> cat test-deployment-service-a.yaml
spec:
replicas: 3
> kubectl patch deployment gpa-app --patch-file test-deployment-service-a.yaml
> cat test-deployment-service-b.yaml
spec:
template:
spec:
containers:
- name: gpa-app
image: nginx:1.22.0
> kubectl patch deployment gpa-app --patch-file test-deployment-service-b.yaml
> cat kubectl get deployment gpa-app -o yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  annotations:
    ...
  generation: 4 # один из признаков того, что ресурс изменился
  resourceVersion: "520392"
  name: gpa-app
  ...
spec:
  replicas: 3 # количество реплик изменено service-A
  ...
  template:
    ...
    spec:
      containers:
      - image: nginx:1.22.0 # образ изменён service-B
        ...
    ...
status:
  ...

К сожалению, отказаться от шага GET-CHECK всё равно не получается. Как и APPLY, команда PATCH тоже вызывает смену версии ресурса, что порождает событие и создаёт «шум», мешая нашим «соседям» — другим сервисам и системам.

В итоге мы пришли к выводу, что GET-CHECK-PATCH удобнее, чем GET-CHECK-APPLY, для совместной работы.

Тем не менее, несмотря на эти улучшения, логика по-прежнему выглядит довольно громоздкой:

  • Для обновления ресурса мы всегда делаем два отдельных вызова API: GET и PATCH (или APPLY).

  • Необходимо реализовывать сложную логику сравнения исходного состояния с новым и принятия решения о том, продолжать ли обновление.

В мире Kubernetes этот подход GET-CHECK-PATCH(APPLY) известен как Client-Side Apply (CSA) — клиентское применение, при котором вся логика слияния, разрешения конфликтов и валидации выполняется на стороне клиента, и только финальный результат применяется к кластеру.

Хотя клиент имеет значительный контроль над процессом управления ресурсами, многие инструменты остаются ему недоступны. Например, клиент не может запретить другому клиенту перезаписать набор полей, которыми он владеет.

Server-Side Apply в Kubernetes

Начиная с Kubernetes v1.22 был введён высокоэффективный и мощный декларативный механизм: Server-Side Apply (SSA) — применение на стороне сервера.

SSA существенно упрощает совместное управление ресурсами, перенося ответственность за обновление, валидацию и консолидацию логики на сам API-сервер. Клиент лишь отправляет желаемое состояние ресурса, а Kubernetes API-сервер берёт на себя всю сложную логику под капотом.

Ключевая возможность, введённая вместе с SSA, — механизм совместного управления полями (shared field management). Теперь Kubernetes API-сервер знает, какой клиент управляет каким полем в спецификации ресурса. Когда клиент отправляет манифест через SSA, API-сервер проверяет, владеет ли этот клиент полями, которые он пытается изменить. Если поле никому не принадлежит или уже принадлежит этому клиенту — изменение применяется успешно. Однако если поле принадлежит другому клиенту, API-сервер вернёт ошибку, сигнализируя о конфликте, либо перезапишет владельца — в зависимости от настроек.

Использование SSA полностью устраняет необходимость в подходе GET-CHECK-PATCH(APPLY). С SSA вы отправляете желаемое состояние, указываете имя клиента (field manager — менеджер полей) и получаете ответ от сервера.

Важно отметить, что использование PATCH вместо APPLY всего манифеста по-прежнему остаётся лучшей практикой: так ваш сервис «заявляет» права только на те конкретные поля, которыми он управляет.

Мы можем использовать те же файлы патчей из предыдущего примера — для изменения реплик и образа — и применить их с помощью SSA:

> kubectl patch deployment gpa-app --patch-file test-deployment-service-a.yaml --field-manager=service-a
> kubectl patch deployment gpa-app --patch-file test-deployment-service-b.yaml --field-manager=service-b
> kubectl get deployment gpa-app -o yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  annotations:
    ...
  generation: 6 # один из признаков того, что ресурс изменился
  resourceVersion: "534637"
  name: gpa-app
  ...
spec:
  replicas: 1 # количество реплик изменено service-A
  ...
  template:
    ...
    spec:
      containers:
      - image: nginx:1.21.0 # образ изменён service-B
        ...
    ...
status:
  ...

Чтобы просмотреть список всех управляемых полей, нужно расширить команду kubectl get, добавив флаг --show-managed-fields:

> kubectl get deployment gpa-app -o yaml --show-managed-fields
apiVersion: apps/v1
kind: Deployment
metadata:
  annotations:
    ...
  ...
  managedFields:
  - apiVersion: apps/v1
    fieldsType: FieldsV1
    fieldsV1:
      f:spec:
        f:replicas: {} # подтверждение, что spec.replicas принадлежит service-a
    manager: service-a
    operation: Update
    time: "2025-08-26T00:23:50Z"
  - apiVersion: apps/v1
    fieldsType: FieldsV1
    fieldsV1:
      f:spec:
        f:template:
          f:spec:
            f:containers:
              k:{"name":"gpa-app"}:
                f:image: {} # подтверждение, что spec.template.spec.containers.[0].image принадлежит service-b
    manager: service-b
    operation: Update
    time: "2025-08-26T00:24:05Z"
  ...
  name: gpa-app
  ...
spec:
  ...

Как видно, Kubernetes «закрепил» поле replicas за «service-a», а поле image — за «service-b».

В этом и заключается суть управления полями в SSA. Если теперь попытаться снова применить весь манифест через SSA, Kubernetes вернёт ошибку, обнаружив конфликт:

> kubectl apply -f test-deployment.yaml --field-manager=service-c --server-side
error: Apply failed with 1 conflict: conflict with "service-a" using apps/v1: .spec.replicas
Please review the fields above--they currently have other managers. Here
are the ways you can resolve this warning:
* If you intend to manage all of these fields, please re-run the apply
command with the `--force-conflicts` flag.
* If you do not intend to manage all of the fields, please edit your
manifest to remove references to the fields that should keep their
current managers.
* You may co-own fields by updating your manifest to match the existing
value; in this case, you'll become the manager if the other manager(s)
stop managing the field (remove it from their configuration).
See https://kubernetes.io/docs/reference/using-api/server-side-apply/#conflicts

Система корректно определяет, что новый заявитель не владеет полями, уже закреплёнными за «service-a» и «service-b». Это поведение является ключевым преимуществом SSA: оно предотвращает непреднамеренные перезаписи и гарантирует, что общие ресурсы обновляются совместно и безопасно.

Заключение

Углублённое изучение управления ресурсами Kubernetes показывает: переход от Client-Side Apply к Server-Side Apply — это не просто смена команды. SSA означает фундаментальный сдвиг в философии взаимодействия с кластером. При этом SSA не является серебряной пулей — у него есть свои сложности, и для успешного внедрения требуется глубокое понимание архитектуры Kubernetes.

Долгое время CSA был нашим надёжным инструментом. Он справляется со своей задачей, но имеет определённые ограничения. Зависимость от аннотации kubectl.kubernetes.io/last-applied-configuration делает его уязвимым к конфликтам и ошибкам — особенно в сложных автоматизированных средах. В руках одного разработчика CSA может быть эффективным инструментом для быстрых и простых операций. Однако как только несколько систем или людей пытаются одновременно управлять одним ресурсом, его хрупкость становится очевидной: CSA способен приводить к непредсказуемым результатам, гонкам состояний и, как следствие, нестабильности кластера.

SSA решает эти проблемы, перемещая сложную логику на API-сервер. Механизм управления владением полями меняет правила игры: API-сервер больше не просто исполняет команды — он становится интеллектуальным арбитром, который точно знает, кто отвечает за каждое поле. Это делает совместную работу безопасной, предотвращая случайные перезаписи и конфликты. Для разработчиков операторов и контроллеров SSA — не просто опция, а необходимость. Он открывает возможность создавать надёжные и масштабируемые системы, способные сосуществовать в одном кластере, не мешая друг другу.

Так когда же использовать каждый из подходов?

CSA по-прежнему уместен в сценариях, где вы управляете ресурсами вручную и не ожидаете внешнего вмешательства. Он прост и нетребователен для разовых операций.

SSA — новый стандарт для всех автоматизированных систем, операторов и команд, работающих в высоконагруженных или общих средах. Это путь к по-настоящему декларативному, безопасному и предсказуемому управлению состоянием кластера.

В конечном счёте, понимание обоих подходов — ключ к эффективной и безошибочной работе с Kubernetes. Выбирая Server-Side Apply, вы не просто применяете новую команду — вы принимаете современный, надёжный и более зрелый способ управления своей инфраструктурой.

Спасибо!

© 2026 meganuke