Когда сервис едет в Kubernetes, за ним тянется не один манифест, а пачка: Deployment, Service, ConfigMap, Ingress, HorizontalPodAutoscaler. Плюс их надо разложить по окружениям — в dev один образ и одна реплика, в prod другой образ и пять. Копировать эти YAML руками на каждое окружение — путь к рассинхрону. Эту задачу решает Helm.
Дальше — на числах: что Helm подставляет в шаблон и что делают с релизом обновление и откат.
Шаблоны чарта не меняются — меняется только слой значений: дефолты дают 1 реплику и 512Mi, values-prod.yaml — 5 реплик, тег 1.5.0 и 1Gi. А helm rollback orders 1 не стирает ревизию 2: он записывает значения первой ревизии новой, третьей.
Что это
Helm — пакетный менеджер для Kubernetes, «apt/npm для кластера». Он упаковывает набор манифестов в чарт (chart) — шаблоны YAML плюс файл значений values.yaml. Устанавливая чарт, Helm подставляет значения в шаблоны и применяет результат в кластер как единый релиз, которым потом можно управлять целиком: обновить, откатить, удалить.
Что это даёт:
- Параметризация вместо копипасты. Один чарт, разные
values— и тот же сервис разворачивается в dev и prod с разными образом, репликами, ресурсами. Значения складываются слоями: дефолты изvalues.yamlчарта, поверх — файл окружения (-f values-prod.yaml), поверх —--setиз командной строки; побеждает верхний слой. - Релиз как единица.
helm upgradeобновляет весь набор объектов одной командой,helm rollbackвозвращает предыдущую версию тоже одной. Слова «атомарно» тут, правда, не будет: по умолчанию Helm применяет изменения по порядку, и если что-то сломается на середине, часть объектов уже обновлена, а релиз останется в состоянииfailed. Всё-или-ничего включается явно — флагом--atomic(он же сам откатит неудачное обновление) в паре с--wait, который дожидается готовности подов. - Готовые чарты. PostgreSQL, Redis, Kafka ставятся из публичных репозиториев, а не собираются вручную. Репозиторий сначала подключают (
helm repo add bitnami https://charts.bitnami.com/bitnami), и только потом ставят чарт. Держите в голове ещё одно: чарт фиксирует конкретную версию образа внутри себя, так что обновление базы — это отдельная работа по обновлению чарта, а не то, что случится само.
Из чего складывается чарт: паспорт, значения по умолчанию, шаблоны манифестов и скачанные зависимости лежат в одной папке, и helm install превращает её в релиз.
Как выглядит
Чарт — это папка с определённой раскладкой:
chart/
Chart.yaml # паспорт чарта: без него это просто папка с YAML
values.yaml # значения по умолчанию
templates/ # шаблоны манифестов
Chart.yaml пропускают чаще всего, а без него Helm откажется работать со словами «Chart.yaml file is missing». Обязательного там всего три поля — apiVersion, name и version; четвёртое, appVersion, не обязательно, но пишут его почти всегда:
# Chart.yaml
apiVersion: v2
name: orders
version: 0.1.0 # версия самого чарта
appVersion: "1.4.0" # версия приложения, которое он ставит
Шаблон деплоймента с подстановками (в двойных фигурных скобках — значения из values.yaml):
# templates/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ .Release.Name }}-app
spec:
replicas: {{ .Values.replicaCount }}
selector:
matchLabels:
app: {{ .Release.Name }}
template:
metadata:
labels:
app: {{ .Release.Name }}
spec:
containers:
- name: app
image: "{{ .Values.image.repo }}:{{ .Values.image.tag }}"
resources:
limits:
memory: {{ .Values.resources.memory }}
Значения по умолчанию:
# values.yaml
replicaCount: 1
image:
repo: registry.example.com/orders
tag: "1.4.0"
resources:
memory: 512Mi
Установка и обновление — с переопределением значений под окружение:
helm install orders ./chart -f values-prod.yaml -n prod
helm upgrade orders ./chart --set replicaCount=5
helm rollback orders # вернуть предыдущую ревизию
helm rollback orders 1 # вернуть конкретную ревизию — первую
Эти две строки делают разное, и путают их постоянно. Число в helm rollback — это номер ревизии, а не «на сколько шагов назад». Без числа Helm откатывает на одну ревизию назад — то, что обычно и нужно в инциденте. Посмотреть, какие ревизии вообще есть, можно командой helm history orders. И сам откат тоже записывается новой ревизией: откатились с третьей на первую — получите четвёртую, а не вернётесь в прошлое.
В values-prod.yaml лежат только отличия прода (образ, пять реплик, больше памяти) — всё остальное берётся из дефолтов чарта.
--set не сохраняется
Строка helm upgrade orders ./chart --set replicaCount=5 делает не то, что кажется. Значение из --set живёт только в этой команде: следующий helm upgrade orders ./chart без него вернёт replicaCount к значению из values.yaml, то есть к единице. Реплики молча уедут с пяти до одной, и в журнале это будет выглядеть как обычное обновление.
Из этого два правила.
Всё, что должно жить, лежит в файле. Отличия среды — в values-prod.yaml, который передают -f при каждом обновлении. Тогда команда повторяема: её можно выполнить второй раз и получить тот же результат.
Если значение всё-таки задано флагом, его надо унести. Флаг --reuse-values берёт значения предыдущего релиза и накладывает новые поверх — тогда прошлый --set не потеряется. Пользоваться им стоит с оглядкой: он же сохраняет и то, что вы хотели убрать, а при смене версии чарта новые значения по умолчанию не подхватываются. Есть и --reset-values — обратное: выкинуть всё и взять только файлы.
Практический вывод: --set хорош для разового эксперимента и для подстановки того, что приходит из конвейера (тег образа), а состояние среды держат в файлах значений, лежащих рядом с кодом.
Посмотреть, что уедет
У Helm есть три способа увидеть результат до применения, и в промышленном контуре ими пользуются всегда.
helm template orders ./chart -f values-prod.yaml # развернуть шаблоны в готовый YAML
helm upgrade orders ./chart -f values-prod.yaml --dry-run # то же плюс проверка на сервере
helm diff upgrade orders ./chart -f values-prod.yaml # разница с тем, что уже установлено
helm template ничего не отправляет в кластер: он подставляет значения и печатает манифесты. Это первое, чем отлаживают шаблоны — видно, во что превратились условия и циклы, и где потерялся отступ.
--dry-run отправляет результат в api-server без записи: добавляется проверка схемы и прав.
helm diff — не встроенная команда, а популярный подключаемый модуль (helm plugin install https://github.com/databus23/helm-diff), и именно он отвечает на вопрос «что изменится в кластере». Разница с template существенная: шаблоны могут не измениться, а изменится версия чарта или значения по умолчанию — и увидеть это можно только сравнением с установленным релизом.
Хуки: миграция перед обновлением
Помните задачу из статьи про доставку: миграцию базы надо выполнить до того, как поднимутся новые поды. В Helm для этого есть хуки — обычные объекты с аннотацией, которая говорит, когда их применять.
# templates/migrate-job.yaml
apiVersion: batch/v1
kind: Job
metadata:
name: {{ .Release.Name }}-migrate
annotations:
"helm.sh/hook": pre-install,pre-upgrade
"helm.sh/hook-weight": "0"
"helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded
spec:
backoffLimit: 1
template:
spec:
restartPolicy: Never
containers:
- name: flyway
image: "{{ .Values.image.repo }}:{{ .Values.image.tag }}"
args: ["migrate"]
Что здесь важно. pre-upgrade означает «примени этот объект и дождись его завершения, прежде чем обновлять остальное». Если задача упала, Helm останавливает обновление — новые поды не поднимутся на несовместимой схеме, и это именно то поведение, которое нужно.
hook-weight задаёт порядок, если хуков несколько (меньше — раньше). hook-delete-policy убирает объект задачи: before-hook-creation удаляет прошлый экземпляр перед новым запуском (иначе второе обновление упадёт на существующем объекте), hook-succeeded подчищает после успеха, оставляя упавшие задачи для разбора.
Оговорка, ради которой стоит прочитать раздел до конца: хуки не участвуют в откате. helm rollback вернёт манифесты, но не отменит уже выполненную миграцию — обратная миграция, если она нужна, это отдельная работа, и потому схему меняют совместимо, как описано в статье про доставку.
Когда Helm не нужен
Helm — не единственный способ и не всегда лучший. Рядом стоит kustomize: он встроен в kubectl (kubectl apply -k), не использует шаблоны вовсе и работает наложением: есть базовые манифесты, есть слои с отличиями для каждой среды.
Разница по существу. Helm — шаблоны и параметры: гибко, позволяет условия и циклы, и легко доводит шаблон до состояния, когда YAML в нём уже не читается. Kustomize — обычные манифесты плюс патчи: читается всегда (это валидный YAML, а не текст со скобками), но выразить «если включено, добавь три объекта» тяжело.
Что выбирают на практике:
- Свой сервис, две-три среды, отличия в образе, репликах и ресурсах — kustomize. Меньше сущностей, ничего не надо устанавливать, шаблонов нет.
- Нужно отдать сервис другим командам или наружу — Helm. Чарт с описанными значениями — это готовый интерфейс настройки, а kustomize такого не даёт.
- Ставить чужое (PostgreSQL, Redis, Kafka, контроллеры) — Helm, потому что чарты уже написаны.
- Совсем маленький проект с одним сервисом — просто манифесты и
kubectl apply -f. Ни Helm, ни kustomize не дают ничего, что оправдало бы новый инструмент.
И часто встречающаяся комбинация: чужое ставят Helm, своё описывают kustomize. Оба подхода умеет применять оператор доставки из статьи про Argo CD, поэтому выбор не запирает.
Глубже: где Helm хранит состояние релизарасширенное
Вопрос, который выглядит теоретическим до первого инцидента: откуда helm rollback знает, что было раньше?
Helm хранит каждую ревизию релиза как объект Secret в том же пространстве имён, где установлен релиз. Имя выглядит как sh.helm.release.v1.orders.v3, тип — helm.sh/release.v1, внутри — сжатый архив с манифестами и значениями этой ревизии.
kubectl get secret -n prod -l owner=helm,name=orders
helm history orders -n prod # то же в читаемом виде: ревизии, статус, дата, описание
helm list -n prod # какие релизы вообще есть в пространстве имён
helm list -A # по всему кластеру
Четыре следствия, каждое из которых всплывает в работе.
Снесли пространство имён — снесли историю. Релиз перестаёт существовать: helm list пуст, rollback невозможен, а объекты, созданные вне этого пространства, остаются сиротами. Поэтому восстановление контура после удаления — это повторный helm install, а не upgrade.
История ограничена. По умолчанию Helm хранит десять последних ревизий (--history-max), старые удаляет. Откатиться на пятнадцать шагов назад нельзя; если нужна длинная история, её держат в репозитории с чартами и значениями, а не в кластере.
Состояние можно потерять и не снося namespace. Если кто-то удалил объект секрета руками (или он попал под уборку по метке), Helm перестанет видеть релиз при живых объектах. Лечится это повторной установкой с --take-ownership в новых версиях, а раньше — правкой аннотаций у каждого объекта.
Права на секреты — это права на релизы. Кто может читать секреты в пространстве имён, тот видит все значения релиза, включая переданные в них пароли. Это ещё один довод не хранить пароли в значениях, а брать их из объектов, созданных отдельно.
Глубже: зависимости чартоврасширенное
Пакетным менеджером Helm называется не только за упаковку: чарт может зависеть от других чартов. Описывают это в Chart.yaml:
dependencies:
- name: postgresql
version: "15.5.x"
repository: https://charts.bitnami.com/bitnami
condition: postgresql.enabled # ставить только если в values это включено
helm dependency update ./chart # скачать зависимости в charts/ и записать Chart.lock
helm dependency build ./chart # поставить ровно то, что записано в Chart.lock
Как это работает и где ловушки.
Значения вложенного чарта настраиваются под его именем. То есть в вашем values.yaml появляется блок postgresql: со всеми настройками зависимости — и именно так её параметризуют, а не правкой скачанного чарта.
Chart.lock фиксирует версии. Как package-lock.json: без него dependency update при следующем запуске притянет более новую подходящую версию, и результат сборки перестанет быть повторяемым. Файл держат в репозитории.
Каталог charts/ — скачанное. Его либо не держат в репозитории (и тогда сборка требует доступа в интернет), либо держат целиком (и тогда сборка работает в закрытом контуре). Второй вариант в корпоративной среде обычно и выбирают.
База данных как зависимость — удобно для стенда, спорно для прода. Чарт PostgreSQL внутри чарта приложения означает, что обновление приложения и обновление базы связаны одним релизом. Для стенда это ускоряет жизнь; для промышленного контура базу обычно ставят отдельно или берут управляемую — о чём говорит статья про основы Kubernetes.
Коротко
- Helm — пакетный менеджер Kubernetes: упаковывает манифесты в чарт (
Chart.yaml+ шаблоны +values.yaml). - Один чарт разворачивается в разные окружения через разные
values— без копипасты YAML. - Единица управления — релиз:
helm install/upgrade/rollbackменяют весь набор объектов сразу. Всё-или-ничего — только с--atomic --wait, по умолчанию неудачное обновление оставляет релиз наполовину применённым. - Готовые чарты (PostgreSQL, Redis, Kafka) экономят написание манифестов с нуля.
--setживёт только в одной команде: следующее обновление вернёт значение изvalues.yaml, поэтому состояние среды держат в файлах (-f), а не во флагах.- До применения смотрят
helm template(во что развернулись шаблоны),--dry-run(проверка на сервере) и модульhelm diff(что изменится в кластере). - Состояние релиза — объект
Secretтипаhelm.sh/release.v1в том же пространстве имён: снесли namespace — потеряли историю, ревизий по умолчанию десять, права на секреты дают доступ к значениям. - Зависимости описывают в
Chart.yaml, фиксируютChart.lock, параметризуют блоком под именем чарта; база как зависимость удобна для стенда и спорна для прода. - Миграцию выполняет хук
pre-upgradeсhook-weightи политикой удаления: упал — обновление остановится, но при откате хуки не отменяются. - Kustomize проще для своих сервисов с двумя-тремя средами, Helm нужен, когда чарт отдают другим или ставят чужое; для одного сервиса хватает
kubectl apply -f.
Что почитать дальше
- Деплой в Kubernetes — манифесты, конфигурация и rolling update, поверх которых работает Helm.
- Argo CD: GitOps-доставка — как деплоить чарты декларативно из Git.
- Kubernetes: основы — поды, деплойменты и сервисы.