3-way merge в werf: деплой в Kubernetes с Helm «на стероидах»

в 9:39, , рубрики: continuous delivery, devops, helm, kubernetes, werf, Блог компании Флант, системы сборки, Флант

Случилось то, чего мы (и не только мы) долго ждали: werf, наша Open Source-утилита для сборки приложений и их доставки в Kubernetes, теперь поддерживает применение изменений с помощью 3-way-merge-патчей! В дополнение к этому, появилась возможность adoption’а существующих K8s-ресурсов в Helm-релизы без пересоздания этих ресурсов.

3-way merge в werf: деплой в Kubernetes с Helm «на стероидах» - 1

Если совсем коротко, то ставим 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
  • и т.д.

Такой подход может показаться удобным с первого взгляда. Однако есть проблемы:

  1. Его тяжело автоматизировать.
  2. Как отразить конфигурацию в Git? Как делать review изменений, происходящих с кластером?
  3. Как обеспечить воспроизводимость конфигурации при перезапуске?

Понятно, что такой подход плохо сочетается с хранением вместе с кодом приложения и инфраструктуры как кода (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.

Читайте также в нашем блоге:

Автор: Timofey Kirillov

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js