Замена 3-way merge на Server-Side Apply: как werf 2.0 решает проблемы Helm 3
В werf 1.2 для обновления ресурсов в Kubernetes мы использовали механизм под названием 3-way merge. Он достался нам от Helm 3, который мы использовали как подсистему развертывания. Хотя 3-way merge и решил часть проблем, существовавших в 2-way merge, многие проблемы, приводящие к некорректным обновлениям ресурсов, так и остались нерешёнными.
В werf 2.0 и Nelm мы пошли дальше и заменили 3-way merge на более современный механизм обновления ресурсов Kubernetes — Server-Side Apply. Он решает все проблемы 3-way merge и гарантирует корректные обновления ресурсов в кластере при развёртывании.
Подробнее о 3-way merge и 2-way merge читайте в нашей статье от 2019 года.
В этой статье мы расскажем, какие проблемы испытывают пользователи Helm 3 и как Server-Side Apply помогает их преодолеть.
Некорректные обновления ресурсов в Helm 3
Helm 3 и werf 1.2 используют 3-way merge (далее — 3WM) для обновления ресурсов в кластере. Но с 3WM можно нередко обнаружить, что развёрнутые ресурсы не соответствуют тому, что описано в Helm-чарте. Давайте попробуем воспроизвести подобный сценарий.
Допустим, у нас есть Helm-чарт с Deployment:
$ cat chart/templates/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp
spec:
selector:
matchLabels:
app: myapp
template:
metadata:
labels:
app: myapp
spec:
containers:
- name: main
image: nginx
…и хуком Job:
$ cat chart/templates/job.yaml
apiVersion: batch/v1
kind: Job
metadata:
name: myjob
annotations:
helm.sh/hook: "post-install,post-upgrade"
spec:
backoffLimit: 0
template:
spec:
restartPolicy: Never
containers:
- name: main
image: alpine
command: ["echo", "succeeded"]
Сделаем первый релиз, используя последнюю версию Helm 3:
$ helm upgrade --install myapp chart
Release "myapp" has been upgraded. Happy Helming!
Теперь в Deployment чарта заменим один контейнер main на два — backend
и frontend
:
$ cat chart/templates/deployment.yaml
...
containers:
- name: backend
image: nginx
- name: frontend
image: nginx
…и одновременно с этим «случайно» сломаем хук Job:
$ cat chart/templates/job.yaml
...
containers:
- name: main
image: alpine
command: ["fail"]
Делаем второй релиз, в этот раз ожидаемо неудачный:
$ helm upgrade --install myapp chart
Error: UPGRADE FAILED: post-upgrade hooks failed: 1 error occurred:
* job myjob failed: BackoffLimitExceeded
Поняв, что допустили ошибку в Job в чарте, исправим её:
$ cat chart/templates/job.yaml
...
containers:
- name: main
image: alpine
command: ["echo", "succeeded"]
…а заодно, решив, что у новых контейнеров в Deployment не очень удачные названия, переименуем их из backend
и frontend
в app
и proxy
:
$ cat chart/templates/deployment.yaml
...
containers:
- name: app
image: nginx
- name: proxy
image: nginx
Делаем третий, успешный релиз:
$ helm upgrade --install myapp chart
Release "myapp" has been upgraded. Happy Helming!
А теперь проверим, совпадают ли Deployment в чарте и Helm-релизе с Deployment в кластере:
$ cat chart/templates/deployment.yaml
...
containers: # корректно
- name: app
- name: proxy
$ helm get manifest myapp
...
containers: # корректно
- name: app
- name: proxy
$ kubectl get deploy myapp -oyaml
...
containers: # некорректно
- name: app
- name: proxy
- name: backend
- name: frontend
…и обнаружим, что Deployment в чарте/релизе имеет два контейнера, а Deployment в кластере — почему-то четыре: два правильных контейнера app
и proxy
и два старых контейнера frontend
и backend
.
Новый релиз не поможет избавиться от ненужных контейнеров frontend
и backend
:
$ helm upgrade --install myapp chart
$ kubectl get deploy myapp -oyaml
...
containers:
- name: app
- name: proxy
- name: backend
- name: frontend
Rollback до самой первой ревизии тоже не поможет:
$ helm rollback myapp 1
$ kubectl get deploy myapp -oyaml
...
containers:
- name: main
- name: backend
- name: frontend
На этом этапе самый простой способ избавиться от ненужных контейнеров — это удалить их в кластере вручную через kubectl edit.
И этот случай не уникален — примерно то же самое регулярно происходит с большинством полей большинства ресурсов при развертывании с Helm 3. А триггером может выступать не только неудачный, но и отменённый релиз (когда Helm получил сигнал INT
, TERM
или KILL
).
Почему это происходит и что делать
Корень проблемы в том, что если каких-то полей ресурса нет в чарте, но они есть в кластере, то не так-то просто понять, должен Helm удалять эти поля или нет.
Но почему бы тогда просто не удалять всё, чего нет в манифесте ресурса в чарте? Да потому что Kubernetes или Kubernetes-операторы могут вносить в ресурс в кластере такие изменения, которые Helm никогда не должен удалять. Например, если Istio добавляет istio-proxy sidecar-контейнер в Deployment, Helm этот sidecar-контейнер удалять не должен, хотя его и нет в чарте.
Чтобы понять, что делать, Helm должен разделить «лишние» поля (те, которые есть только в ресурсе в кластере, но не в чарте) на «свои» и «чужие». «Свои» поля ему удалять можно, а «чужие» — нельзя. При использовании helm upgrade
«своими» полями считаются все поля, присутствующие в ресурсе нового релиза и предыдущего удачного релиза. Здесь обычно и возникает проблема: что будет, если предыдущий релиз был неудачным или отменённым, то есть был задеплоен не полностью, и при этом в нём произошло что-то важное, например появились новые «свои» поля? Это приведет к тому, что Helm некоторые «свои» поля начнёт расценивать как «чужие», и никогда не будет их удалять.
В итоге, чем чаще релизы заканчиваются неудачно или отменяются, тем больше «осиротевших» полей можно обнаружить на ресурсах в кластере. Иногда это что-то безобидное, а иногда это может быть что-то, приводящее к отказу в обслуживании или даже к порче/потере данных.
В рамках Helm простого решения для этой проблемы нет. Одним из способов могла бы быть новая схема Helm-релизов, где мы для каждого отдельного ресурса фиксировали бы его последнее применённое состояние. Но есть способ лучше — использование Server-Side Apply вместо 3WM.
Что такое Server-Side Apply
В Kubernetes 1.22 включена поддержка нового способа обновления ресурсов в кластере, который называется Server-Side Apply (далее — SSA). Давайте сравним обновление ресурсов через 3WM с обновлением через SSA.
Чтобы обновить ресурс через 3WM, нужно выполнить следующие шаги:
Взять манифест ресурса из последнего удачного релиза.
Взять манифест ресурса из чарта.
Взять манифест ресурса из кластера.
На основе этих трёх манифестов составить 3WM-патч.
Отправить в Kubernetes HTTP PATCH-запрос с 3WM-патчем.
Чтобы обновить ресурс через SSA, необходимо выполнить всего лишь две операции:
Взять манифест ресурса из чарта.
Отправить в Kubernetes HTTP PATCH-запрос с манифестом ресурса.
Преимущества SSA:
Простота использования.
Не нужен манифест ресурса из последнего релиза, который иногда может быть недостоверным.
Не надо вычислять, какие поля ресурса «свои», а какие нет — Kubernetes сам отслеживает это в поле
managedFields
ресурса.Обновление ресурса и сохранение информации о «своих» полях — это одна атомарная операция.
Если бы мы сумели заменить 3WM на SSA в Helm, нам бы больше не понадобилось обращаться к манифестам предыдущих релизов — за исключением случаев, когда нам необходимо понять, какие ресурсы надо удалить целиком, если они уже были удалены из чарта. Это бы полностью решило проблемы с «осиротевшими» полями в ресурсах в кластере.
Поддержка Server-Side Apply в Helm, werf и других инструментах
Flux, ArgoCD, kubectl/kustomize уже имеют поддержку SSA, хотя по умолчанию пока она включена только во Flux. За внедрение SSA в Helm 3 так никто и не взялся, даже несмотря на то, что поддержка SSA была уже в Kubernetes 1.16 (включалась через feature gate
), а в Kubernetes 1.22 она и вовсе была включена по умолчанию.
В werf 2.0 мы разработали и внедрили новый движок развёртывания Nelm, который пришёл на смену Helm 3. В Nelm, помимо всего прочего, мы полностью заменили 3WM на SSA. Внедрение SSA позволило решить ещё целый ряд проблем вроде этой (до сих пор существует в Helm, а появилась ещё в версии 2). Также SSA позволил реализовать несколько фич вроде автоматического сброса при новом релизе тех изменений ресурсов, которые были внесены вручную через kubectl edit
.
SSA в экспериментальном режиме был внедрён уже в werf 1.2 и эксплуатировался (в том числе в production) более года. Все пользователи werf 2.0 уже используют SSA по умолчанию, все проблемы 3WM исчезли, и на этом этапе мы рекомендуем werf 2.0 и SSA к использованию в production.
Для пользователей werf 1.2: миграция на werf 2.0 очень проста, и если не учитывать более строгую валидацию Helm-чартов, то в них почти ничего менять не нужно.
P. S.
Читайте также в нашем блоге: