Боль «голых» манифестов Kubernetes

Разбираемся, почему набор YAML-файлов перестаёт работать, как только приложение выходит за пределы одного окружения.

Голые манифесты — это обычные YAML-файлы Kubernetes (Deployment, Service, ConfigMap), которые применяются командой kubectl apply -f без какого-либо слоя шаблонизации или управления версиями.

Вы уже умеете описывать приложение в Kubernetes: Deployment запускает поды, Service даёт им стабильный адрес, ConfigMap и Secret хранят конфигурацию. Пока приложение одно и окружение одно, эта схема прекрасна. Проблемы начинаются ровно в тот момент, когда у вас появляется второе что-нибудь: второе окружение, вторая копия сервиса, второй разработчик.

Чтобы прочувствовать масштаб боли, давайте посчитаем. Даже скромное приложение в продакшене — это не один файл. Обычно набирается Deployment, Service, ConfigMap, Secret, Ingress, HorizontalPodAutoscaler, ServiceAccount, иногда PodDisruptionBudget и NetworkPolicy. Девять-десять YAML-файлов на один компонент. Умножьте на число микросервисов, а затем ещё на число окружений — и вы получаете несколько сотен файлов, в которых руками поддерживается консистентность. Человек физически не способен удержать в голове, что во всех этих файлах app: web написано одинаково, а не где-то проскочило app: Web с большой буквы, из-за чего Service перестанет находить поды.

Корень всех четырёх проблем ниже — один: в голых манифестах нет уровня абстракции между «как приложение устроено» и «как оно сконфигурировано здесь и сейчас». Структура (что у нас Deployment ссылается на этот Service, а Service открывает этот порт) и конфигурация (5 реплик, образ версии 1.4.3, такой-то домен) свалены в один и тот же текст. Любой инструмент, который захочет навести порядок, должен будет эти два слоя разделить — именно это и делает Helm.

Проблема первая: дублирование

Представьте, что вам нужно развернуть одно и то же приложение в трёх окружениях — dev, staging и prod. Манифесты почти одинаковые, отличаются лишь несколько строк: количество реплик, адрес базы данных, домен, лимиты ресурсов. На практике это означает три почти идентичных папки YAML, которые расходятся при каждой правке.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: web
spec:
  replicas: 1            # в prod нужно 5, в staging 2
  template:
    spec:
      containers:
        - name: web
          image: myapp:1.4.2
          env:
            - name: DATABASE_URL
              value: postgres://db-dev:5432/app   # в каждом окружении свой

Когда вы меняете образ на myapp:1.4.3, вам нужно вручную поправить три файла. Забыли один — окружения разъехались, и баг «у меня воспроизводится только на staging» гарантирован. Это классическая нарушенная DRY: одна и та же сущность описана в нескольких местах.

Соблазн «решить» дублирование копированием папок только усугубляет ситуацию. Папки dev/, staging/ и prod/ начинают жить своей жизнью: кто-то поправил лимиты памяти в проде под инцидент, но не перенёс это в staging; кто-то добавил в dev новую переменную окружения для отладки и забыл про неё. Через полгода между папками десятки расхождений, и ни одно из них не задокументировано как осознанное решение — это просто исторический мусор. Когда наступает время выкатить большое изменение, никто уже не может уверенно сказать, какие из различий между окружениями намеренные, а какие — забытые ошибки.

Проблема вторая: окружения как первоклассная сущность

В голых манифестах нет понятия «параметр окружения». Различия между dev и prod размазаны по десяткам строк в разных файлах. Нет единого места, куда можно заглянуть и спросить: «а чем prod отличается от staging?» Ответ — в diff между двумя папками, который никто не поддерживает в чистоте.

Хочется обратной структуры: один шаблон приложения и отдельный маленький файл значений на каждое окружение. Меняешь образ в одном месте — он применяется везде. Различия окружений собраны в компактных файлах вроде values-prod.yaml.

Проблема третья: версионирование и откат

Вы выкатили новую версию через kubectl apply, и что-то сломалось. Как откатиться? В мире голых манифестов вам нужно вручную найти предыдущие YAML в git, выкатить их обратно и надеяться, что вы ничего не забыли (например, удалённый при апгрейде ConfigMap). Нет понятия «релиз номер 7, откати на релиз номер 6». Состояние кластера не атомарно: kubectl apply -f ./manifests/ применяет файлы по одному, и сбой в середине оставляет кластер в полусломанном виде.

Откат «вернуть старые YAML» коварен ещё и тем, что он неполный по построению. Представьте: в релизе 7 вы переименовали ConfigMap с app-config на app-settings. Откатываете старые манифесты — они снова создают app-config, но старый-новый app-settings так и остаётся висеть в кластере, потому что kubectl apply на старых файлах про него ничего не знает и не удаляет. Кластер оказывается в состоянии, которого не было ни в релизе 6, ни в релизе 7 — это «франкенштейн» из обоих. Чтобы откат был честным, инструмент должен помнить полный набор объектов каждой ревизии и при откате вычислять разницу: что создать, что обновить, а что удалить. Ровно это Helm и делает, храня манифесты каждой ревизии целиком.

Отдельная боль — наблюдаемость выката. После kubectl apply вы не получаете ответа на простой вопрос: «дождались ли поды готовности или Deployment завис на старом ReplicaSet?». Приходится отдельно бегать с kubectl rollout status по каждому ресурсу. Helm же умеет ждать готовности всего релиза одним флагом и сообщать честный итог установки.

Проблема четвёртая: установка чужого софта

Нужно поставить в кластер PostgreSQL, Redis или ingress-контроллер. Без Helm вы идёте на GitHub проекта, копируете десяток YAML-файлов, разбираетесь, какие значения подставить, и держите эту копипасту у себя. Обновился апстрим — повторяете вручную. Helm превращает это в одну команду helm install.

Как Helm решает всё это

Helm вводит три идеи поверх Kubernetes:

ИдеяЧто даёт
ШаблонизацияОдин шаблон + файл значений вместо копипасты на каждое окружение
Пакет (chart)Приложение и все его ресурсы упакованы в один версионируемый артефакт
РелизУстановка чарта — это релиз с номером ревизии, который можно обновлять и откатывать одной командой

Под капотом это работает прозрачно для кластера. Helm не учит Kubernetes ничему новому и не добавляет в кластер своих контроллеров (в версии 3 — вообще никаких серверных компонентов). Он берёт шаблоны, подставляет значения, получает на выходе обычный поток манифестов и отдаёт его API-серверу теми же вызовами, что и kubectl. С точки зрения Kubernetes разницы между «применил Helm» и «применил kubectl» нет — в кластере лежат идентичные объекты. Вся «магия» Helm живёт до момента применения (рендеринг шаблонов) и рядом с объектами (метаданные релиза и история ревизий).

Когда голых манифестов достаточно

Справедливости ради: Helm — не серебряная пуля, и тащить его в любой проект не обязательно. Если у вас одно крошечное приложение в одном окружении, которое меняется раз в месяц, голый kubectl apply или связка вроде Kustomize вполне закроют потребность без новой сущности в стеке. Helm начинает окупаться, когда появляется хотя бы одно из трёх: несколько окружений с реальными различиями, потребность переиспользовать один и тот же шаблон для многих установок, или необходимость ставить и обновлять чужой готовый софт. Понимать, какую боль вы лечите, важнее, чем механически добавлять инструмент.

Частые ошибки в понимании

  • «Helm заменяет Kubernetes» — нет. Helm на выходе генерирует те же самые манифесты и отдаёт их в API Kubernetes. Это надстройка, а не альтернатива.
  • «Можно обойтись скриптами с sed» — можно, пока шаблон тривиален. Но sed не знает про типы YAML, отступы и зависимости между ресурсами; вы быстро упрётесь в нечитаемый bash.
  • «Дублирование — не проблема, у нас всё в git» — git хранит историю, но не устраняет рассинхрон трёх копий. DRY-проблема остаётся.

Итог

  • Голые манифесты ломаются на дублировании между окружениями, отсутствии версионирования и ручной установке стороннего софта.
  • Helm добавляет три слоя: шаблонизацию, упаковку в чарт и релизы с откатом.
  • Helm не заменяет Kubernetes — он генерирует для него манифесты.
Проверьте себя
1. Какую проблему голых манифестов решает шаблонизация Helm?
AДелает кластер быстрее
BУстраняет дублирование почти одинаковых YAML между окружениями
CЗаменяет Kubernetes API
DШифрует секреты автоматически
2. Что Helm отдаёт в Kubernetes на выходе?
AСобственный бинарный формат
BТе же стандартные манифесты Kubernetes
CDocker-образы
DКоманды kubectl
3. Почему откат через kubectl apply неудобен?
Akubectl не умеет apply
BНет понятия пронумерованного релиза и атомарного отката на предыдущую ревизию
CОткат запрещён в Kubernetes
DНужно перезагружать кластер