Kustomize base/custom/patch: DRY Helm values для нескольких окружений
Published: 2026-02-07
Шесть окружений, один git-репозиторий, никакого дублирования Helm values. Вот цель. Трёхуровневая структура Kustomize её достигает. Этот пост — полное описание структуры, что относится к каждому уровню, как работают патчи, частые ошибки и как всё валидировать локально перед пушем.
Три уровня
fluxcd/base/ ← Уровень 1: общие HelmRelease, все общие values
fluxcd/custom/ ← Уровень 2: опциональные компоненты (не для каждого окружения)
projects/{env}/hub/ ← Уровень 3: env-оверлей = base+custom + дельта-патчи
Объект HelmRelease начинается в base/. Он содержит всё, что одинаково во всех окружениях: репозиторий и тег образа, запросы/лимиты ресурсов, probe-настройки, параметры serviceMonitor, конфиг TLS, feature flags.
Ценность этой структуры: когда обновляется версия чарта, меняется одна строка в base/. Когда корректируются лимиты ресурсов, меняется один блок в base/. Патчи никогда не затрагивают версии чартов или общие values — только дельты конкретного окружения.
Что идёт в base
Всё стабильное и общее. Пример — base/monitoring/kube-prom-stack/helmrelease.yaml:
yamlapiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: kube-prom-stack
spec:
interval: 30m
timeout: 15m
chart:
spec:
chart: kube-prometheus-stack
version: "85.2.0"
sourceRef:
kind: HelmRepository
name: prometheus-community
namespace: flux-system
values:
grafana:
enabled: false # Grafana живёт только на infra, никогда на spoke
defaultRules:
create: false
kubeScheduler:
enabled: false
kubeProxy:
enabled: false
prometheus:
prometheusSpec:
retention: 7d
storageSpec:
volumeClaimTemplate:
spec:
resources:
requests:
storage: 20Gi
Обратите внимание на grafana.enabled: false. Grafana живёт только на infra (hub). Все spoke отправляют метрики в Prometheus hub через remote_write. На spoke работает только стек, но не слой визуализации.
Версия чарта — в base/. Обновление kube-prometheus-stack с 85.2.0 до 86.0.0 — один коммит, одна строка, автоматически применяется ко всем окружениям.
Что идёт в custom
Компоненты, нужные не всем окружениям. Дерево custom/ повторяет структуру base/:
custom/
├── apps/ ← allure, sonarqube, vault, trivy-operator, terraform-operator
├── db/ ← cassandra, clickhouse, minio, mongodb, redis
├── elk/ ← apm-server, kibana, otel-collector
├── monitoring/← abot, cctp, exporters, apps-rules
├── queue/ ← kafka, kafka-ui, rabbitmq
└── web/ ← apisix-ext (только для demo)
dev и test включают полный стек баз данных. sre и loadgds — облегчённые, без Kafka, Mongo, Cassandra, ClickHouse.
Каждая поддиректория custom/ имеет свой kustomization.yaml со списком HelmRelease-файлов:
yaml# custom/db/kustomization.yaml
resources:
- redis/
- mongodb/
- cassandra/
- clickhouse/
- minio/
Таким образом, custom/db/ — самостоятельная цель Kustomize. Hub-оверлей может ссылаться на неё как на директорию, что разрешено load-restrictor.
Что идёт в патчи (только дельта)
Патчи содержат только то, что отличается между окружениями. Никогда не дублируйте base-values в патчах:
| Что меняется | Пример |
|---|---|
| Имя хоста для ingress | elasticsearch.dev.test.example.com |
| StorageClass | local-path (on-prem) или yc-network-ssd (Yandex Cloud) |
| Учётные данные | ES_PASSWORD, REDIS_PASSWORD |
| LoadBalancer IP | externalIP per-cluster |
| Toleration | только для managed-узлов YC |
| Количество реплик | sre нужно 3, dev — 1 |
| Лимиты ресурсов | loadgds нужен большой Elasticsearch, dev — минимальный |
Минимальный патч:
yaml# projects/dev/kustomization/hub/patches/elasticsearch.yaml
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: elasticsearch
namespace: dev
spec:
values:
master:
persistence:
storageClass: local-path
esConfig:
elasticsearch.yml: |
xpack.security.enabled: true
esJavaOpts: "-Xmx2g -Xms2g"
Строка namespace: dev в metadata обязательна — Kustomize использует её для нахождения нужного объекта. Без неё Kustomize не знает, к какому из HelmRelease с именем elasticsearch применить патч (их может быть по одному на окружение в одной kustomization).
Сравните с тем же чартом на sre (Yandex Cloud, больше ресурсов):
yaml# projects/sre/kustomization/hub/patches/elasticsearch.yaml
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: elasticsearch
namespace: sre
spec:
values:
master:
persistence:
storageClass: yc-network-ssd
resources:
requests:
memory: 8Gi
cpu: 2
esJavaOpts: "-Xmx6g -Xms6g"
replicas: 3
Патчи разные; base/ идентична. Обновление версии чарта в base/ автоматически затронет оба окружения.
Hub kustomization.yaml: сборка оверлея
yaml# projects/dev/kustomization/hub/kustomization.yaml
namespace: dev
resources:
- ../../../../base/
- ../../../../custom/db/
- ../../../../custom/elk/
- ../../../../custom/monitoring/
- spoke.yaml # Flux Kustomization для spoke-слоя
- monitoring.yaml # Flux Kustomization для monitoring-слоя
patches:
- path: patches/elasticsearch.yaml
- path: patches/kube-prom-stack.yaml
- path: patches/redis.yaml
# Добавление kubeConfig во ВСЕ HelmRelease
- target:
kind: HelmRelease
patch: |-
- op: add
path: /spec/kubeConfig
value:
secretRef:
name: dev-kubeconfig
JSON-патч kubeConfig применяется ко всем HelmRelease автоматически — никакого дублирования на каждый чарт. Это единственное место в hub-оверлее, где упоминается dev-kubeconfig.
namespace: dev в верхней части устанавливает namespace для всех ресурсов в этом оверлее, если у них нет собственного. Именно так все HelmRelease оказываются в namespace dev на hub без повторения namespace: dev в каждом файле.
Правило «только директории»
load-restrictor Kustomize блокирует ссылки за пределы текущей директории при локальном запуске kubectl kustomize. Всегда ссылайтесь на директории с kustomization.yaml внутри, никогда на отдельные файлы:
yaml# Правильно
resources:
- ../../../../base/
- ../../../../custom/db/
# Неправильно — сломает kubectl kustomize локально
resources:
- ../../../../custom/db/redis/helmrelease.yaml
Это также делает структуру самодокументирующейся: заглянув в любой kustomization.yaml, сразу понятно, какие компоненты включены, без подсчёта YAML-файлов.
Стратегии патчей: strategic merge vs JSON patch
Kustomize поддерживает два формата патчей:
Strategic merge patch (используем для большинства патчей): выглядит как частичный YAML-объект, сливается с base по тому же пути:
yaml# Merge: только перечисленные ключи изменяются, остальное из base сохраняется
spec:
values:
master:
replicas: 3
JSON patch (используем для структурных операций — например, добавления kubeConfig):
yaml- op: add
path: /spec/kubeConfig
value:
secretRef:
name: dev-kubeconfig
Используйте strategic merge для переопределения значений. JSON patch — когда нужны add, remove или replace на пути, который может не существовать.
Нюанс со strategic merge для блока values: HelmRelease: Kustomize сливает на уровне YAML-ключей, а не на уровне Helm values. Если в base:
yamlvalues:
redis:
password: placeholder
replicas: 1
А в патче:
yamlvalues:
redis:
password: "${REDIS_PASSWORD}"
Результат:
yamlvalues:
redis:
password: "${REDIS_PASSWORD}"
replicas: 1 # сохранено из base
Работает ожидаемо. Но если нужно удалить ключ — strategic merge не удаляет ключи без директивы $patch: delete. Для удаления ключей используйте JSON patch с op: remove.
Локальная валидация рендера
bash# Полный hub-оверлей
kubectl kustomize fluxcd/projects/dev/kustomization/hub/
# Проверить, что kubeConfig добавлен во все HelmRelease
kubectl kustomize fluxcd/projects/dev/kustomization/hub/ | grep -A 5 "kubeConfig"
# Проверить values конкретного чарта после патчинга
kubectl kustomize fluxcd/projects/dev/kustomization/hub/ | \
yq '. | select(.metadata.name == "elasticsearch") | .spec.values'
# Diff между двумя окружениями
diff \
<(kubectl kustomize fluxcd/projects/dev/kustomization/hub/) \
<(kubectl kustomize fluxcd/projects/test/kustomization/hub/)
Запускайте это перед каждым коммитом, который затрагивает патчи. Ловит YAML-ошибки и конфликты слияния до того, как они попадут в Flux.
Для CI добавьте в GitLab pipeline:
yamlkustomize-validate:
stage: validate
image: bitnami/kubectl:latest
script:
- for env in dev test sre loadgds demo; do
echo "Validating $env...";
kubectl kustomize fluxcd/projects/${env}/kustomization/hub/ > /dev/null;
done
Выполняется ~5 секунд и ловит 90% ошибок патчей до merge.
Добавление нового окружения
- Скопируйте директорию
projects/{env}/существующего окружения - Обновите
namespace:вkustomization.yaml - Обновите имя секрета в патче kubeConfig
- Обновите имена хостов и StorageClass в патчах
- Добавьте новый SealedSecret kubeconfig
- Сделайте коммит и дайте Flux подхватить изменения
Весь процесс занимает 20–30 минут, и новое окружение получает те же версии Helm-чартов и те же values, что и все существующие. Никаких ручных установок Helm, никаких скопированных values-файлов.