Helm против Kustomize и операторов. Реальный пример

Когда Helm — не лучший выбор, и как выглядит итоговый чарт реального веб-приложения.

Helm, Kustomize и операторы решают пересекающиеся задачи разными способами. Helm — шаблонизация и пакетирование; Kustomize — наложение патчей без шаблонов; оператор — кастомная логика жизненного цикла.

Helm против Kustomize

Kustomize (встроен в kubectl -k) идёт от базовых манифестов и накладывает на них патчи (overlays) per-окружение — без языка шаблонов вообще. Вы не вставляете {{ .Values }}, а пишете обычный YAML-патч, который Kustomize мёржит поверх базы.

КритерийHelmKustomize
Подходшаблоны + 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 и безопасный деплой.
Проверьте себя
1. Чем принципиально Kustomize отличается от Helm?
AKustomize быстрее
BKustomize накладывает YAML-патчи на базу без языка шаблонов, Helm использует шаблоны + values
CKustomize шифрует секреты
DЭто одно и то же
2. Что умеет оператор, чего не делает Helm?
AРендерить шаблоны
BНепрерывно управлять рантайм-жизненным циклом приложения (бэкап, failover) через Custom Resource
CСтавить чарты
DХранить values
3. Какой приём НЕ использован в итоговом deployment.yaml примера?
Ainclude для лейблов
Bnindent для отступов
Cручное шифрование пароля в шаблоне
Ddefault для тега образа