Случилось то, чего мы (и не только мы) долго ждали: werf, наша Open Source-утилита для сборки приложений и их доставки в Kubernetes, теперь поддерживает применение изменений с помощью 3-way-merge-патчей! В дополнение к этому, появилась возможность adoption’а существующих K8s-ресурсов в Helm-релизы без пересоздания этих ресурсов.
Если совсем коротко, то ставим
Но давайте начнём с теории: что вообще такое 3-way-merge-патчи, как люди пришли к подходу с их генерацией и почему они важны в CI/CD-процессах с инфраструктурой на базе Kubernetes? А после этого — посмотрим, что же представляет собой 3-way-merge в werf, какие режимы используются по умолчанию и как этим управлять.
Итак, начнем с задачи выката ресурсов, описанных в YAML-манифестах, в Kubernetes.
Для работы с ресурсами Kubernetes API предлагает такие основные операции: create, patch, replace и delete. Предполагается, что с их помощью нужно сконструировать удобный непрерывный выкат ресурсов в кластер. Как?
Первый подход к управлению объектами в Kubernetes — использование императивных команд kubectl для создания, изменения и удаления этих объектов. Проще говоря:
Такой подход может показаться удобным с первого взгляда. Однако есть проблемы:
Понятно, что такой подход плохо сочетается с хранением вместе с кодом приложения и инфраструктуры как кода (IaC; или даже GitOps как более современного варианта, набирающего популярность в Kubernetes-экосистеме). Поэтому дальнейшего развития эти команды в kubectl не получили.
С первичным созданием все просто: отправляем манифест в операцию
С удалением тоже просто: подставляем тот же
Операция
Чтобы хранить конфигурацию в Git и обновлять с помощью replace, надо делать операцию
Также стоит отметить, что хотя
Итого: можем ли мы построить непрерывный выкат только с помощью create, replace и delete, обеспечив хранение конфигурации инфраструктуры в Git’е вместе с кодом и удобный CI/CD?
В принципе, можем… Для этого потребуется реализовать операцию merge манифестов и какую-то обвязку, которая:
При обновлении надо учесть, что ресурс мог поменяться со времени последнего
Однако зачем изобретать велосипед, когда kube-apiserver предлагает другой способ обновления ресурсов: операцию
Вот мы и добрались до патчей.
Патчи — это основной способ применения изменений к существующим объектам в Kubernetes. Операция
Optimistic locking в данном случае не требуется. Эта операция более декларативная по сравнению с replace, хотя сначала может показаться наоборот.
Таким образом:
Однако, чтобы это сделать, необходимо создать правильный патч!
При первой установке релиза Helm выполняет операцию
При обновлении релиза Helm для каждого ресурса:
Такой патч мы будем называть 2-way-merge patch, потому что в его создании участвуют 2 манифеста:
При удалении операция
Подход с 2 way merge patch имеет проблему: он приводит к рассинхрону реального состояния ресурса в кластере и манифеста в Git.
Мы получили рассинхронизацию и потеряли декларативность.
Вообще говоря, полное соответствие манифеста ресурса в работающем кластере и манифеста из Git получить невозможно. Потому что в реальном манифесте могут быть служебные аннотации/лейблы, дополнительные контейнеры и другие данные, добавляемые и удаляемые из ресурса динамически какими-то контроллерами. Эти данные мы не можем и не хотим держать в Git. Однако мы хотим, чтобы при выкате те поля, которые мы явно указали в Git’е, принимали соответствующие значения.
Получается такое общее правило синхронизированного ресурса: при выкате ресурса можно менять или удалять только те поля, которые явно прописаны в манифесте из Git’а (или были прописаны в предыдущей версии, а теперь удалены).
Основная идея 3-way-merge patch: генерируем патч между последней применённой версией манифеста из Git’а и целевой версией манифеста из Git’а с учетом текущей версии манифеста из работающего кластера. Итоговый патч должен соответствовать правилу синхронизированного ресурса:
Именно по такому принципу генерирует патчи
Теперь, когда разобрались с теорией, пора рассказать, что же мы сделали в werf.
Ранее werf, как и Helm 2, использовал 2-way-merge-патчи.
Для того, чтобы перейти на новый вид патчей — 3-way-merge, — первым шагом мы ввели так называемые repair-патчи.
При деплое используется стандартный 2-way-merge-патч, но werf дополнительно генерирует такой патч, который бы синхронизировал реальное состояние ресурса с тем, что написано в Git (создается такой патч с использованием того же правила синхронизированного ресурса, описанного выше).
В случае возникновения рассинхрона, в конце деплоя пользователь получает WARNING с соответствующим сообщением и патчем, который надо применить, чтобы привести ресурс к синхронизированному виду. Также этот патч записывается в специальную аннотацию
Генерация repair-патчей — это временная мера, которая позволяет испытать на деле создание патчей по принципу 3-way-merge, но автоматически эти патчи не применять. На данный момент такой режим работы включен по умолчанию.
Начиная с 1 декабря 2019 г. beta- и alpha-версии werf начинают по умолчанию использовать полноценные 3-way-merge-патчи для применения изменений только для новых Helm-релизов, выкатываемых через werf. Уже существующие релизы продолжат использовать подход с 2-way-merge + repair-патчами.
Данный режим работы можно включить явно настройкой
Примечание: фича появлялась в werf на протяжении нескольких релизов: в альфа-канале она стала готовой с версии v1.0.5-alpha.19, а в бета-канале — с v1.0.4-beta.20.
Начиная с 15 декабря 2019 г. beta- и alpha-версии werf начинают по умолчанию использовать полноценные 3-way-merge-патчи для применения изменений для всех релизов.
Данный режим работы можно включить явно настройкой
В Kubernetes существует 2 типа автомасштабирования: HPA (горизонтальный) и VPA (вертикальный).
Горизонтальный автоматически выбирает количество реплик, вертикальный — количество ресурсов. И количество реплик, и требования к ресурсам указываются в манифесте ресурса (см.
Проблема: если пользователь сконфигурирует ресурс в чарте так, что в нем будут указаны определенные значения по ресурсам или репликам и для данного ресурса будут включены автоскейлеры, то при каждом деплое werf будет сбрасывать эти значения в то, что записано в манифесте чарта.
Решений у проблемы два. Для начала лучше всего отказаться от явного указания автомасштабируемых значений в манифесте чарта. Если же этот вариант по каким-то причинам не подходит (например, потому что в чарте удобно задать начальные ограничения ресурсов и количество реплик), то werf предлагает следующие аннотации:
При наличии такой аннотации werf не будет сбрасывать соответствующие значения при каждом деплое, а лишь установит их при первоначальном создании ресурса.
Подробнее — см. в документации проекта по HPA и VPA.
Пользователь пока может запретить использование новых патчей в werf с помощью переменной окружения
Освоение метода применения изменений 3-way-merge-патчами позволило нам сразу реализовать такую фичу, как adoption существующих в кластере ресурсов в Helm-релиз.
Helm 2 имеет проблему: нельзя добавить в манифесты чарта ресурс, который уже существует в кластере, без пересоздания с нуля этого ресурса (см. #6031, #3275). Мы научили werf принимать существующие ресурсы в релиз. Для этого нужно установить на текущую версию ресурса из работающего кластера аннотацию (например, с помощью
Теперь ресурс нужно описать в чарте и при следующем деплое werf’ом релиза с соответствующим именем существующий ресурс будет принят в этот релиз и останется под его управлением. Более того, в процессе принятия ресурса в релиз werf приведет текущее состояние ресурса из работающего кластера к состоянию, описанному в чарте, используя те же 3-way-merge-патчи и правило синхронизированного ресурса.
Примечание: настройка
Подробности — в документации.
Надеюсь, после этой статьи стало понятнее, что такое 3-way-merge-патчи и почему к ним пришли. С практической точки зрения развития проекта werf их реализация стала еще одним шагом на пути улучшения Helm-подобного деплоя. Теперь можно забыть о проблемах с синхронизацией конфигурации, которые часто возникали при использовании Helm 2. Вместе с тем, была добавлена новая полезная фича adoption’а уже выкаченных Kubernetes-ресурсов в Helm-релиз.
В Helm-подобном деплое по-прежнему остаются некоторые проблемы и трудности, такие как использование Go-шаблонов, и мы продолжим их решать.
Информацию о методах обновления ресурсов и adoption’е можно также найти на этой странице документации.
Отдельного замечания достойна вышедшая буквально на днях новая мажорная версия Helm — v3, — которая также использует 3-way-merge-патчи и избавляется от Tiller. Новая версия Helm требует миграции уже существующих установок, чтобы сконвертировать их в новый формат хранения релизов.
Werf со своей стороны на данный момент уже избавился от использования Tiller, переключился на 3-way-merge и добавил многое другое, при этом оставшись совместимым с уже существующими инсталляциями на Helm 2 (никаких скриптов миграции выполнять не нужно). Поэтому, пока werf не переключен на Helm 3, пользователи werf не теряют основных преимуществ Helm 3 перед Helm 2 (в werf они также есть).
Тем не менее, переключение werf на кодовую базу Helm 3 неизбежно и произойдет в ближайшем будущем. Предположительно это будет werf 1.1 или werf 1.2 (на данный момент, главная версия werf — 1.0; подробнее про устройство версионирования werf см. здесь). За это время Helm 3 успеет стабилизироваться.
Читайте также в нашем блоге:
Если совсем коротко, то ставим
WERF_THREE_WAY_MERGE=enabled
— получаем деплой «как в kubectl apply
», совместимый с существующими инсталляциями на Helm 2 и даже немного больше.Но давайте начнём с теории: что вообще такое 3-way-merge-патчи, как люди пришли к подходу с их генерацией и почему они важны в CI/CD-процессах с инфраструктурой на базе Kubernetes? А после этого — посмотрим, что же представляет собой 3-way-merge в werf, какие режимы используются по умолчанию и как этим управлять.
Что такое 3-way-merge-патч?
Итак, начнем с задачи выката ресурсов, описанных в YAML-манифестах, в Kubernetes.
Для работы с ресурсами Kubernetes API предлагает такие основные операции: create, patch, replace и delete. Предполагается, что с их помощью нужно сконструировать удобный непрерывный выкат ресурсов в кластер. Как?
Императивные команды kubectl
Первый подход к управлению объектами в Kubernetes — использование императивных команд kubectl для создания, изменения и удаления этих объектов. Проще говоря:
- командой
kubectl run
можно запустить Deployment или Job:
kubectl run --generator=deployment/apps.v1 DEPLOYMENT_NAME --image=IMAGE
- командой
kubectl scale
— поменять количество реплик:
kubectl scale --replicas=3 deployment/mysql
- и т.д.
Такой подход может показаться удобным с первого взгляда. Однако есть проблемы:
- Его тяжело автоматизировать.
- Как отразить конфигурацию в Git? Как делать review изменений, происходящих с кластером?
- Как обеспечить воспроизводимость конфигурации при перезапуске?
- …
Понятно, что такой подход плохо сочетается с хранением вместе с кодом приложения и инфраструктуры как кода (IaC; или даже GitOps как более современного варианта, набирающего популярность в Kubernetes-экосистеме). Поэтому дальнейшего развития эти команды в kubectl не получили.
Операции create, get, replace и delete
С первичным созданием все просто: отправляем манифест в операцию
create
у kube api и ресурс создан. YAML-представление манифеста можно хранить в Git, а для создания — использовать команду kubectl create -f manifest.yaml
.С удалением тоже просто: подставляем тот же
manifest.yaml
из Git в команду kubectl delete -f manifest.yaml
.Операция
replace
позволяет полностью заменить конфигурацию ресурса на новую, без пересоздания ресурса. Это означает, что перед тем, как делать изменение в ресурс, логично запросить текущую версию операцией get
, изменить ее и обновить операцией replace
. В kube apiserver встроен optimistic locking и, если после операции get
объект поменялся, то операция replace
не пройдет.Чтобы хранить конфигурацию в Git и обновлять с помощью replace, надо делать операцию
get
, мержить конфиг из Git’а с тем, что мы получили, и выполнять replace
. Штатно kubectl позволяет лишь пользоваться командой kubectl replace -f manifest.yaml
, где manifest.yaml
— уже полностью подготовленный (в нашем случае — смерженный) манифест, который требуется установить. Получается, пользователю необходимо реализовать merge манифестов, а это дело нетривиальное…Также стоит отметить, что хотя
manifest.yaml
и хранится в Git, мы не можем знать заранее, надо создавать объект или обновлять его — это должен делать пользовательский софт.Итого: можем ли мы построить непрерывный выкат только с помощью create, replace и delete, обеспечив хранение конфигурации инфраструктуры в Git’е вместе с кодом и удобный CI/CD?
В принципе, можем… Для этого потребуется реализовать операцию merge манифестов и какую-то обвязку, которая:
- проверяет наличие объекта в кластере,
- выполняет первичное создание ресурса,
- обновляет или удаляет его.
При обновлении надо учесть, что ресурс мог поменяться со времени последнего
get
и автоматически обрабатывать случай optimistic locking — делать повторные попытки обновления.Однако зачем изобретать велосипед, когда kube-apiserver предлагает другой способ обновления ресурсов: операцию
patch
, которая снимает с пользователя часть описанных проблем?Patch
Вот мы и добрались до патчей.
Патчи — это основной способ применения изменений к существующим объектам в Kubernetes. Операция
patch
работает так, что:- пользователю kube-apiserver требуется послать патч в JSON-виде и указать объект,
- а apiserver сам разберется с текущим состоянием объекта и приведет его к требуемому виду.
Optimistic locking в данном случае не требуется. Эта операция более декларативная по сравнению с replace, хотя сначала может показаться наоборот.
Таким образом:
- с помощью операции
create
мы создаем объект по манифесту из Git’а, - с помощью
delete
— удаляем, если объект больше не требуется, - с помощью
patch
— изменяем объект, приводя его к виду, описанному в Git.
Однако, чтобы это сделать, необходимо создать правильный патч!
Как работают патчи в Helm 2: 2-way-merge
При первой установке релиза Helm выполняет операцию
create
для ресурсов чарта.При обновлении релиза Helm для каждого ресурса:
- считает патч между версией ресурса из прошлого чарта и текущей версией чарта,
- применяет этот патч.
Такой патч мы будем называть 2-way-merge patch, потому что в его создании участвуют 2 манифеста:
- манифест ресурса из предыдущего релиза,
- манифест ресурса из текущего ресурса.
При удалении операция
delete
в kube apiserver вызывается для ресурсов, которые были объявлены в прошлом релизе, но не объявлены в текущем.Подход с 2 way merge patch имеет проблему: он приводит к рассинхрону реального состояния ресурса в кластере и манифеста в Git.
Иллюстрация проблемы на примере
- В Git, в чарте хранится манифест, в котором поле
image
у Deployment имеет значениеubuntu:18.04
. - Пользователь через
kubectl edit
поменял значение этого поля наubuntu:19.04
. - При повторном деплое чарта Helm не генерирует патч, потому что поле
image
в предыдущей версии релиза и в текущем чарте одинаковы. - После повторного деплоя
image
остаетсяubuntu:19.04
, хотя в чарте написаноubuntu:18.04
.
Мы получили рассинхронизацию и потеряли декларативность.
Что такое синхронизированный ресурс?
Вообще говоря, полное соответствие манифеста ресурса в работающем кластере и манифеста из Git получить невозможно. Потому что в реальном манифесте могут быть служебные аннотации/лейблы, дополнительные контейнеры и другие данные, добавляемые и удаляемые из ресурса динамически какими-то контроллерами. Эти данные мы не можем и не хотим держать в Git. Однако мы хотим, чтобы при выкате те поля, которые мы явно указали в Git’е, принимали соответствующие значения.
Получается такое общее правило синхронизированного ресурса: при выкате ресурса можно менять или удалять только те поля, которые явно прописаны в манифесте из Git’а (или были прописаны в предыдущей версии, а теперь удалены).
3-way-merge patch
Основная идея 3-way-merge patch: генерируем патч между последней применённой версией манифеста из Git’а и целевой версией манифеста из Git’а с учетом текущей версии манифеста из работающего кластера. Итоговый патч должен соответствовать правилу синхронизированного ресурса:
- новые поля, добавленные в целевую версию, добавляются с помощью патча;
- ранее существовавшие поля в последней применённой версии и не существующие в целевой — обнуляются с помощью патча;
- поля в текущей версии объекта, отличающиеся от целевой версии манифеста, — обновляются с помощью патча.
Именно по такому принципу генерирует патчи
kubectl apply
:- последняя примененная версия манифеста сохраняется в аннотации самого объекта,
- целевая — берется из указанного YAML-файла,
- текущая — из работающего кластера.
Теперь, когда разобрались с теорией, пора рассказать, что же мы сделали в werf.
Применение изменений в werf
Ранее werf, как и Helm 2, использовал 2-way-merge-патчи.
Repair patch
Для того, чтобы перейти на новый вид патчей — 3-way-merge, — первым шагом мы ввели так называемые repair-патчи.
При деплое используется стандартный 2-way-merge-патч, но werf дополнительно генерирует такой патч, который бы синхронизировал реальное состояние ресурса с тем, что написано в Git (создается такой патч с использованием того же правила синхронизированного ресурса, описанного выше).
В случае возникновения рассинхрона, в конце деплоя пользователь получает WARNING с соответствующим сообщением и патчем, который надо применить, чтобы привести ресурс к синхронизированному виду. Также этот патч записывается в специальную аннотацию
werf.io/repair-patch
. Предполагается, что пользователь руками сам применит этот патч: werf его применять не будет принципиально.Генерация repair-патчей — это временная мера, которая позволяет испытать на деле создание патчей по принципу 3-way-merge, но автоматически эти патчи не применять. На данный момент такой режим работы включен по умолчанию.
3-way-merge patch только для новых релизов
Начиная с 1 декабря 2019 г. beta- и alpha-версии werf начинают по умолчанию использовать полноценные 3-way-merge-патчи для применения изменений только для новых Helm-релизов, выкатываемых через werf. Уже существующие релизы продолжат использовать подход с 2-way-merge + repair-патчами.
Данный режим работы можно включить явно настройкой
WERF_THREE_WAY_MERGE_MODE=onlyNewReleases
уже сейчас.Примечание: фича появлялась в werf на протяжении нескольких релизов: в альфа-канале она стала готовой с версии v1.0.5-alpha.19, а в бета-канале — с v1.0.4-beta.20.
3-way-merge patch для всех релизов
Начиная с 15 декабря 2019 г. beta- и alpha-версии werf начинают по умолчанию использовать полноценные 3-way-merge-патчи для применения изменений для всех релизов.
Данный режим работы можно включить явно настройкой
WERF_THREE_WAY_MERGE_MODE=enabled
уже сейчас.Как быть с автомасштабированием ресурсов?
В Kubernetes существует 2 типа автомасштабирования: HPA (горизонтальный) и VPA (вертикальный).
Горизонтальный автоматически выбирает количество реплик, вертикальный — количество ресурсов. И количество реплик, и требования к ресурсам указываются в манифесте ресурса (см.
spec.replicas
или spec.containers[].resources.limits.cpu
, spec.containers[].resources.limits.memory
и другие).Проблема: если пользователь сконфигурирует ресурс в чарте так, что в нем будут указаны определенные значения по ресурсам или репликам и для данного ресурса будут включены автоскейлеры, то при каждом деплое werf будет сбрасывать эти значения в то, что записано в манифесте чарта.
Решений у проблемы два. Для начала лучше всего отказаться от явного указания автомасштабируемых значений в манифесте чарта. Если же этот вариант по каким-то причинам не подходит (например, потому что в чарте удобно задать начальные ограничения ресурсов и количество реплик), то werf предлагает следующие аннотации:
-
werf.io/set-replicas-only-on-creation=true
-
werf.io/set-resources-only-on-creation=true
При наличии такой аннотации werf не будет сбрасывать соответствующие значения при каждом деплое, а лишь установит их при первоначальном создании ресурса.
Подробнее — см. в документации проекта по HPA и VPA.
Запретить использование 3-way-merge patch
Пользователь пока может запретить использование новых патчей в werf с помощью переменной окружения
WERF_THREE_WAY_MERGE_MODE=disabled
. Однако начиная с 1 марта 2020 года данный запрет перестанет работать и возможно будет лишь использование 3-way-merge-патчей.Adoption ресурсов в werf
Освоение метода применения изменений 3-way-merge-патчами позволило нам сразу реализовать такую фичу, как adoption существующих в кластере ресурсов в Helm-релиз.
Helm 2 имеет проблему: нельзя добавить в манифесты чарта ресурс, который уже существует в кластере, без пересоздания с нуля этого ресурса (см. #6031, #3275). Мы научили werf принимать существующие ресурсы в релиз. Для этого нужно установить на текущую версию ресурса из работающего кластера аннотацию (например, с помощью
kubectl edit
):"werf.io/allow-adoption-by-release": RELEASE_NAME
Теперь ресурс нужно описать в чарте и при следующем деплое werf’ом релиза с соответствующим именем существующий ресурс будет принят в этот релиз и останется под его управлением. Более того, в процессе принятия ресурса в релиз werf приведет текущее состояние ресурса из работающего кластера к состоянию, описанному в чарте, используя те же 3-way-merge-патчи и правило синхронизированного ресурса.
Примечание: настройка
WERF_THREE_WAY_MERGE_MODE
не влияет на adoption ресурсов — в случае adoption всегда используется 3-way-merge-патч.Подробности — в документации.
Выводы и дальнейшие планы
Надеюсь, после этой статьи стало понятнее, что такое 3-way-merge-патчи и почему к ним пришли. С практической точки зрения развития проекта werf их реализация стала еще одним шагом на пути улучшения Helm-подобного деплоя. Теперь можно забыть о проблемах с синхронизацией конфигурации, которые часто возникали при использовании Helm 2. Вместе с тем, была добавлена новая полезная фича adoption’а уже выкаченных Kubernetes-ресурсов в Helm-релиз.
В Helm-подобном деплое по-прежнему остаются некоторые проблемы и трудности, такие как использование Go-шаблонов, и мы продолжим их решать.
Информацию о методах обновления ресурсов и adoption’е можно также найти на этой странице документации.
Helm 3
Отдельного замечания достойна вышедшая буквально на днях новая мажорная версия Helm — v3, — которая также использует 3-way-merge-патчи и избавляется от Tiller. Новая версия Helm требует миграции уже существующих установок, чтобы сконвертировать их в новый формат хранения релизов.
Werf со своей стороны на данный момент уже избавился от использования Tiller, переключился на 3-way-merge и добавил многое другое, при этом оставшись совместимым с уже существующими инсталляциями на Helm 2 (никаких скриптов миграции выполнять не нужно). Поэтому, пока werf не переключен на Helm 3, пользователи werf не теряют основных преимуществ Helm 3 перед Helm 2 (в werf они также есть).
Тем не менее, переключение werf на кодовую базу Helm 3 неизбежно и произойдет в ближайшем будущем. Предположительно это будет werf 1.1 или werf 1.2 (на данный момент, главная версия werf — 1.0; подробнее про устройство версионирования werf см. здесь). За это время Helm 3 успеет стабилизироваться.
P.S.
Читайте также в нашем блоге:
- Цикл заметок о нововведениях в werf:
- «werf — наш инструмент для CI/CD в Kubernetes (обзор и видео доклада)»;
- «Сборка и деплой однотипных микросервисов с werf и GitLab CI»;
- «Знакомство с Helm 3».