FluxCD hub-and-spoke: один кластер управляет двумя другими
Published: 2026-02-09
Наша GitOps-топология — три k3s-кластера: infra, dev и test. FluxCD работает только на infra. Infra-кластер выступает hub: читает Git-репозиторий и применяет HelmRelease не только к себе, но и к dev и test через удалённые kubeconfig-ссылки. Это и есть паттерн hub-and-spoke.
Этот пост — полная настройка, операционные нюансы, которые мы обнаружили за шесть месяцев работы с этой топологией, и команды для отладки, которые не раз нас выручали.
Зачем hub-and-spoke
Альтернатива — запустить Flux на каждом кластере. Это нормально, когда кластеры независимы, но у нас общие версии чартов, пути в Vault и конфиги мониторинга. При hub-and-spoke есть один источник истины и одно место для отслеживания дрейфа.
С точки зрения эксплуатации: если dev в рассинхроне — смотришь статус Kustomization на infra-кластере, а не на dev.
Более тонкое преимущество — управление жизненным циклом. При обновлении самого Flux делаешь это один раз на infra. Все spoke наследуют обновление через механизм kubeconfig без прямого доступа к ним. Spoke-кластеры могут быть полностью изолированы от интернета — доступ к Git нужен только hub-кластеру.
Компромиссы
| Аспект | Hub-and-spoke | Flux на каждом кластере |
|---|---|---|
| Обновление Flux | одно место | каждый кластер отдельно |
| Изоляция отказов | отказ hub — реконсиляция spoke останавливается | независимо |
| Общий конфиг | легко, единая base | нужны соглашения по синхронизации репо |
| Доступ spoke к интернету | не требуется | нужен доступ к Git |
| Аудит | единый кластер для всех событий | распределённо |
Если кластеры находятся в разных доменах безопасности (разные команды, разные требования compliance), лучше Flux на каждом кластере. Hub-and-spoke хорош, когда одна команда эксплуатирует несколько кластеров с большим пересечением конфигов.
Структура директорий
fluxcd/
kustomization.yaml # корень — перечисляет infra и spoke kustomization
infra-kustomization.yaml # HelmRelease для infra-кластера (самого себя)
dev-kustomization.yaml # HelmRelease для dev-кластера
test-kustomization.yaml # HelmRelease для test-кластера (закомментировано до готовности)
base/
monitoring/ # общие конфиги чартов
external-secrets/
...
custom/
monitoring/ # переопределения для конкретных окружений
...
Директория base/ содержит манифесты HelmRelease без поля spec.kubeConfig. Файлы kustomization для каждого кластера добавляют его через JSON-патчи. Это делает base переносимой — если когда-нибудь захочется запустить Flux напрямую на dev, просто убираешь патч с kubeConfig.
Директория custom/ содержит переопределения значений для конкретного окружения. Например, у dev может быть replicas: 1, а у test — replicas: 2. Это strategic merge патчи Kustomize поверх base.
Как ресурсы направляются на spoke
Каждый HelmRelease, который должен оказаться в dev или test, получает spec.kubeConfig через патч Kustomization:
yaml# dev-kustomization.yaml
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
name: dev
namespace: flux-system
spec:
interval: 10m
path: ./fluxcd/custom
sourceRef:
kind: GitRepository
name: flux-system
kubeConfig:
secretRef:
name: dev-kubeconfig # SealedSecret, расшифрован Sealed Secrets на infra
patches:
- patch: |
- op: add
path: /spec/kubeConfig
value:
secretRef:
name: dev-kubeconfig
target:
kind: HelmRelease
Flux применяет манифест HelmRelease на infra, но Helm controller обращается к API-серверу dev через kubeconfig из секрета dev-kubeconfig.
Поле kubeConfig в Kustomization обрабатывает ресурсы, не являющиеся HelmRelease (ConfigMap, Secret и т.д.). Патч на HelmRelease обеспечивает, что Helm controller тоже использует remote kubeconfig при выполнении Helm-операций.
Зачем патч нужен в дополнение к kubeConfig
Поле kubeConfig в Kustomization говорит контроллеру Kustomize применять ресурсы к удалённому кластеру. Но HelmRelease — особый случай: Helm controller реконсилирует их отдельно. Без патча, добавляющего spec.kubeConfig в каждый HelmRelease, Helm controller выполнял бы операции на локальном (infra) кластере.
Это типичная ловушка при первоначальной настройке hub-and-spoke: Kustomization показывает Ready=True, а чарты оказываются не на том кластере.
Секрет с kubeconfig
kubeconfig извлекается с dev-узла, адрес сервера заменяется на внешний IP узла, затем запечатывается с помощью контроллера Sealed Secrets на infra-кластере:
bash# 1. Извлечь и исправить адрес
kubectl --kubeconfig /etc/rancher/k3s/k3s.yaml config view --raw \
| sed 's/127.0.0.1/192.168.1.20/g' > /tmp/dev.yaml
# 2. Создать манифест Secret
kubectl create secret generic dev-kubeconfig \
--namespace=flux-system \
--from-file=value=/tmp/dev.yaml \
--dry-run=client -o yaml > /tmp/dev-secret.yaml
# 3. Запечатать публичным сертификатом infra
kubeseal --cert .kubeconfigs/sealed-secrets-infra.pem \
--format yaml \
< /tmp/dev-secret.yaml \
> fluxcd/dev-kubeconfig-sealed.yaml
# 4. Закоммитить и запушить
git add fluxcd/dev-kubeconfig-sealed.yaml && git commit -m "add dev kubeconfig" && git push
Flux обнаруживает новый файл, контроллер Sealed Secrets на infra расшифровывает его, и hub сразу получает доступ к dev-кластеру.
Важен namespace
Secret с kubeconfig должен быть в том же namespace, что и Kustomization, ссылающаяся на него — в большинстве настроек это flux-system. Если секрет в другом namespace, Flux не найдёт его и запишет в лог secret "dev-kubeconfig" not found.
Ротация сертификатов
k3s ротирует CA кластера только по явному запросу (например, k3s certificate rotate). Однако при пересборке dev-кластера CA меняется и сохранённый kubeconfig становится недействительным. После пересборки кластера:
bash# Повторно извлечь, запечатать, закоммитить
kubectl --kubeconfig /etc/rancher/k3s/k3s.yaml config view --raw \
| sed 's/127.0.0.1/192.168.1.20/g' > /tmp/dev.yaml
kubectl create secret generic dev-kubeconfig \
--namespace=flux-system \
--from-file=value=/tmp/dev.yaml \
--dry-run=client -o yaml \
| kubeseal --cert .kubeconfigs/sealed-secrets-infra.pem --format yaml \
> fluxcd/dev-kubeconfig-sealed.yaml
git add fluxcd/dev-kubeconfig-sealed.yaml && git commit -m "rotate dev kubeconfig" && git push
Порядок через dependsOn
Некоторые HelmRelease зависят от других, которые должны быть задеплоены первыми (например, CRD мониторинга перед PrometheusRules). Используйте spec.dependsOn:
yamlspec:
dependsOn:
- name: kube-prom-stack # имя объекта Kustomization, не директории
Частое заблуждение: dependsOn ссылается на имя объекта Kustomization, а не на путь или имя файла.
Зависимости между spoke
Нельзя создать dependsOn, пересекающий границы spoke. Если PrometheusRules на dev зависят от CRD kube-prom-stack на infra, нужно установить CRD отдельно на dev (через выделенный CRD HelmRelease) и сделать Kustomization PrometheusRules зависимым от него.
Попытка сослаться на Kustomization из другого kubeConfig в dependsOn молча завершится неудачей — Flux будет ждать вечно, потому что ищет именованную Kustomization в локальном namespace flux-system.
Проверка состояния spoke
Из infra-кластера:
bash# Все Kustomization
flux get kustomizations -A
# HelmRelease на dev spoke
flux get helmreleases -n dev --kubeconfig .kubeconfigs/infra.yaml
# Принудительная синхронизация
flux reconcile kustomization dev --kubeconfig .kubeconfigs/infra.yaml
# Наблюдение за reconciliation в реальном времени
flux get kustomization dev -w
Дрейф на dev отображается как failed HelmRelease в выводе flux get hr — заходить по SSH на dev-узел не нужно.
Поля статуса для мониторинга
bashkubectl get kustomization dev -n flux-system -o yaml | grep -A5 "conditions:"
Ключевые типы conditions:
Ready: True— все ресурсы применены успешноReconciling: True— в процессеStalled: True— заблокировано, обычно ожидание зависимости
Для HelmRelease добавьте --verbose к flux get для просмотра сообщения Helm:
bashflux get helmrelease monitoring -n flux-system --verbose
Suspend и resume spoke
Во время обслуживания кластера (обновление ОС узла и т.д.) приостановите spoke, чтобы Flux не мешал вашим изменениям:
bashflux suspend kustomization dev
# ... обслуживание ...
flux resume kustomization dev
Flux немедленно выполнит reconcile после resume, вернув кластер в желаемое состояние. Любые ручные изменения, сделанные во время обслуживания, будут отменены — это намеренное поведение GitOps.
Чтобы исключить конкретные ресурсы из reconciliation без приостановки всей kustomization, аннотируйте их:
bashkubectl annotate helmrelease myapp -n app kustomize.toolkit.fluxcd.io/reconcile=disabled
Добавление нового spoke
Если добавляется новый кластер (например, staging):
- Запустить
install-k3s.yaml+ Cilium для нового узла - Запустить
get-kubeconfig.yamlдля извлечения и запечатывания kubeconfig - Добавить
staging-kustomization.yamlв репо (скопировать с dev, поменять имена) - Раскомментировать запись в корневом
kustomization.yaml - Запушить — Flux подхватит меньше чем за минуту
Время от «кластер готов» до «все приложения развёрнуты Flux» — обычно 3–5 минут. Узкое место — загрузка чартов при первом pull, а не реконсиляция Flux.
Решение типичных проблем
HelmRelease оказывается на infra вместо spoke
- Причина:
spec.kubeConfigне в HelmRelease (патч не применён) - Решение: проверить, что патч
target.kind: HelmReleaseкорректен, а путь./fluxcd/customсодержит нужные HelmRelease
Kustomization Ready, но чартов нет на spoke
- Причина: Kustomization применила ресурсы к spoke, но HelmRelease нужен отдельный
spec.kubeConfig - Решение: добавить патч kubeConfig в Kustomization
secret "dev-kubeconfig" not found
- Причина: Secret в неправильном namespace или контроллер Sealed Secrets не смог расшифровать
- Решение: проверить
kubectl get sealedsecret -n flux-system, проверить логи контроллера Sealed Secrets
Reconciliation завис на Progressing
- Причина: зависимый ресурс не готов (CRD ещё не установлен и т.д.)
- Решение:
flux get hr -Aдля поиска заблокированного релиза, проверить events черезkubectl describe helmrelease