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.ClusterName → GetCluster() → 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 плоскости управления,
-
или что угодно, что должно корректно работать сразу со многими кластерами, —
мне было бы интересно узнать, как вы к этому подходите. Что сработало, что оказалось болезненным и где эта модель подходит, а где нет.
Комментарии и обратная связь приветствуются.