Typhon freight hauler concept, Anton Swanepoel
Меня зовут Дмитрий Сугробов, я разработчик в «Леруа Мерлен». В статье расскажу, зачем нужен Helm, как он упрощает работу с Kubernetes, что поменялось в третьей версии и как с его помощью обновлять приложения в продакшене без простоя.
Это конспект по мотивам выступления на конференции @Kubernetes Conference by Mail.ru Cloud Solutions — если не хотите читать, смотрите видео.
Почему мы используем Kubernetes в продакшене
«Леруа Мерлен» — лидер на рынке DIY-ритейла в России и Европе. В нашей компании больше ста разработчиков, 33 000 внутренних сотрудников и огромное количество людей, посещающих гипермаркеты и сайт. Для того чтобы сделать всех их счастливыми, мы решили придерживаться стандартных подходов в индустрии. Разрабатывать новые приложения, используя микросервисную архитектуру; для изоляции окружений и правильной доставки использовать контейнеры; а для оркестрации использовать Kubernetes. Цена использования оркестраторов стремительно дешевеет: на рынке растёт количество инженеров, владеющих технологией, появляются провайдеры, предлагающие Kubernetes как сервис.
Всё, что делает Kubernetes, конечно, можно сделать другими способами, например, обмазав скриптами какой-нибудь Дженкинс и docker-compose, но зачем усложнять жизнь, если есть готовое и надежное решение? Поэтому мы пришли к Kubernetes и уже год его используем в продакшене. Сейчас у нас двадцать четыре кластера Kubernetes, самому старому из них больше года, в нем порядка двухсот подов.
Проклятие большого количества YAML-файлов в Kubernetes
Для запуска микросервиса в Kubernetes создадим по меньшей мере пять YAML-файлов: для Deployment, Service, Ingress, ConfigMap, Secrets — и отправим в кластер. Для следующего приложения напишем тот же пакет ямликов, с третьим — еще один и так далее. Помножим количество документов на количество окружений, уже получим сотни файлов, и это ещё не учитывая динамические окружения.

Adam Reese, core maintainer Helm, ввел понятие «Цикл разработки в Kubernetes», которое выглядит так:
- Copy YAML — копировать YAML-файл.
- Paste YAML — вставить его.
- Fix Indents — починить отступы.
- Repeat — повторить заново.
Вариант рабочий, но приходится много раз копировать YAML-файлы. Чтобы этот цикл изменить, и придумали Helm.
Что такое Helm
Во-первых, Helm — пакетный менеджер, помогающий находить и устанавливать нужные программы. Для установки, например, MongoDB не нужно заходить на официальный сайт и скачивать бинарники, достаточно выполнить команду helm install stable/mongodb
.
Во-вторых, Helm — шаблонизатор, помогает параметризировать файлы. Вернемся к ситуации с YAML-файлами в Kubernetes. Проще написать тот же файл YAML, добавить в него некоторые placeholder-ы, в которые Helm подставит значения. То есть вместо большого набора ямликов будет набор темплейтов (шаблонов), в которые в нужный момент подставятся нужные значения.
В-третьих, Helm — мастер по развертыванию. С его помощью можно устанавливать, откатывать и обновлять приложения. Давайте разберёмся, как это делать.

Как пользоваться Helm для деплоя собственных приложений
Установим Helm клиент на компьютер, следуя официальной инструкции. Затем создадим набор YAML-файлов. Вместо указания конкретных значений оставим плейсхолдеры, которые в будущем Helm заполнит информацией. Набор таких файлов называется Helm чарт. В консольный клиент Helm его можно отправить тремя способами:
- указать папочку с шаблонами;
- упаковать в .tar архив и указать на него;
- положить шаблон в удалённый репозиторий и добавить ссылку на репозиторий в Helm клиент.
Ещё нужен файл со значениями — values.yaml. Данные оттуда будут подставляться в шаблон. Создадим и его.

Во второй версии Helm есть дополнительное серверное приложение — Tiller. Оно висит снаружи Kubernetes и ждёт запросы от Helm-клиента, а при вызове подставляет нужные значения в шаблон и отправляет в Kubernetes.

Helm 3 устроен проще: вместо обработки шаблонов на сервере, информация теперь обрабатывается целиком на стороне Helm-клиента и отправляется напрямую в Kubernetes API. Это упрощение повышает безопасность кластера и облегчает схему выката.
Как всё это работает
Запускаем команду helm install
. Укажем название релиза приложения, дадим путь до values.yaml. В конце укажем репозиторий, в котором лежит чарт и название чарта. В примере это «lmru» и «bestchart» соответственно.
helm install --name bestapp --values values.yaml lmru/bestchart
Выполнение команды возможно только единожды, при повторном выполнении вместо install
нужно использовать upgrade
. Для простоты вместо двух команд можно выполнять команду upgrade
с дополнительным ключом --install
. При первом исполнении Helm отправит команду на установку релиза, а в дальнейшем будет его обновлять.
helm upgrade --install bestapp --values values.yaml lmru/bestchart
Подводные камни деплоя новых версий приложения с Helm
В этом месте рассказа я играю с залом в «Кто хочет стать миллионером», и мы выясняем, как заставить Helm обновить версию приложения. Смотреть видео.
Когда я изучал работу Helm, меня удивило странное странное поведение при попытке обновления версий запущенных приложений. Код приложения обновил, в докер-реджистри загрузил новый образ, отправил команду на деплой – и ничего не произошло. Ниже несколько не совсем удачных способов обновления приложений. Изучая каждый из них более подробно, начинаешь понимать внутреннее устройство инструмента и причины такого не очевидного поведения.
Способ 1. Не менять информацию с момента последнего запуска
Как гласит официальный сайт Helm, «Kubernetes чарты бывают большими и сложными, поэтому Helm старается лишний раз ничего не трогать». Поэтому, если обновить latest-версию образа приложения в docker registry и выполнить команду helm upgrade
, то ничего не произойдёт. Helm будет думать, что ничего не поменялось и посылать в Kubernetes команду на обновление приложения не нужно.
Здесь и дальше тег latest показан исключительно в качестве примера. При указании этого тега Kubernetes будет каждый раз скачивать образ из docker registry, вне зависимости от параметра imagePullPolicy. Использование latest в продакшене нежелательно и вызывает побочные эффекты.
Способ 2. Обновлять LABEL в image
Как написано в той же документации, «Helm будет обновлять приложение, только если оно поменялось с момента последнего релиза». Логичным вариантом для этого покажется обновление метки LABEL в самом докер-образе. Однако Helm не заглядывает в образы приложений и не имеет понятия о каких-либо изменениях в них. Соответственно, при обновлении меток в образе Helm о них не узнает, и команда обновления приложения в Kubernetes не поступит.
Способ 3. Использовать ключ --force

Обратимся к мануалам и поищем нужный ключик. Больше всего по смыслу подходит ключ --force
. Несмотря на говорящее название, поведение отличается от ожидаемого. Вместо форсированного обновления приложения, его реальное предназначение — восстановление находящегося в статусе FAILED релиза. Если не использовать этот ключ, то нужно последовательно выполнять команды helm delete && helm install --replace
. Вместо этого предлагается использовать ключ --force
, который автоматизирует последовательное выполнение этих команд. Больше информации в этом пулл-реквесте. Для того чтобы сказать Helm всё-таки обновить версию приложения, к сожалению, этот ключ не подойдёт.
Способ 4. Изменять labels напрямую в Kubernetes

Обновление label напрямую в кластере с помощью команды kubectl edit
— плохая идея. Это действие приведёт к неконсистентности информации между работающим приложением и тем, что изначально отправилось на деплой. Поведение Helm при деплое в этом случае отличается от его версии: Helm 2 ничего делать не будет, а Helm 3 задеплоит новую версию приложения. Для понимания причины нужно понять, как работает Helm.
Как устроен Helm
Для определения, изменилось ли приложение с момента последнего релиза, Helm может воспользоваться:
- запущенным приложением в Kubernetes;
- новым values.yaml и актуальным чартом;
- внутренней информацией Helm о релизах.
helm history
, мы получим всю информацию о версиях, установленных с помощью Helm.

Ещё есть подробная информация об отправленных шаблонах и значениях. Мы можем её запросить:

Во второй версии Helm эта информация лежит в том же неймспейсе, где запущен Tiller (по умолчанию — kube-system), в ConfigMap, помеченном меткой «OWNER=TILLER»:

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

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

Третий Helm использует стратегию three-way merge: в дополнение к той информации учитывает ещё и приложение, которое работает прямо сейчас в Kubernetes.

По этой причине старая версия Helm не будет ничего делать, так как не учитывает информацию приложения в кластере, а вот Helm 3 получит изменения и отправит новое приложение на деплой.
Способ 5. Использовать ключ --recreate-pods
С помощью ключа --recreate-pods
можно достичь того, что изначально планировалось получить с помощью ключа --force
. Контейнеры перезапустятся и, согласно политике imagePullPolicy: Always для тега latest (об этом в сноске выше), Kubernetes скачает и запустит новую версию образа. Делать будет это не самым лучшим образом: не учитывая StrategyType деплоймента, резко выключит все старые инстансы приложения и пойдёт запускать новые. Во время перезапуска система не будет работать, пользователи будут страдать.
В самом Kubernetes похожая проблема тоже существовала продолжительное время. И вот, спустя 4 года после открытия Issue, проблему исправили, и начиная с 1.15 версии Kubernetes появляется возможность rolling-restart подов.
Helm же просто выключает все приложения и запускает рядом новые контейнеры. В продакшене так делать нельзя, чтобы не вызвать простой приложения. Такое нужно только для нужд разработки, можно выполнять только в stage-окружениях.
Как обновить версию приложения с помощью Helm?
Будем менять значения, отправляемые в Helm. Как правило, это значения, подставляемые на место тега образа. В случае latest, часто используемого для непродуктивных окружений, в роли изменяемой информации выступает аннотация, которая для самого Kubernetes бесполезна, а для Helm будет выступать сигналом к необходимости обновления приложения. Варианты заполнения значения аннотации:
- Рандомное значение с помощью стандартной функции —
{{ randAlphaNum 6 }}
.
Есть нюанс: после каждого деплоя с использованием чарта с такой переменной значение аннотации будет уникальным, и Helm будет полагать, что есть изменения. Получается, всегда будем перезапускать приложение, даже если не поменяли его версию. Это не критично, так как простоя не будет, но всё же неприятно. - Вставлять текущую дату и время —
{{ .Release.Date }}
.
Вариант похож на рандомное значение с постоянно уникальной переменной. - Более правильный способ — использовать контрольные суммы. Это SHA образа либо SHA последнего коммита в гите —
{{ .Values.sha }}
.
Их надо будет подсчитывать и отправлять в Helm клиент на вызывающей стороне, например в Дженкинсе. Если приложение поменялось, то и контрольная сумма поменяется. Следовательно, Helm будет обновлять приложение только тогда, когда нужно.
Подытожим наши попытки
- Helm делает изменения наименее инвазивным способом, поэтому любое изменение на уровне образа приложения в Docker Registry не приведёт к обновлению: после выполнения команды ничего не произойдёт.
- Ключ
--force
используется для восстановления проблемных релизов и не связан с принудительным обновлением. - Ключ
--recreate-pods
принудительно обновит приложения, но сделает это вандальным способом: резко выключит все контейнеры. От этого пострадают пользователи, на проде так делать не стоит. - Напрямую вносить изменения в кластер Kubernetes с помощью команды
kubectl edit
не надо: нарушим консистентность, а поведение будет отличаться в зависимости от версии Helm. - С выходом новой версии Helm появилось много нюансов. Issues в репозитории Helm описаны понятным языком, они помогут разобраться в деталях.
- Добавление изменяемой аннотации в чарт сделает его более гибким. Это позволит выкатывать приложение правильно, без простоя.
Мысль из разряда «мир во всём мире», работающая во всех сферах жизни: читайте инструкцию перед применением, а не после. Только владея полной информацией, получится строить надёжные системы и делать пользователей счастливыми.
Другие ссылки по теме:
- Знакомство с Helm 3
- Официальный сайт Helm
- Репозиторий Helm на GitHub
- 25 полезных инструментов Kubernetes: развертывание и управление
Этот доклад впервые прозвучал на @Kubernetes Conference by Mail.ru Cloud Solutions. Смотрите видео других выступлений и подписывайтесь на анонсы мероприятий в Telegram Вокруг Kubernetes в Mail.ru Group.
Автор: Дмитрий Сугробов