У тебя есть Deployment, Service и ConfigMap. В dev ты хочешь одну реплику, образ с тегом latest и лимит памяти 256Mi. В stage - две реплики и образ по коммиту. В prod - шесть реплик, anti-affinity, лимит 2Gi и отдельный namespace. Что обычно происходит дальше? Появляются три почти одинаковые папки с тремя почти одинаковыми YAML, и через месяц они расходятся: в prod кто-то добавил probe, а в dev забыл, и баг ловится только на бою. Копипаста манифестов - это технический долг, который зреет молча.
Helm решает это Go-шаблонами: ты обмазываешь YAML конструкциями {{ .Values.replicas }}, и манифест перестаёт быть валидным YAML - это уже текстовый шаблон, который kubectl не понимает без рендера. Kustomize заходит с другой стороны. Идея простая до неприличия: храни чистый валидный YAML как базу, а различия между окружениями описывай отдельными наложениями (overlays), которые патчат базу. Никаких шаблонов, никакого нового языка - только Kubernetes YAML и операции наложения. Поэтому связку "kustomize kubernetes" так любят в GitOps: то, что лежит в git, можно прочитать глазами и скормить в kubectl as-is.
Kustomize встроен прямо в kubectl - флаг -k. Отдельный бинарь ставить не обязательно, хотя для свежих фич он бывает полезен (об этом ниже). Это и есть главная развилка в споре "helm vs kustomize": Helm - про упаковку и дистрибуцию чужого софта, Kustomize - про управление твоей собственной kubernetes конфигурацией по окружениям.

Механика: base, overlays и как наложение работает под капотом
Сердце Kustomize - файл kustomization.yaml. Он не описывает ресурсы, он описывает, какие ресурсы взять и что с ними сделать. Когда ты запускаешь kubectl kustomize или kubectl apply -k, движок собирает граф: читает resources, применяет к ним трансформеры (генераторы, патчи, префиксы, лейблы) и на выходе печатает финальный плоский YAML. Важно понять: Kustomize ничего не отправляет в кластер сам по себе - он рендерит. apply -k = "отрендерь и примени". kubectl kustomize = "просто отрендерь, покажи".
Каноническая структура - база плюс наложения:
Код: Выделить всё
myapp/
base/
deployment.yaml
service.yaml
kustomization.yaml
overlays/
dev/
kustomization.yaml
replicas-patch.yaml
prod/
kustomization.yaml
replicas-patch.yaml
resources-patch.yaml
Код: Выделить всё
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- deployment.yaml
- service.yaml
commonLabels:
app: payment-api
Наложение overlays/prod указывает на базу как на свой ресурс и добавляет различия:
Код: Выделить всё
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: payment-prod
namePrefix: prod-
resources:
- ../../base
patches:
- path: replicas-patch.yaml
- path: resources-patch.yaml
images:
- name: registry.cyberlake.ru/payment-api
newTag: 1.8.4
Патчи: strategic merge против JSON 6902
Патч - это способ сказать "в базовом ресурсе поменяй вот это поле". Есть два диалекта, и понимать разницу критично.
Strategic merge patch - ты пишешь кусок того же ресурса, указываешь, кого патчить (по kind и name), и заполняешь только те поля, что хочешь изменить. Kustomize мёрджит это поверх базы по правилам Kubernetes:
Код: Выделить всё
apiVersion: apps/v1
kind: Deployment
metadata:
name: payment-api
spec:
replicas: 6
template:
spec:
containers:
- name: api
resources:
limits:
memory: 2Gi
JSON 6902 patch - это адресная операция по пути JSON Pointer: add, replace, remove, и т.д. Применяй, когда strategic merge не умеет: например, удалить элемент массива по индексу или поправить поле, у которого нет merge key.
Код: Выделить всё
patches:
- target:
kind: Deployment
name: payment-api
patch: |-
- op: replace
path: /spec/template/spec/containers/0/imagePullPolicy
value: IfNotPresent
- op: remove
path: /spec/template/spec/tolerations/0
Генераторы ConfigMap и Secret: почему хеш в имени спасает деплои
Тут Kustomize делает то, чего нет в голом kubectl и что в Helm приходится городить руками. configMapGenerator и secretGenerator создают ConfigMap/Secret из файлов или литералов - и добавляют к имени суффикс-хеш от содержимого:
Код: Выделить всё
configMapGenerator:
- name: app-config
literals:
- LOG_LEVEL=info
- FEATURE_X=true
files:
- app.properties
secretGenerator:
- name: db-creds
envs:
- db.env
Поведением суффикса можно управлять через generatorOptions (например, disableNameSuffixHash: true, если хеш мешает - скажем, ConfigMap читает внешний оператор по фиксированному имени). По умолчанию хеш лучше оставлять.
Практика: смотрим, что реально отрендерится
Золотое правило Kustomize - всегда смотри вывод перед apply. Команда:
Код: Выделить всё
$ kubectl kustomize overlays/prod
Код: Выделить всё
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: payment-api
name: prod-payment-api
namespace: payment-prod
spec:
replicas: 6
template:
spec:
containers:
- image: registry.cyberlake.ru/payment-api:1.8.4
name: api
resources:
limits:
memory: 2Gi
Применяем:
Код: Выделить всё
$ kubectl apply -k overlays/prod
deployment.apps/prod-payment-api created
service/prod-payment-api created
Код: Выделить всё
$ diff <(kubectl kustomize overlays/dev) <(kubectl kustomize overlays/prod)
Грабли и антипаттерны из практики
- Версия kustomize в kubectl отстаёт. Встроенный в kubectl Kustomize - это вмороженная версия, обычно на пару релизов позади отдельного бинаря. Свежие фичи (например, новые трансформеры или replacements в полном объёме) могут не работать через kubectl -k. Если упёрся в "поле не распознано" - ставь отдельный kustomize (актуальная линейка v5.x) и рендери им: kustomize build overlays/prod | kubectl apply -f -.
- Overlay-наследование больше двух уровней. Соблазн сделать base -> region-base -> prod-eu -> prod-eu-canary. Каждый уровень добавляет неявности: чтобы понять, что получится, надо держать в голове четыре файла. Глубокие цепочки наложений - путь к манифестам, которые никто не понимает. Держи плоско: одна база, тонкие наложения.
- vars и хрупкие замены. Старый механизм vars для проброса значений (имя сервиса в аргументы) ненадёжен и выводится из обращения - используй современный трансформер replacements, который берёт значение из одного ресурса по пути и кладёт в другой. Не строй логику на vars в новых проектах.
- commonLabels трогает селекторы. commonLabels проставляет лейблы и в metadata, и в spec.selector. Если ты докинул commonLabels в наложение поверх уже задеплоенного Deployment, selector у которого иммутабелен, apply упадёт. Для лейблов, которые не должны попадать в селекторы, есть labels с опцией includeSelectors: false.
- Секреты в открытом YAML. secretGenerator кладёт значения в git как base64 - это не шифрование. Для GitOps используй sealed-secrets, SOPS или внешний vault, а в Kustomize держи только ссылки.
Не воюй с инструментами, выбирай по задаче. Kustomize бери, когда это твои манифесты и тебе нужны окружения: dev/stage/prod, фиче-ветки, регионы. Чистый YAML, прозрачный diff, дружелюбность к GitOps - Argo CD и Flux умеют рендерить Kustomize нативно.
Helm бери, когда нужна упаковка и дистрибуция: ты ставишь чужое приложение (postgres-operator, ingress-nginx, cert-manager) из Artifact Hub, тебе важны версионирование релиза, rollback и values как публичный API чарта. Параметризовать сотню значений шаблонами проще, чем сотней патчей.
И связка - часто лучший ответ. Два рабочих паттерна. Первый: helm template печатает чистый YAML из чужого чарта, ты кладёшь его в base и накатываешь сверху свои overlays Kustomize - получаешь и экосистему Helm, и патчинг без форка чарта. Второй: Kustomize умеет вызывать Helm сам через helmCharts в kustomization.yaml (нужен флаг --enable-helm), рендерит чарт и патчит его на лету. Argo CD поверх этого даёт GitOps-обёртку. Так и работают зрелые платформенные команды - не "или-или", а Helm для пакетов, Kustomize для окружений.
Мини-лаба: собери три окружения руками
- Создай base/ с deployment.yaml (nginx, 1 реплика) и kustomization.yaml, перечисли ресурс и добавь commonLabels.
- Сделай overlays/dev/kustomization.yaml с resources: [../../base] и namePrefix: dev-. Запусти kubectl kustomize overlays/dev и убедись, что имя получило префикс.
- Сделай overlays/prod/ с патчем replicas: 4 (strategic merge) и секцией images, меняющей тег nginx на конкретную версию.
- Прогони diff <(kubectl kustomize overlays/dev) <(kubectl kustomize overlays/prod) и найди все различия.
- Добавь в prod configMapGenerator с одним литералом, отрендери дважды, меняя значение, и убедись, что суффикс-хеш в имени меняется.
- Примени dev в кластер: kubectl apply -k overlays/dev, проверь kubectl get deploy.
- Чем принципиально отличается подход Kustomize от Helm и почему манифест Kustomize остаётся валидным YAML, а шаблон Helm - нет?
- Зачем configMapGenerator добавляет хеш-суффикс к имени и как это связано с rolling update при изменении конфига?
- В каком случае нужен JSON6902 patch вместо strategic merge, и почему привязка к индексу массива - это риск?
- Назови два паттерна связки Helm и Kustomize и объясни, когда какой уместен.
Kustomize - это наложение, а не шаблонизация: чистая база плюс тонкие overlays на окружение. Ты получаешь валидный YAML, прозрачный diff, автоматическую перекатку при смене конфига через генераторы и идеальную совместимость с GitOps. Helm и Kustomize не конкуренты, а инструменты под разные задачи: Helm упаковывает и раздаёт, Kustomize настраивает под окружение, и в продакшене они отлично работают вместе.