Helm против Kustomize и операторов. Реальный пример
Когда Helm — не лучший выбор, и как выглядит итоговый чарт реального веб-приложения.
Helm, Kustomize и операторы решают пересекающиеся задачи разными способами. Helm — шаблонизация и пакетирование; Kustomize — наложение патчей без шаблонов; оператор — кастомная логика жизненного цикла.
Helm против Kustomize
Kustomize (встроен в kubectl -k) идёт от базовых манифестов и накладывает на них патчи (overlays) per-окружение — без языка шаблонов вообще. Вы не вставляете {{ .Values }}, а пишете обычный YAML-патч, который Kustomize мёржит поверх базы.
| Критерий | Helm | Kustomize |
| Подход | шаблоны + values | патчи поверх базы, без шаблонов |
| Распространение | да (репозитории, версии) | нет встроенного пакетирования |
| Кривая входа | выше (Go templates) | ниже (чистый YAML) |
| Логика/условия | богатая (if/range/функции) | ограниченная |
| Лучшая ниша | переиспользуемые пакеты для многих | свои манифесты под пару окружений |
Эмпирика: публикуете чарт для других или нужна сложная параметризация → Helm. Свои манифесты, два-три окружения, не любите шаблоны → Kustomize. Их даже комбинируют: Helm рендерит, Kustomize патчит сверху.
Глубинная разница — в том, что читает человек, открывая репозиторий. В Helm-чарте исходник — это шаблон с дырками {{ .Values.foo }}, и чтобы понять итоговый манифест, его нужно мысленно отрендерить; зато одна правка шаблона аккуратно расходится по всем окружениям. В Kustomize исходник — это валидный YAML, который можно применить как есть, а различия окружений вынесены в маленькие читаемые патчи; зато при росте числа параметров overlays начинают дублировать структуру и расползаться. Грубо говоря, Helm платит читаемостью базы за мощь параметризации, а Kustomize — мощью параметризации за читаемость базы.
Отсюда и практический водораздел. Если вы отдаёте приложение наружу — клиентам, в публичный репозиторий чартов, другим командам, которым нужно версионирование и понятные «ручки» настройки, — выигрывает Helm. Если вы разворачиваете свои сервисы в своём кластере и окружений немного, Kustomize даёт меньше магии и более прозрачный дифф между prod и staging. А комбинация (Helm рендерит чарт, Kustomize накладывает точечный патч поверх) спасает в типичной ситуации «чужой чарт почти подходит, но нужно поправить одно поле, которого автор не вынес в values».
Helm против операторов
Оператор — это контроллер в кластере, кодирующий эксплуатационные знания (как делать бэкап, failover, мажорный апгрейд БД) через Custom Resource. Helm — инструмент установки; он не следит за приложением в рантайме. Для сложного stateful-софта (Postgres-кластер с автофейловером, Kafka) оператор делает то, что Helm не может: реагирует на события и управляет жизненным циклом непрерывно. Часто их сочетают: Helm устанавливает оператор, а оператор управляет приложением.
Ключевое слово здесь — непрерывно. Helm срабатывает в момент команды: отрендерил, применил, вышел. Что произойдёт с приложением через час, ему неинтересно — он не «дежурит» у кластера. Оператор же работает по циклу согласования (reconcile loop): он постоянно сравнивает желаемое состояние, описанное в Custom Resource, с фактическим и реагирует на расхождения сам. Упала реплика-лидер базы — оператор проведёт выборы нового лидера и переключит трафик; пришло время бэкапа — снимет его по расписанию; запросили мажорный апгрейд — выполнит многошаговую процедуру с проверками на каждом шаге. Закодировать такую живую логику в статичном чарте невозможно в принципе: чарт описывает состояние, а не поведение во времени.
Поэтому противопоставление «Helm или оператор» обычно ложное. Зрелый паттерн — двухслойный: Helm-чартом вы устанавливаете сам оператор (это обычный набор Deployment + CRD + RBAC, который прекрасно пакетируется), а затем создаёте Custom Resource, по которому оператор поднимает и пожизненно ведёт ваше приложение. Helm отвечает за «поставить и обновить движок», оператор — за «эксплуатировать то, что движок запустил». Писать собственный оператор стоит лишь тогда, когда эксплуатация приложения действительно требует кастомной логики; для stateless-веб-сервиса это избыточно, и хватает чистого чарта.
Реальный пример: чарт веб-приложения
Соберём всё изученное. Структура итогового чарта:
webapp/
├── Chart.yaml # version + appVersion + dependency postgresql
├── values.yaml # безопасные дефолты
├── values.schema.json # валидация
├── Chart.lock
└── templates/
├── _helpers.tpl # fullname, labels, selectorLabels
├── deployment.yaml # образ, probe, resources, env
├── service.yaml
├── ingress.yaml # за if .Values.ingress.enabled
├── configmap.yaml
├── hpa.yaml # за if .Values.autoscaling.enabled
├── migrate-job.yaml# pre-upgrade хук миграции
├── NOTES.txt
└── tests/
└── test-http.yaml # helm test: curl до сервиса
Фрагмент deployment.yaml, собирающий ключевые приёмы курса:
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "webapp.fullname" . }}
labels: {{- include "webapp.labels" . | nindent 4 }}
spec:
replicas: {{ .Values.replicaCount }}
selector:
matchLabels: {{- include "webapp.selectorLabels" . | nindent 6 }}
template:
metadata:
labels: {{- include "webapp.selectorLabels" . | nindent 8 }}
spec:
containers:
- name: web
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
ports:
- containerPort: {{ .Values.service.port }}
env:
- name: DATABASE_URL
value: {{ .Values.databaseUrl | quote }}
readinessProbe:
httpGet: { path: /healthz, port: {{ .Values.service.port }} }
resources: {{- toYaml .Values.resources | nindent 12 }}
Здесь работают: include для имени и лейблов, nindent для отступов, default для тега, quote для строки, toYaml для блока ресурсов, selectorLabels отдельно от полных лейблов. Деплой такого чарта в прод: helm upgrade --install web ./webapp -n prod -f values/prod.yaml --set image.tag=$SHA --wait --atomic.
Разберём, почему каждый кусочек именно такой. include "webapp.fullname" вместо хардкода имени даёт единое, предсказуемое именование всех ресурсов и позволяет ставить чарт несколько раз в одном namespace под разными релизами без коллизий. default .Chart.AppVersion на теге образа — это разумный запасной аэродром: если в values тег не задали, возьмётся версия приложения из Chart.yaml, но в проде мы всё равно перекрываем его конкретным SHA через --set, как учили в уроке про CI. quote на databaseUrl защищает от того самого превращения строки в неожиданный тип. А вынос лейблов в selectorLabels и labels по-разному принципиален: селектор Deployment иммутабелен после создания, поэтому в него идёт только узкий стабильный набор (name + instance), тогда как полные лейблы включают version, который меняется с каждым релизом, — смешай их, и первый же апгрейд с новой версией упадёт на попытке изменить неизменяемое поле.
Обратите внимание и на необязательные ресурсы за условиями: ingress.yaml и hpa.yaml отрендерятся только при enabled: true. Это и есть безопасные дефолты в действии — «голая» установка поднимет работающее приложение без внешнего доступа и без автоскейлинга, а прод-окружение включит и то и другое осознанно через свой values/prod.yaml. Хук-миграция (migrate-job.yaml как pre-upgrade) выполнит схему БД до выката нового кода, а tests/test-http.yaml позволит после деплоя прогнать helm test и убедиться, что сервис реально отвечает, — полный цикл от упаковки до проверки в одном чарте.
Как выбрать инструмент: итоговая логика
Под капотом все три приводят кластер к желаемому состоянию, но на разных уровнях абстракции. Helm абстрагирует пакет приложения (шаблон + версии + релизы), Kustomize — вариации манифестов (база + патчи), оператор — эксплуатацию (рантайм-логика). Они не взаимоисключающи: типичный зрелый стек — Helm для пакетирования и установки (в т.ч. операторов), Kustomize или values для окружений, операторы для сложного stateful. Выбор определяется вопросом «что именно я хочу абстрагировать»: распространяемый пакет, локальные вариации или живую эксплуатацию.
Частые ошибки
- Тащить Helm туда, где хватит Kustomize. Для пары своих манифестов под два окружения шаблоны — избыточная сложность.
- Ждать от Helm рантайм-логики оператора. Helm ставит и обновляет, но не реагирует на события кластера.
- Противопоставлять инструменты. Их часто комбинируют, а не выбирают «или-или».
Итог
- Helm — шаблоны и пакеты; Kustomize — патчи без шаблонов; оператор — рантайм-логика жизненного цикла.
- Выбор по вопросу «что абстрагировать»: распространяемый пакет / локальные вариации / эксплуатация; их комбинируют.
- Итоговый чарт веб-приложения собирает весь курс: include/nindent/default/quote/toYaml, хук-миграцию, probe, test и безопасный деплой.