Трезвый взгляд на Helm 2: «Вот такой, какой есть...» +34


Как и любое другое решение, Helm — пакетный менеджер для Kubernetes — имеет плюсы, минусы и область применения, поэтому при его использовании стоит правильно оценивать свои ожидания…


Мы используем Helm в своём арсенале инструментов непрерывного выката. На момент написания статьи в наших кластерах более тысячи приложений и примерно 4000 инсталляций этих приложений в различных вариациях. Периодически мы сталкиваемся с проблемами, но в целом довольны решением, не имеем простоев и потерь данных.

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

[BUG] После выката состояние ресурсов релиза в кластере не соответствуют описанному Helm-чарту


При работе Helm не учитывает состояние ресурсов релиза в кластере. При повторной инсталляции результат определяется только текущей и сохранённой конфигурациями. Таким образом, состояние ресурса в кластере и реестре Helm различается, а Helm это никак не учитывает.

Рассмотрим, как данная проблема проявляется:

  1. Шаблон ресурса в чарте соответствует состоянию X.
  2. Пользователь выполняет инсталляцию чарта (Tiller сохраняет состояние ресурса X).
  3. Далее пользователь вручную меняет ресурс в кластере (состояние изменяется с X на Y).
  4. Не делая никаких изменений, он выполняет helm upgrade… И ресурс по-прежнему в состоянии Y, хотя пользователь ожидает X.

И это ещё не всё. В какой-то момент пользователь меняет шаблон ресурса в чарте (новое состояние W) — тогда мы имеем два сценария после выполнения helm upgrade:

  • Падает применение патча X-W.
  • После применение патча ресурс переходит в состояние Z, которое не соответствует желаемому.

Для избежания подобной проблемы предлагается организовать работу с релизами следующим образом: никто не должен изменять ресурсы вручную, Helm — единственный инструмент для работы с ресурсами релиза. В идеале, изменения чарта версионируются в Git-репозитории и применяются исключительно в рамках CD.

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

  1. Узнаём состояния ресурсов релиза через helm get.
  2. Узнаём состояния ресурсов в Kubernetes через kubectl get.
  3. Если ресурсы отличаются, то проводим синхронизацию Helm с Kubernetes:
    1. Создаём отдельную ветку.
    2. Обновляем манифесты чарта. Шаблоны должны соответствовать состояниям ресурсов в Kubernetes.
    3. Выполняем деплой. Синхронизируем состояние в реестре Helm и кластере.
    4. После этого ветку можно удалить и продолжить штатную работу.

При применении патчей с использованием команды kubectl apply выполняется так называемый 3-way-merge, т.е. учитывается реальное состояние обновляемого ресурса. Посмотреть код алгоритма можно здесь, а почитать про него — здесь.

На момент написания статьи разработчики Helm ищут пути внедрения 3-way-merge в Helm 3. С Helm 2 не всё так радужно: 3-way-merge внедрять не планируется, но есть PR для исправления способа создания ресурсов — узнать подробности или даже поучаствовать можно в рамках соответствующего issue.

[BUG] Error: no RESOURCE with the name NAME found


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

При неудавшемся выкате релиз сохраняется в реестре с пометкой FAILED, а при инсталляции Helm опирается на состояние последнего релиза DEPLOYED, который в данном случае ничего не знает о новых ресурсах. В результате, Helm пытается повторно создать эти ресурсы и завершается с ошибкой «no RESOURCE with the name NAME found» (ошибка говорит об обратном, но проблема именно в этом). Отчасти проблема связана с тем, что Helm не учитывает состояние ресурсов релиза в кластере при создании патча, о чём написано в предыдущем разделе.

В настоящий момент единственным решением будет удаление новых ресурсов вручную.

Для того, чтобы избежать подобного состояния, можно автоматически удалять новые ресурсы, созданные в текущем upgrade/rollback, если команда в итоге завершается с ошибкой. После долгого обсуждения с разработчиками в Helm для команд upgrade/rollback добавили опцию --cleanup-on-fail, которая активирует автоматическую очистку при неудачном выкате. Наш PR находится на стадии обсуждения, поиска наилучшего решения.

Начиная с версии Helm 2.13 в командах helm install/upgrade появляется опция --atomic, которая активирует очистку и rollback при провальной инсталляции (подробнее см. в PR).

[BUG] Error: watch closed before Until timeout


Проблема может возникать, когда Helm hook выполняется слишком долго (например, при миграциях) — даже несмотря на то, что не превышаются указанные timeout'ы у helm install/upgrade, а также spec.activeDeadlineSeconds у соответствующего Job.

Такую ошибку выдаёт API-сервер Kubernetes во время ожидания выполнения hook job. Helm не обрабатывает эту ошибку и сразу падает — вместо того, чтобы повторить запрос на ожидание.

В качестве решения можно увеличить таймаут в api-server: --min-request-timeout=xxx в файле /etc/kubernetes/manifests/kube-apiserver.yaml.

[BUG] Error: UPGRADE FAILED: «foo» has no deployed releases


Если первый релиз через helm install завершился с ошибкой, то последующий helm upgrade вернёт подобную ошибку.

Казалось бы, решение довольно простое: нужно вручную выполнить helm delete --purge после провальной первой инсталляции, но это ручное действие ломает автоматику CI/CD. Чтобы не прерываться на выполнение ручных команд, можно использовать возможности werf для выката. При использовании werf проблемный релиз будет автоматически пересоздан при повторной инсталляции.

Кроме того, начиная с версии Helm 2.13 в командах helm install и helm upgrade --install достаточно указать опцию --atomic и после провальной инсталяции релиз автоматически удалится (подробности см. в PR).

Autorollback


В Helm не хватает опции --autorollback, которая при выкате запомнит текущую успешную ревизию (упадёт, если последняя ревизия не успешна) и после неуспешной попытки деплоя выполнит rollback до сохранённой ревизии.

Так как для продуктива критично, чтобы всё работало без перебоев, необходимо искать решения, выкат должен быть предсказуемым. Для того, чтобы минимизировать вероятность простоя продуктива, часто используется подход с несколькими контурами (к примеру, staging, qa и production), который заключается в последовательном выкате на контуры. При таком подходе большинство проблем фиксируется до выката в продуктив и в связке с autorolback’ом позволяет достигать неплохих результатов.

Для организации autorollback можно использовать плагин helm-monitor, который позволяет завязать rollback на метрики из Prometheus. Неплохая статья с описанием этого подхода доступна здесь.

Для некоторых наших проектов используется достаточно простой подход:

  1. Перед деплоем запоминаем текущую ревизию (считаем, что в нормальной ситуации, если релиз существует, то он обязательно в состоянии DEPLOYED):

    export _RELEASE_NAME=myrelease
    export _LAST_DEPLOYED_RELEASE=$(helm list -adr | \ 
     grep $_RELEASE_NAME | grep DEPLOYED | head -n2 | awk '{print $2}')
  2. Запускаем install или upgrade:

    helm install/upgrade ... || export _DEPLOY_FAILED=1
  3. Проверяем статус деплоя и делаем rollback до сохранённого состояния:

    if [ "$_DEPLOY_FAILED" == "1" ] && [ "x$_LAST_DEPLOYED_RELEASE" != "x" ] ; then
      helm rollback $_RELEASE_NAME $_LAST_DEPLOYED_RELEASE
    fi
  4. Завершаем pipeline с ошибкой, если деплой оказался неуспешным:

    if [ "$_DEPLOY_FAILED" == "1" ] ; then exit 1 ; fi

Опять же, начиная с версии Helm 2.13, при вызове helm upgrade достаточно указать опцию --atomic и после провальной инсталяции будет автоматически выполнен rollback (подробности см. в PR).

Ожидание готовности ресурсов релиза и обратная связь в момент выката


По задумке, Helm должен следить за выполнением соответствующих liveness- и readiness-проб при использовании опции --wait:


      --wait                     if set, will wait until all Pods, PVCs, Services, and minimum number of Pods of a Deployment are in a ready state before marking the release as successful. It will wait for as long as --timeout

Должным образом сейчас эта функция не работает: поддерживаются не все ресурсы и не все версии API. Да и сам заявленный процесс ожидания не удовлетворяет нашим потребностям.

Как и в случае с использованием kubectl wait, нет быстрой обратной связи и нет возможности регулировать это поведение. Если выкат завершается с ошибкой, то мы об этом узнаём только по истечении timeout. При проблемной инсталляции необходимо как можно раньше завершить процесс выката, зафейлить CI/CD pipeline, откатить релиз до рабочей версии и перейти к отладке.

Если проблемный релиз откатили, а Helm не возвращает никакой информации в процессе выката, то к чему сводится отладка?! В случае с kubectl wait можно организовать отдельный процесс для показа логов, для которого потребуются имена ресурсов релиза. Как организовать простое и рабочее решение — сходу неясно. А помимо логов pod'ов полезная информация может содержаться и в процессе выката, событиях ресурсов…

Наша CI/CD-утилита werf может деплоить Helm-чарт и следить за готовностью ресурсов, а также выводить сопутствующую выкату информацию. Все данные объединяются в единый поток и выдаются в лог.

Эта логика вынесена в отдельное решение kubedog. С помощью утилиты можно подписаться на ресурс и получать события и логи, а также своевременно узнать о провалившимся выкате. Т.е. в качестве решения после вызова helm install/upgrade без опции --wait можно вызвать kubedog для каждого ресурса релиза.

Мы стремились сделать инструмент, который предоставит всю необходимую информацию для отладки в выводе CI/CD pipeline. Подробнее об утилите можно почитать в нашей недавней статье.

Возможно, в Helm 3 когда-нибудь появится подобное решение, но пока наш issue находится в подвисшем состоянии.

Безопасность при использовании helm init по умолчанию


По умолчанию, при выполнении команды helm init, серверный компонент устанавливается в кластер с правами, аналогичными суперпользователю, что может приводить к нежелательным последствиям при доступе третьих лиц.

Чтобы обеспечить безопасность кластера, необходимо ограничить возможности Tiller, а также позаботиться о соединении — безопасности сети, по которой происходит общение между компонентами Helm.

Первое может быть достигнуто за счёт использования стандартного механизма Kubernetes RBAC, который позволит ограничить действия tiller, а второе — настройкой SSL. Подробнее можно почитать в документации Helm: Securing your Helm Installation.

Бытует мнение, что наличие серверного компонента — Tiller — серьёзная архитектурная ошибка, буквально инородный ресурс с правами суперпользователя в экосистеме Kubernetes. Отчасти мы согласны: реализация неидеальная, — но взглянем на это и с другой стороны. Если прервать процесс деплоя и убить Helm client, то система не останется в неопределенном состоянии, т.е. Tiller доведёт состояние релиза до валидного. Также необходимо понимать, что несмотря на то, что в Helm 3 отказываются от Tiller, эти функции так или иначе будут выполняться контроллером CRD.

Марсианские Go-шаблоны


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

Отсутствие секретов из коробки


Хранить и сопровождать код приложения, инфраструктуры, а также шаблонов для выката удобно, когда они расположены в одном месте. И секреты не исключение.

Helm не поддерживает секреты из коробки, однако доступен плагин helm-secrets, который по сути является прослойкой между sops, менеджером секретов от Mozilla, и Helm.

При работе с секретами мы используем собственное решение, реализованное в werf (документация по секретам). Из особенностей:

  • Простота реализации.
  • Хранение секрета в файле, а не только в YAML. Удобно при хранении сертификатов, ключей.
  • Перегенерация секретов с новым ключом.
  • Выкат без секретного ключа (при использовании werf). Может пригодиться для тех случаев, когда у разработчика нет этого секретного ключа, но есть необходимость запустить деплой на тестовый или локальный контур.

Заключение


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

Несмотря на то, что MR и issue могут висеть месяцами (вот пример того, как мы добавляли before-hook-creation policy для хуков в течение нескольких месяцев), участвовать в развитии проекта всё же можно. Каждый четверг проходит получасовой митинг разработчиков Helm, на котором можно узнать о приоритетах и текущих направлениях команды, задать вопросы и форсировать свои наработки. О мите и других каналах связи подробно написано здесь.

Использовать Helm или нет — решать, конечно, вам. Сами мы на сегодня придерживаемся такой позиции, что, несмотря на недостатки, Helm является приемлемым решением для деплоя и участвовать в его развитии полезно для всего сообщества.

P.S.


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




К сожалению, не доступен сервер mySQL