Мультикластерный cert-manager с шардингом без лидера

Схема хаб-архитектуры мультикластерного управления сертификатами

Upstream runtime: multicluster-runtime Демонстрационный репозиторий: multi-cluster-cert-manager-example

Мультикластерность чаще всего рассматривается как задача распределения: разослать ресурсы по кластерам и дать каждому из них обрабатывать их локально. Системы вроде Karmada, Open Cluster Management и GitOps-процессы на базе Flux или Argo CD превратили эту модель в надёжный и хорошо понятый подход.

Эта статья — о другой модели.

Здесь мы разберём небольшую конкретную демонстрацию, в которой единая хаб-плоскость управления (hub control plane) активно обрабатывает ресурсы cert-manager.io/v1 Certificate сразу в нескольких подключённых кластерах (downstream clusters), выпускает TLS-секреты и делает всё это безопасно при работе нескольких реплик контроллера — без использования выбора лидера (leader election).

В демонстрации используется multicluster-runtime с шардированным координатором (HRW + Leases), что позволяет масштабировать такую схему.

Что нового: шардированная координация без выбора лидера

Ключевое отличие этой демонстрации — не сам cert-manager, а новая конфигурация шардирования и фенсинга (sharding and fencing) в multicluster-runtime, которая делает эту модель плоскости управления жизнеспособной.

Традиционно менеджеры контроллеров опираются на выбор лидера (leader election), что означает:

  • одна активная реплика;

  • все остальные реплики простаивают;

  • нет безопасного способа разделить работу по кластерам.

В данной демонстрации все реплики хаба остаются активными, а владение управляется явно — для каждого кластера в отдельности.

Это обеспечивается шардированным координатором (экспериментальная функция) со следующими настройками:

  • HRW (rendezvous hashing, рандеву-хеширование) Детерминированно назначает каждый подключённый кластер конкретной реплике хаба на основе текущего набора участников. При масштабировании реплик вверх или вниз перераспределяется лишь минимальное подмножество кластеров.

  • Шард-Leases на каждый кластер Каждый подключённый кластер огорожен (fenced) Kubernetes Lease (например, mcr-shard-<cluster>), что гарантирует единственного владельца-писателя даже при нескольких активных репликах.

  • Peer Leases для отслеживания участников Реплики хаба сигнализируют о своей жизнеспособности через peer Leases, позволяя координатору реагировать на рестарты и сбои без глобального лидера.

  • Настраиваемые временные параметры TTL аренды, интервалы обновления и частота ребалансировки задаются в конфигурации, позволяя гибко балансировать между скоростью реакции и нагрузкой.

В данной демонстрации шардирование включается конфигурацией, эквивалентной следующей:

  • WithShardLease("kube-system", "mcr-shard")

  • WithPerClusterLease(true)

  • пользовательские интервалы аренды и синхронизации

Результат — детерминированное владение, чистый переход при отказе и нулевое двойное согласование без выбора единственного глобального лидера.

Именно это позволяет одному ReplicaSet хаба безопасно управлять множеством подключённых кластеров одновременно.

Структура кода: три небольших элемента

1) Обнаружение подключённых кластеров (файловый провайдер)

Хабу нужен способ перечислить подключённые кластеры и установить с ними соединение. В этой демонстрации мы указываем multicluster-runtime на директорию с kubeconfig-файлами:

kubeconfigDir := os.Getenv("KUBECONFIG_DIR")
if kubeconfigDir == "" {
  kubeconfigDir = "./kubeconfigs"
}
provider, err := fileprovider.New(fileprovider.Options{
  KubeconfigDirs: []string{kubeconfigDir},
  ClusterOptions: []cluster.Option{
    func(o *cluster.Options) { o.Scheme = scheme },
  },
})

Это превращает подключённые кластеры в «динамический инвентарь»: достаточно положить kubeconfig в папку — и хаб начнёт управлять этим кластером.

2) Включение шардирования и фенсинга (новая часть)

Вместо выбора лидера (один активный под) мы держим несколько реплик хаба активными и позволяем координатору назначать владение кластером через HRW + Leases:

mgr, err := mcmanager.New(cfg, provider, manager.Options{Scheme: scheme},
  mcmanager.WithCoordinator(
    sharded.New(
      coordClient,
      ctrl.Log.WithName("sharded-coordinator"),
      // пространство имён и префикс шард-lease
      sharded.WithShardLease("kube-system", "mcr-shard"),
      sharded.WithPerClusterLease(true),

      // Тайминги для демо: быстрый переход при отказе без лишней нагрузки на API.
      sharded.WithLeaseTimings(30*time.Second, 10*time.Second, 2*time.Second),
      sharded.WithSynchronizationIntervals(4*time.Second, 20*time.Second),
    ),
  ),
)

Главная идея: каждый подключённый кластер имеет единственного «владельца» — реплику хаба, а владение огорожено Lease.

3) Один reconciler, работающий со всеми кластерами

Reconciler выглядит как обычный код controller-runtime, за исключением того, что каждый запрос содержит ClusterName и мы получаем нужный клиент для нужного кластера:

if err := mcbuilder.ControllerManagedBy(mgr).
  Named("multicluster-certificates").
  For(&cmapi.Certificate{}).
  Complete(mcreconcile.Func(func(ctx context.Context, req mcreconcile.Request) (ctrl.Result, error) {
    cl, err := mgr.GetCluster(ctx, req.ClusterName)
    if err != nil { return ctrl.Result{}, err }
    c := cl.GetClient()

// ...читаем Certificate и записываем TLS Secret в ЭТОТ подключённый кластер...

Вот паттерн «активного мультикластера» в одной строке: req.ClusterNameGetCluster()GetClient()

Что делает эта демонстрация

Мы поднимем три kind-кластера:

  • kind-mgmt — хаб / плоскость управления;

  • kind-a и kind-b — подключённые кластеры.

Хаб:

  • обнаруживает подключённые кластеры через kubeconfig-файлы;

  • отслеживает ресурсы Certificate в каждом подключённом кластере;

  • выпускает сертификат (самоподписанный в целях демонстрации);

  • записывает полученный TLS-секрет обратно в тот же подключённый кластер.

Никаких контроллеров cert-manager в kind-a или kind-b нет.

Это активное мультикластерное согласование (reconciliation), а не распределение или репликация.

Почему это интересно

cert-manager — хороший стресс-тест для мультикластерных архитектур, потому что:

  • выпуск сертификатов нередко требует глобальной координации (ограничения частоты запросов, общие ACME-аккаунты);

  • волны обновлений сертификатов — это реальная проблема;

  • запускать контроллеры в каждом кластере не всегда желательно.

Большинство существующих подходов решают задачу распределения. Эта демонстрация сосредоточена на владении, фенсинге и корректности при управлении множеством API-серверов из одного контроллера.

Небольшое отступление

В этой демонстрации вы встретите nginx. Не потому что он чем-то примечателен, — просто он знаком, предсказуем и до сих пор появляется в большинстве примеров.

Впрочем…​ nginx в 2026-м? Рановато для ностальгии?

Обзор настройки

Вам понадобятся:

  • kind, kubectl, docker, go (1.24+);

  • демонстрационный репозиторий: github.com/zachsmith1/multi-cluster-cert-manager-example

1) Создайте три kind-кластера

kind create cluster --name mgmt
kind create cluster --name a
kind create cluster --name b

Экспортируйте kubeconfig-файлы для подключённых кластеров, чтобы хаб мог обращаться к ним изнутри kind-mgmt:

mkdir -p ./kubeconfigs
kind get kubeconfig --name a --internal > ./kubeconfigs/a.kubeconfig
kind get kubeconfig --name b --internal > ./kubeconfigs/b.kubeconfig

2) Установите CRD cert-manager в подключённых кластерах (только CRD)

Нам нужна поверхность API, но не сами контроллеры:

kubectl --context kind-a apply \
  -f https://github.com/cert-manager/cert-manager/releases/download/v1.14.5/cert-manager.crds.yaml
kubectl --context kind-b apply \
  -f https://github.com/cert-manager/cert-manager/releases/download/v1.14.5/cert-manager.crds.yaml

3) Соберите и разверните хаб-контроллер

Соберите образ и загрузите его в kind-mgmt:

docker build -t mcr-hub:dev .
kind load docker-image --name mgmt mcr-hub:dev

Задеплойте контроллер и подмонтируйте kubeconfig-файлы подключённых кластеров:

kubectl --context kind-mgmt apply -f manifests/hub-controller.yaml
kubectl --context kind-mgmt -n mcr-hub create secret generic downstream-kubeconfigs \
  --from-file=./kubeconfigs/a.kubeconfig \
  --from-file=./kubeconfigs/b.kubeconfig

Дождитесь готовности обеих реплик:

kubectl --context kind-mgmt -n mcr-hub rollout status deploy/mcr-hub

Примечание: обе реплики остаются активными. Выбор лидера не используется.

4) Создайте Certificate и HTTPS-нагрузки в подключённых кластерах

Примените ресурсы Certificate:

kubectl --context kind-a apply -f manifests/cert-a.yaml
kubectl --context kind-b apply -f manifests/cert-b.yaml

Разверните nginx-нагрузку, монтирующую TLS-секрет, созданный хабом:

kubectl --context kind-a apply -f manifests/nginx.yaml
kubectl --context kind-b apply -f manifests/nginx.yaml

Убедитесь, что секреты созданы:

kubectl --context kind-a get secret demo-tls
kubectl --context kind-b get secret demo-tls

Выполните curl изнутри каждого кластера:

kubectl --context kind-a run curl --image=curlimages/curl --rm -i --restart=Never -- \
  curl -k https://nginx-tls.default.svc.cluster.local
kubectl --context kind-b run curl --image=curlimages/curl --rm -i --restart=Never -- \
  curl -k https://nginx-tls.default.svc.cluster.local

5) Главное: шардирование и владение

При работе двух реплик хаба каждым подключённым кластером в один момент времени владеет только одна реплика.

Шардированный координатор хранит это состояние в виде Leases в кластере хаба:

kubectl --context kind-mgmt -n kube-system get lease | grep mcr-

Что происходит под капотом:

  • HRW (рандеву-хеширование) детерминированно назначает каждый кластер конкретному участнику;

  • шард-Leases на каждый кластер ограничивают право собственности;

  • при изменении числа реплик перераспределяется минимум кластеров;

  • двойное согласование исключено без единого глобального лидера.

Именно это делает активные мультикластерные контроллеры практически применимыми.

Что намеренно не охватывает эта демонстрация

  • ACME, DNS-01, HTTP-01, Vault и прочие методы выпуска (логика выпуска самоподписанная, чтобы сосредоточиться на архитектуре);

  • веб-хуки и cainjector;

  • копирование секретов между кластерами (всё хранится локально в каждом кластере).

Главное здесь — форма согласования, а не конкретный эмитент.

Как это отображается на реальный форк cert-manager

Чтобы превратить это в полноценный мультикластерный cert-manager:

  • сохраните ту же обвязку multicluster-runtime;

  • сохраните шардированный координатор;

  • замените самоподписанный эмитент на реальные пайплайны cert-manager.

Ничего в мультикластерной плоскости управления менять не нужно.

Заключительные мысли и открытый вопрос

Большинство мультикластерных систем сегодня останавливаются на распределении. Это работает — до тех пор, пока координация не становится важной.

Если вы строите:

  • централизованные контроллеры,

  • hub-and-spoke плоскости управления,

  • или что угодно, что должно корректно работать сразу со многими кластерами, —

мне было бы интересно узнать, как вы к этому подходите. Что сработало, что оказалось болезненным и где эта модель подходит, а где нет.

Комментарии и обратная связь приветствуются.

© 2026 meganuke