Боль «голых» манифестов 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 — он генерирует для него манифесты.