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):

  1. Запустить install-k3s.yaml + Cilium для нового узла
  2. Запустить get-kubeconfig.yaml для извлечения и запечатывания kubeconfig
  3. Добавить staging-kustomization.yaml в репо (скопировать с dev, поменять имена)
  4. Раскомментировать запись в корневом kustomization.yaml
  5. Запушить — 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