Когда Kubernetes-кластеров становится больше одного, инфраструктура начинает жить по новым правилам. Один кластер развёрнут в основном датацентре, второй — в резервной площадке. Сложности начинаются в тот момент, когда этим кластерам нужно взаимодействовать друг с другом. Сервисы в одном кластере должны обращаться к сервисам в другом, приложениям требуется нормальная маршрутизация, а администраторам хочется управлять этим без набора костылей.
В этой статье разберём, как объединить два Kubernetes-кластера в единую сетевую среду, где:
pod'ы могут общаться напрямую между собой
сервисы доступны между кластерами
трафик можно гибко маршрутизировать
В качестве сетевого слоя будем использовать Calico, а для межкластерного взаимодействия сервисов — Istio. Первый даст маршрутизацию и связность, второй — discovery, балансировку и управление трафиком на уровне приложений.
Дисклеймер
Описанный подход на момент написания обкатывается только на тестовых контурах. Под нагрузкой поведение системы не валидировалось, поэтому гарантировать стабильность или предсказуемость в реальных сценариях не могу.
Фактически, сейчас можно опираться лишь на синтетические тесты Istio и рассчитывать на корректную работу Calico. Отдельный риск — наличие критических багов в BGP-реализации, которые в условиях межкластерной маршрутизации могут проявиться не сразу.
Зачем объединять два кластера
Если второй Kubernetes-кластер у вас уже есть, значит причина для этого давно найдена. Обычно вопрос стоит не в необходимости второго кластера, а в том, как заставить два независимых окружения работать удобно и предсказуемо, если на то есть потребность.
Чаще всего несколько кластеров связывают через внешний входной слой: LoadBalancer, Ingress, публичные DNS-имена. Для пользовательских запросов это нормально, но для внутреннего взаимодействия сервисов такой путь выглядит не очень. Один backend в первом кластере обращается ко второму backend во втором кластере, при этом запрос выходит наружу, проходит через ingress-контур и только потом возвращается обратно во внутреннюю сеть.
В этой статье пойдём другим путём — построим привычную внутреннюю связность между кластерами. Такую, где можно обращаться к сервисам через Service, DNS-имена Kubernetes и при необходимости напрямую по Pod IP, будто перед нами не два отдельных кластера, а единая среда.
Архитектура решения

В основе решения лежат два независимых Kubernetes-кластера, каждый со своим control plane, worker-нодами и стандартным набором сервисов. Pod CIDR и Service CIDR в кластерах не должны пересекаться.
Кластер | Pod CIDR | Service CIDR |
|---|---|---|
cluster-a | 10.10.0.0/16 | 10.96.0.0/12 |
cluster-b | 10.20.0.0/16 | 10.97.0.0/12 |
Также стоит заранее проверить MTU, firewall и доступность портов для BGP, Istio east-west gateway. Если в качестве маршрутизации используется BGP, то перед настройкой Calico стоит отдельно согласовать с сетевой командой: какие ASN будут использоваться, где допустим eBGP, где нужен iBGP, предполагаются ли route reflectors, какие префиксы разрешено анонсировать и какие фильтры будут применяться. Брать первый попавшийся ASN — нельзя, может оказаться так, что этот номер уже используется в другой зоне или зарезервирован под существующую схему маршрутизации.
За сетевую часть отвечает Calico. В зависимости от режима он может работать как через BGP, так и через overlay (VXLAN/IPIP). Calico анонсирует маршруты:
по умолчанию — префиксы отдельных Pod’ов (/32);
либо агрегированные блоки, если используется настройка IPPool с заданным blockSize.
BGP-сессии при этом могут устанавливаться:
напрямую между нодами;
через выделенные route reflectors;
либо с внешними сетевыми устройствами (например, ToR-роутерами).
В межкластерном сценарии это означает, что при наличии BGP-пиринга между кластерами (напрямую, через RR или через общую внешнюю сеть) ноды одного кластера начинают знать маршруты до Pod-сетей другого кластера и наоборот. Если в первом кластере Pod имеет адрес 10.10.1.24/32, а во втором — 10.20.1.24/32, то в таблицах маршрутизации появляются соответствующие записи, и трафик начинает ходить напрямую на уровне L3.
По умолчанию Service CIDR через BGP не распространяется. Однако Calico умеет анонсировать сервисные адреса (ClusterIP), если это явно включить через настройки advertise service IP. Если это не сделано, доступ к сервисам обеспечивается через kube-proxy либо eBPF dataplane в Calico, который может работать без kube-proxy.
Настраиваем BGP через Calico
Прежде чем настраивать BGP, полезно хотя бы на базовом уровне понять, как он работает. Сначала посмотрим на глобальную конфигурацию. В каждом кластере задаём свой ASN 64512 и 64513 через ресурс BGPConfiguration:
apiVersion: projectcalico.org/v3 kind: BGPConfiguration metadata: name: default spec: asNumber: 64512 nodeToNodeMeshEnabled: false
Mesh здесь отключаем, почти всегда используем route reflector или внешние маршрутизаторы. Дальше описываем соседство через BGPPeer, указывая IP и ASN противоположной стороны — это может быть ToR-коммутатор, route reflector или ноды второго кластера:
apiVersion: projectcalico.org/v3 kind: BGPPeer metadata: name: peer-to-cluster-b spec: peerIP: 192.168.100.10 asNumber: 64513
Если всё настроено корректно, на сетевом оборудовании появятся маршруты, покрывающие диапазоны 10.10.0.0/16 и 10.20.0.0/16 как правило, в виде более мелких префиксов, а не одного маршрута.
Проверка:
calicoctl node status— смотрим, что BGP-сессии живы,на внешнем маршрутизаторе — появились ли маршруты до pod CIDR,
ip route— проверяем, что маршруты приходят на ноды.
Иногда нужно задать пиринг не для всех нод, а только для конкретных. В этом случае используется селектор:
apiVersion: projectcalico.org/v3 kind: BGPPeer metadata: name: peer-only-workers spec: nodeSelector: role == "worker" peerIP: 192.168.100.10 asNumber: 64513
Когда нод становится >= 100, лучше использовать некоторые ноды как отражатели. Делаем ноду route reflector’ом:
apiVersion: projectcalico.org/v3 kind: Node metadata: name: node-rr-1 spec: bgp: routeReflectorClusterID: 192.168.0.1
Дальше настраиваем пиринг нод к RR. При этом RR-ноды нужно заранее промаркировать label’ом, например route-reflector=true.
apiVersion: projectcalico.org/v3 kind: BGPPeer metadata: name: peer-to-rr spec: nodeSelector: all() peerSelector: route-reflector == "true"
Плюсы: резкое уменьшение количества BGP-сессий. Минусы: нужна отказоустойчивость, минимум 2 отражателя.
Анонсируем Service IP через BGP в Calico
С подами разобрались, но в реальности пользователи/приложения ходят не в Pod IP, а в Service. Calico может анонсировать IP-адреса сервисов (ClusterIP или ExternalIP) в BGP, и внешняя сеть начинает видеть их как обычные маршруты. Для этого используется настройка serviceClusterIPs и/или serviceExternalIPs в ресурсе BGPConfiguration:
apiVersion: projectcalico.org/v3 kind: BGPConfiguration metadata: name: default spec: serviceClusterIPs: - cidr: 10.96.0.0/12
После этого Calico начнёт анонсировать диапазон сервисов в BGP. Анонс идёт с учетом режима — в режиме ExternalTrafficPolicy=Local маршрут будет объявляться только с нод, где есть backend-поды. Если используется ExternalIP, добавляем:
spec: serviceExternalIPs: - cidr: 203.0.113.0/24
Теперь эти адреса тоже будут доступны извне через BGP — без отдельного балансировщика.
Проверка:
calicoctl node status— смотрим, что BGP-сессии живы,на внешнем маршрутизаторе — появились ли маршруты до service CIDR,
ip route— проверяем, что маршруты приходят на ноды.
Отдельно стоит отметить, что это один из способов сделать Kubernetes-сервисы публичными. Вместо классической схемы с Ingress-контроллером или облачным LoadBalancer’ом, вы фактически делегируете публикацию сервисов на уровень сетевой инфраструктуры через BGP.
Дальше логично пойти ещё на уровень выше и посмотреть, как объединять кластеры между собой не только на уровне сети, но и на уровне сервисов. Для этого хорошо подходит Istio — в следующем разделе после склеивания DNS, разберём одну из моделей мультикластерного деплоя и посмотрим, как сервисы могут жить сразу в нескольких кластерах и иметь дополнительные политики отказаустойчивости и балансировки.
Если хочется расширить картину, то помимо Istio существуют и другие варианты: Linkerd с мультикластером через service mirroring, Consul с федерацией кластеров и встроенным сервис-дискавери, а также Cilium с Cluster Mesh без sidecar’ов. Для базовых сценариев можно обойтись и нативным Kubernetes Multi-Cluster Services API, если не требуется полноценный service mesh.
Склеиваем DNS-пространство кластеров через CoreDNS
Как только между кластерами появляется нормальная L3-связанность (спасибо Calico и настроенным маршрутам), возникает естественное желание: пусть сервисы резолвятся так же просто, как внутри одного кластера. Делать это через один общий домен cluster.local — нельзя, возникает коллизия имён, поэтому — развести DNS-зоны по кластерам и связать их через CoreDNS.
cluster-a.local:53 { kubernetes cluster-a.local } cluster-b.local:53 { forward . 10.200.0.10 }
Где 10.200.0.10 — адрес CoreDNS второго кластера. Но чтобы это работало как обычно, одного форвардинга недостаточно — нужно, чтобы сами Pod’ы знали, какие DNS-зоны искать. Для этого нужно поправить настройки kubelet (через --cluster-domain), чтобы в resolv.conf внутри Pod’ов появились search-домены вроде:
search default.svc.cluster-a.local svc.cluster-a.local cluster-a.local cluster-b.local
Если не хочется трогать основной CoreDNS, эту же логику можно вынести в NodeLocal DNSCache. Это по сути тот же CoreDNS, только запущенный на каждой ноде и выступающий как локальный DNS-прокси. В нём можно описать зоны и форвардинг, не изменяя основную конфигурацию.
cluster-a.local:53 { forward . 10.201.0.10 force_tcp } cluster-b.local:53 { forward . 10.200.0.10 force_tcp } .:53 { forward . /etc/resolv.conf prefer_udp }
Здесь каждая зона указывает в какой кластер идти за ответом, а всё остальное уходит в стандартный резолвер linux.
Объединяем кластеры через Istio Multicluster

Нужно понимать, что Istio оперирует понятием сетевой модели — это напрямую влияет на то, как будет ходить трафик. Network — это группа workload’ов, которые могут напрямую достучаться друг до друга без gateway. Отсюда вытекают два базовых сценария: single network, где все Pod’ы между кластерами доступны напрямую и multi-network где прямой связности нет и трафик обязан идти через east-west gateway.

Если Calico уже обеспечивает pod-to-pod между кластерами, можно рассматривать архитектуру как single network — Istio в этом случае будет напрямую балансировать между Pod IP, без обязательного захода через gateway. Но как только появляется изоляция сетей, разные VPC или ограничения инфраструктуры, происходит переход в multi-network модель, где gateway становится обязательным элементом и точкой входа для межкластерного трафика. В этой модели Istio уже не использует Pod IP напрямую — весь межкластерный трафик идёт через east-west gateway, который выступает как точка маршрутизации и mTLS-терминации.

Также разделены модели управления: multi-primary (оба кластера с control plane)

и primary-remote (один управляет вторым).

Сделаем через multi-primary, как самую очевидную и простую реализацию:
$ cat <<EOF > cluster1.yaml apiVersion: install.istio.io/v1alpha1 kind: IstioOperator spec: values: global: meshID: mesh1 multiCluster: clusterName: cluster-a network: network-a EOF
Во втором кластере — cluster-b и network-b. Если сети разные — поднимаем east-west gateway в каждом кластере:
samples/multicluster/gen-eastwest-gateway.sh --network network-a | istioctl install -y -f -
И отдельно экспонируем сервисы через него:
kubectl apply -n istio-system -f samples/multicluster/expose-services.yaml
После этого связываем control plane через remote secrets:
istioctl create-remote-secret --context=cluster-b --name=cluster-b | kubectl apply -f - --context=cluster-a
И в обратную сторону. По сути, мы даём istiod доступ к API другого кластера, чтобы он мог подтягивать endpoints.
Если раньше istioctl proxy-config endpoints <pod> показывал только локальные IP, то теперь там появляются адреса второго кластера или IP его east-west gateway. Сервис reviews.default.svc.cluster.local становится глобальным: Envoy получает список endpoints из всех кластеров и сам решает, куда отправить запрос. По умолчанию Istio использует round-robin между всеми endpoints, независимо от того, в каком кластере они находятся. Чтобы добиться нормального поведения, сначала локально, потом failover, нужно явно настроить DestinationRule с localityLbSetting
trafficPolicy: loadBalancer: localityLbSetting: enabled: true failover: - from: network-a to: network-b
Без этого трафик может гулять между кластерами. В облаках особо критично - так как запросы между датацентрами могут стоить отдельных ресурсов.
Безопасность: mTLS между кластерами
Когда кластеры начинают общаться друг с другом напрямую, вопрос безопасности встает особо остро - трафик начинает выходить за пределы одного доверенного домена. В Istio это решается через mTLS, в основе которого лежит: шифрование, модель идентичности и доверия. Каждый workload получает SPIFFE-идентичность (spiffe://cluster.local/ns/default/sa/default), а сертификаты для этих идентичностей выпускаются Certificate Authority (CA) внутри mesh.
Sidecar-прокси автоматически шифруют весь east-west трафик между сервисами и используют эти сертификаты для взаимной аутентификации. В multi-cluster сам факт сетевой связности между кластерами не означает наличие доверия между ними.
Чтобы Pod из cluster-a мог безопасно ходить в сервис в cluster-b, необходимо настроить доверие между mesh’ами:
либо использовать общий root CA
либо обменяться trust bundle между mesh’ами (например, через SPIFFE Trust Domain Federation)
При корректно настроенном доверии Pod из cluster-a, обращаясь к сервису в cluster-b, устанавливает TLS-соединение, где обе стороны взаимно аутентифицируются по сертификатам и проверяют их через свой trust bundle. Включается через PeerAuthentication:
apiVersion: security.istio.io/v1beta1 kind: PeerAuthentication metadata: name: default namespace: istio-system spec: mtls: mode: STRICT
После этого любой незашифрованный трафик перестаёт проходить. Дополнительно можно ограничить, кто с кем вообще имеет право общаться, через AuthorizationPolicy
apiVersion: security.istio.io/v1beta1 kind: AuthorizationPolicy metadata: name: allow-reviews spec: rules: - from: - source: principals: ["cluster.local/ns/frontend/sa/frontend-sa"]
Здесь доступ даётся по конкретной SPIFFE-идентичности. Но даже если между кластерами есть полная L3-связанность, доступ на уровне приложений не становится автоматически разрешённым. Сеть отвечает за доставку пакетов, а фактическое решение пускать или нет принимается sidecar-прокси.
