惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

aimingoo的专栏
aimingoo的专栏
Y
Y Combinator Blog
云风的 BLOG
云风的 BLOG
Microsoft Azure Blog
Microsoft Azure Blog
腾讯CDC
T
The Blog of Author Tim Ferriss
P
Proofpoint News Feed
Hugging Face - Blog
Hugging Face - Blog
博客园_首页
小众软件
小众软件
美团技术团队
Martin Fowler
Martin Fowler
爱范儿
爱范儿
有赞技术团队
有赞技术团队
博客园 - 【当耐特】
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
Microsoft Security Blog
Microsoft Security Blog
宝玉的分享
宝玉的分享
J
Java Code Geeks
B
Blog
V
V2EX
Stack Overflow Blog
Stack Overflow Blog
B
Blog RSS Feed
博客园 - Franky

Все публикации подряд на Хабре

Ловим музу за клавиатуру: как айтишнику стать автором Что умеет Midjourney в 2026? Мой немного грустный разбор этого шикарного инструмента Никто не любит писать тесты, но ИИ может исправить это IPv8 выглядит как мечта. Поэтому почти наверняка не взлетит Производители вернули в продажу материнки с DDR3. Что происходит? Управление агентом с телефона через Telegram теперь в KodaCode От координации к лидерству: как меняется роль руководителя разработки Я сделала родителям бизнес вместо пенсии: зарабатываем 70 тысяч, мама не даёт продать В три раза быстрее приемка товара и оптимизация трудозатрат на 73%: как «РСТ-Инвент» помог Gulliver Group ИИ-шечный мир победил? О влиянии искусственного интеллекта на игропром Кремль снижает давление на Телеграмм пока Европа строит интернет по паспорту Как CEO, CTO и CIO за 8 часов собрали ИИ-директора, который умеет держать позицию под давлением Как (не) потерять домен за выходные Вместо 8 разных VPS: как я организовал практику студентам на одном сервере Почему твой Open Source проект не замечают? R&D: искусство управления неопределенностью в разработке AI-дефляция: вакансий для разработчиков больше, а рост зарплат — худший за 15 лет Мы отдали управление роботами OpenClaw. Что из этого вышло Галактический ID: система идентификации для всех форм разумной жизни Шесть основ бизнес-анализа: начинаем с вопроса «Кто в игре?» Код-ревью, в котором дело не в коде Данные переехали. Команда — нет Системной подход к сдаче OSWE в 2025 Почему комната управления реактором покрашена в цвет морской пены 4 YAML-файла вместо PySpark: как аналитикам строить пайплайны без разработчиков LLM-агент для поиска свободных доменов: автоматизируем подбор Когда, зачем и как правильно начинать новую сессию в Claude Code? Как я заставил нейросеть писать макросы для FreeCAD Анатомия ИИ‑агента для подбора персонала. От тысячи резюме к топ‑10 за минуты Опыт разработчика как экономика внимания
Два Kubernetes-кластера — одна сеть: объединяем через Mes...
Константин · 2026-05-07 · via Все публикации подряд на Хабре

Сложный

9 мин

8.7K

Когда 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.

single network model

single network model

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

multiple networks model

multiple networks model

$ 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-прокси.