company_banner

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

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



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


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

    • +37
    • 2,5k
    • 2
    Флант
    557,82
    Специалисты по DevOps и Kubernetes
    Поделиться публикацией

    Комментарии 2

      0

      Получается, что там где используется helm3 с werf лучше не играться?

        0

        Почему? Они друг другу не должны мешать.

      Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

      Самое читаемое