← назад к разделу

Когда сервис едет в Kubernetes, за ним тянется не один манифест, а пачка: Deployment, Service, ConfigMap, Ingress, HorizontalPodAutoscaler. Плюс их надо разложить по окружениям — в dev один образ и одна реплика, в prod другой образ и пять. Копировать эти YAML руками на каждое окружение — путь к рассинхрону. Эту задачу решает Helm.

Дальше — на числах: что Helm подставляет в шаблон и что делают с релизом обновление и откат.

один чарт, разные values, одна ось ревизий релиза чарт ./chart — один на оба окружения templates/deployment.yaml replicas: {{ .Values.replicaCount }} image: {{ .Values.image.tag }} values.yaml — дефолты чарта replicaCount: 1 · tag: 1.4.0 resources.memory: 512Mi values-prod.yaml — отличия прода replicaCount: 5 · tag: 1.5.0 resources.memory: 1Gi что уехало в кластер одна команда — все 5 объектов релиза ревизии: 1 релиз orders · namespace devhelm install orders ./chartreplicas: 1image orders:1.4.0 · memory 512Miтекущая — 1install подставил дефолты чарта: 1 реплика, 512Mi — релиз orders, ревизия 1 релиз orders · namespace devhelm upgrade orders ./chart--set replicaCount=5replicas: 1 → 5образ и память — из values.yaml2текущая — 2--set задан в команде, а не в values.yaml: следующий upgrade без него вернёт 1 релиз orders · namespace devhelm rollback orders 1replicas: 5 → 1значения ревизии 1 вернулись целиком23текущая — 3rollback не стирает ревизию 2: он записал значения ревизии 1 как ревизию 3 релиз orders · namespace prodhelm install orders ./chart-f values-prod.yaml -n prodreplicas: 5image orders:1.5.0 · memory 1Giтекущая — 1в values-prod.yaml только отличия — и prod получает 5 реплик, тег 1.5.0 и 1Gi

Шаблоны чарта не меняются — меняется только слой значений: дефолты дают 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), и только потом ставят чарт. Держите в голове ещё одно: чарт фиксирует конкретную версию образа внутри себя, так что обновление базы — это отдельная работа по обновлению чарта, а не то, что случится само.
чарт orders Chart.yaml паспорт values.yaml дефолты templates/ шаблоны charts/ зависимости релиз в кластере

Из чего складывается чарт: паспорт, значения по умолчанию, шаблоны манифестов и скачанные зависимости лежат в одной папке, и 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.

Что почитать дальше