Настраиваем доступ к сервисам и подам Kubernetes в облаке

от автора

Сталкивались ли вы с ситуацией, когда нужно получить доступ к сети подов или сервисов в кластере Kubernetes? Кто-то может возразить, что маппинга портов через port-forward или использования NodePort вполне достаточно, однако часто это не так. Список реальных кейсов велик, рассмотрим несколько для примера:

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

  • используются внешние балансировщики (например, SIP/RTP-прокси для телефонии или антиспам-решения), когда они не могут быть размещены внутри Kubernetes;

  • присутствуют аппаратные решения вроде NGFW от именитых производителей.

В тексте мы в первую очередь будем опираться на практику Managed Kubernetes-сервиса Selectel, но он также будет полезен, если у вас свой K8s с CNI Calico. Cразу оговоримся: для других CNI «рецепты» из текста не подойдут. 

Используйте навигацию, если не хотите читать текст целиком:

Если было бы интересно ознакомиться с рекомендациями для Cilium — пишите в комментариях! Скоро мы добавим выбор CNI в управляемом K8s, и Cilium в списке будет первым. Stay tuned! 🙂

Сетевая модель Kubernetes

Прежде чем перейти к практике, вспомним базовую сетевую модель Kubernetes — особенно важную часть, связанную с внешним (North-South) трафиком и публикацией сервисов.

Типовая схема взаимодействия включает:

  • поды, объединенные в ClusterIP-сервисы;

  • (опционально) Ingress-контроллер, обеспечивающий маршрутизацию на L7-уровне;

  • NodePort и LoadBalancer-сервисы для выхода за пределы кластера;

  • протоколы маршрутизации, такие как BGP или gARP, на границе с физической сетью.

Ниже — пример, как Kubernetes-ресурсы «наслаиваются» друг на друга, чтобы обеспечить обмен трафика между «внешним миром» и кластером (North-South):

Облачная инфраструктура для ваших проектов

Виртуальные машины в Москве, Санкт-Петербурге и Новосибирске с оплатой по потреблению.

Подробнее →

Пример инфраструктуры

Представим, что в облаке у вас развернута небольшая инфраструктура — виртуальная машина и managed-кластер Kubernetes. Все это показано на схеме ниже:

Топология инфраструктуры.

Топология инфраструктуры.

Казалось бы, ничего не мешает посмотреть, какие сети использует K8s для подов и сервисов, и добавить маршруты вручную. Но что, если у нас десятки или сотни нод? Работает автоскейлер, и новые ноды добавляются и удаляются из кластера? Узлов, на которые нужно добавить маршруты, становится все больше с ростом инфраструктуры?

Очевидно, для уменьшения объема работы администратора нужен обмен маршрутной информацией в реальном времени. Значит, ищем сервис с поддержкой протокола динамической маршрутизации. В нашем случае этим протоколом будет BGP, так как используемый в Managed Kubernetes-сервисе Calico CNI поддерживает именно его.

Задача: настроить обмен маршрутами между воркер-нодами (worker node) и виртуальной машиной по BGP. Ноды будут передавать маршруты ВМ до сервисов и подов, а ВМ — анонсировать префикс VPN-сети. И главное — при появлении новых воркер-нод в кластере подсети подов будут анонсироваться автоматически. Осталось только все настроить. Приступим!

Настройка компонентов

На управляющей машине

На любимом ноутбуке или десктопе (или может быть, виртуалке) потребуется стандартный набора инструментов: bash, awk, curl, jqOpenStack CLI и любимый текстовый редактор. Дополнительно рекомендуем установить calicoctl — статически скомпилированный бинарный файл на Go, который достаточно скопировать и запустить. Подробная инструкция по установке — в официальной документации.  Далее приведем «выжимки» для разных ОС.

Linux

curl -sSL https://github.com/projectcalico/calico/releases/

Windows

Invoke-WebRequest -Uri "https://github.com/projectcalico/calico/releases/download/v3.29.4/calicoctl-windows-amd64.exe" -OutFile "calicoctl.exe"

MacOS

Скомпилированный бинарный файл:

curl -sSL https://github.com/projectcalico/calico/releases/download/v3.29.4/calicoctl-darwin-amd64 -o calicoctl

brew:

brew install calicoctl

calicoctl необязателен, так как начиная с версии 3.19 конфигурацией Calico можно управлять через CustomResourceDefinitions. Однако calicoctl все еще нужен для следующих подкоманд:

  • сalicoctl node,

  • calicoctl ipam,

  • calicoctl convert,

  • calicoctl version.

Инфраструктура

Если вы используете облачную инфраструктуру (как в нашем примере), важно учитывать возможную фильтрацию трафика со стороны провайдера. Например, в облаке Selectel действует IP/MAC-антиспуфинг — и для корректной работы Kubernetes-кластера нужно добавить разрешенные IP-адреса на сетевые порты.

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

1. Проверяем, включена ли фильтрация трафика.

openstack network show <cloud_network_name> -c port_security_enabled -f value

2. Если фильтрация включена — продолжаем настройку. Добавляем подсеть сервисов в разрешенные «пары» для порта воркер-ноды MKs. Выясняем, какая подсеть используется для подов, фиксируем:

Обратите внимание: в Selectel MKs подсети фиксированы.

  • 10.10.0.0/16 — для подов.

  • 10.96.0.0/12 — для сервисов.

kubectl -n kube-system get cm kube-proxy -o jsonpath='{.data.config\.conf}' | awk '/^clusterCIDR/ { print $2 }'

3. Выясняем, какая подсеть используется для сервисов, фиксируем:

echo '{"apiVersion":"v1","kind":"Service","metadata":{"name":"one"},"spec":{"clusterIP":"198.51.100.1","ports":[{"port":80}]}}' | kubectl apply -f - 2>&1 | sed 's/.*valid IPs is //'

4. Разрешаем дополнительные IP-адреса на портах.

Важно! Сеть подов уже добавляется в разрешенные при создании в Selectel кластера MKs. Выбираем один из вариантов.

Вручную

Фиксируем идентификаторы портов ВМ:

openstack server list -c Name -c Networks openstack port list

Сопоставляем их по IP-адресам. Далее для каждого порта воркер-ноды выполняем:

openstack port set --allowed-address ip-address=<k8s_service_cidr> <worker_node_port_uuid>

Полуавтоматически

Применим небольшой shell-скрипт:

#!/bin/bash k8s_service_cidr=$(echo '{"apiVersion":"v1","kind":"Service","metadata":{"name":"one"},"spec":{"clusterIP":"198.51.100.1","ports":[{"port":80}]}}' | kubectl apply -f - 2>&1 | sed 's/.*valid IPs is //') vm_ips=$(openstack server list --long --tags mks_cluster=true -c Networks -f json | jq -r '.[].Networks[][0]') port_ids=$(openstack port list --any-tags mks_cluster=true --long -f json | jq -r --arg nodes_ip "$vm_ips" '.[] | select(."Fixed IP Addresses"[0].ip_address as $ips | ($nodes_ip|split("\n")) | index($ips)) | .ID') for id in $port_ids   do     openstack port set --allowed-address ip-address=${k8s_service_cidr} ${id} echo "Port AAPs: " openstack port show -c allowed_address_pairs ${id}   done

В кластере MKs

Для CNI Calico можно использовать два подхода к настройке динамической маршрутизации:

  1. «Установка на не-кластерный узел» (например, в Docker-контейнере).

  2. BGP-пиринг с ВМ, сервером или маршрутизатором.

Мы рассмотрим второй вариант как более универсальный, так как его можно применить и в случае использования программного или аппаратного маршрутизатора. 

1. Просматриваем глобальную конфигурацию BGP:

kubectl get bgpconfiguration default -o json | jq 'pick(.apiVersion, .kind, .spec)'

Пример вывода:

{   "apiVersion": "crd.projectcalico.org/v1",   "kind": "BGPConfiguration",   "spec": { "asNumber": 65065, "logSeverityScreen": "Info", "nodeToNodeMeshEnabled": false   } }

Фиксируем AS-номер, который используется в кластере (в выводе выще — AS 65065). Он потребуется для дальнейшей конфигурации.

2. Добавляем глобальный BGP-пир:

cat <<EOF | kubectl apply -f - apiVersion: crd.projectcalico.org/v1 kind: BGPPeer metadata:   name: vpn-server spec:   peerIP: 10.15.1.50   asNumber: 64999   keepOriginalNextHop: true EOF
  • 10.15.1.50 — наша ВМ с VPN и bird/frr.

  • keepOriginalNextHop — важный параметр. Без него на ВМ окажется несколько маршрутов с одинаковыми метриками и next-hop в виде воркер-ноды кластера MKs. Корректность работы будет под вопросом.

Добавляем подсеть сервисов для анонсирования.

При использовании kubectl:

kubectl patch bgpconfiguration default -p '{"spec":{"serviceClusterIPs":[{"cidr": "10.96.0.0/12"}]}}' --type='merge'

Через calicoctl:

calicoctl patch bgpconfiguration default -p '{"spec":{"serviceClusterIPs":[{"cidr": "10.96.0.0/12"}]}}' --allow-version-mismatch

Директиву —allow-version-mismatch используйте с осторожностью. Можно использовать референс из официальной документации. Важно: serviceClusterIPs применяется только в конфигурации по умолчанию (default). Если вы создадите дополнительный конфиг, параметр serviceClusterIPs будет проигнорирован в ней.

Еще один момент: в базовой конфигурации подсеть сервисов по BGP не анонсируется. Значение serviceClusterIPs — это пустой список.

Добавляем BGP-фильтры (опционально). Пока фильтров нет, но в будущем их важно добавить, чтобы не принимать лишние или ошибочные маршруты. Подробнее — в официальной документации.

На виртуальной машине

ВМ с VPN-сервером находится в облаке. IP-адрес по схеме — 10.15.1.50. Подключаемся к виртуальной машине. Напомним, что ОС сервера — Ubuntu 22.04.5 LTS.

Установка ПО

Для настройки маршрутизации с поддержкой BGP выберите один из двух популярных вариантов: Bird 2 или FRRouting. Оба подходят, выбор зависит от ваших предпочтений.

Bird 2:

sudo add-apt-repository ppa:cz.nic-labs/bird sudo apt update && sudo apt install bird2

FRRouting:

curl -s https://deb.frrouting.org/frr/keys.gpg | sudo tee /usr/share/keyrings/frrouting.gpg > /dev/null export FRRVER="frr-stable" echo deb '[signed-by=/usr/share/keyrings/frrouting.gpg]' https://deb.frrouting.org/frr $(lsb_release -s -c) $FRRVER | sudo tee -a /etc/apt/sources.list.d/frr.list sudo apt update sudo apt install frr frr-pythontools

Конфигурация ПО

BGP-сессии строятся не с мастер-нодами, а только с воркер-нодами. Мастер-ноды отклоняют подключения по BGP.

Первый вариант: bird2

1. Приводим конфигурационный файл /etc/bird/bird.conf к следующему виду:

log syslog all; protocol device { } protocol direct {     disabled;       # Disable by default     ipv4;           # Connect to default IPv4 table     ipv6;           # ... and to default IPv6 table } protocol kernel {     ipv4 {          # Connect protocol to IPv4 table by channel         export all; # Export to protocol. default is export none     }; } protocol kernel {     ipv6 { export all; }; } protocol static {     ipv4;           # Again, IPv4 channel with default options }  filter out_prefix {     if ( net ~ [172.19.7.0/24] ) then {         accept;     }      else reject; } template bgp mks_worker_nodes {     #bfd;     description "MKS Worker Nodes";     local 10.15.1.50 as 64999;     neighbor as 65065;     ipv4 {        #next hop self bgp;        import all;        export filter out_prefix;     };     debug { states, routes, filters, interfaces, events }; }  protocol bgp worker_node_1 from mks_worker_nodes {     description "my-mks-stage-cluster-node-0wt9d";     neighbor 10.15.1.11; }   protocol bgp worker_node_2 from mks_worker_nodes {     description "my-mks-stage-cluster-node-c12uj";     neighbor 10.15.1.12; } protocol bgp worker_node_3 from mks_worker_nodes {     description "my-mks-stage-cluster-node-9vpor";     neighbor 10.15.1.13; }

2. Задействуем и запускаем Bird:

sudo systemctl enable bird # по умолчанию Ubuntu запускает bird после установки # поэтому перезапускаем сервис sudo systemctl restart bird

3. Проверяем статус:

birdcl show status

Ожидаемый вывод:

BIRD 2.17.1 ready. BIRD 2.17.1 Router ID is 10.15.1.50 Hostname is vpn-server Current server time is 2025-04-30 14:47:02.497 Last reboot on 2025-04-30 14:43:11.050 Last reconfiguration on 2025-04-30 14:43:11.050 Daemon is up and running

4. Проверяем, что демон прослушивает порт:

ss -ptln | grep 179

Пример вывода:

LISTEN 0  8        0.0.0.0:179   0.0.0.0:*users:(("bird",pid=4740,fd=8))

Второй вариант: frrouting.

1. Убеждаемся, что bgpd для frr задействован:

grep bgp /etc/frr/daemons  bgpd=yes bgpd_options="   -A 127.0.0.1" # bgpd_wrap="/usr/bin/daemonize /usr/bin/mywrapper"

2. Приводим конфигурацию FRR (/etc/frr/frr.conf) к следующему виду:

frr version 10.3 frr defaults traditional hostname vpn-server log syslog informational no ipv6 forwarding service integrated-vtysh-config ! ip prefix-list default_mks description "MKS default prefixes for pods and services" ip prefix-list default_mks seq 10 permit 10.10.0.0/16 ip prefix-list default_mks seq 11 permit 10.96.0.0/12 ip prefix-list default_mks seq 1000 deny any ip prefix-list my_vpn seq 10 permit 172.19.7.0/24 ip prefix-list my_vpn seq 1000 deny any ! ip router-id 10.15.1.50 ! router bgp 64999 bgp router-id 10.15.1.50 bgp log-neighbor-changes no bgp network import-check neighbor 10.15.1.11 remote-as 65065 neighbor 10.15.1.11 description my-mks-stage-cluster-node-0wt9d neighbor 10.15.1.11 interface eth0 neighbor 10.15.1.12 remote-as 65065 neighbor 10.15.1.12 description my-mks-stage-cluster-node-c12uj neighbor 10.15.1.12 interface eth0 ! address-family ipv4 unicast   network 172.19.7.0/24   redistribute local   neighbor 10.15.1.11 prefix-list default_mks in   neighbor 10.15.1.11 prefix-list my_vpn out   neighbor 10.15.1.12 prefix-list default_mks in   neighbor 10.15.1.12 prefix-list my_vpn out exit-address-family exit !

Используйте vtysh, чтобы конфигурировать frr императивно. Это может быть удобно, если вы привыкли настраивать сетевое оборудование через CLI.

no bgp network import-check — параметр, который отключает проверку наличия маршрута в RIB перед анонсом. Можно не использовать, но тогда важно заранее проверить порядок запуска демона и VPN-сервиса. 

3. Задействуем и запускаем frr:

sudo systemctl enable frr # по умолчанию в Ubuntu systemd запускает frr # после установк поэтому перезапускаем сервис sudo systemctl restart frr

4. Проверяем, что демон прослушивает порт:

ss -ptln | grep 179 LISTEN 0  4096     0.0.0.0:179   0.0.0.0:*users:(("bgpd",pid=799,fd=22))        LISTEN 0  4096        [::]:179      [::]:*users:(("bgpd",pid=799,fd=23)) 

Пример вывода:

LISTEN 0  4096     0.0.0.0:179   0.0.0.0:*users:(("bgpd",pid=799,fd=22))        LISTEN 0  4096        [::]:179      [::]:*users:(("bgpd",pid=799,fd=23))  

Проверка работоспособности

Похоже, все настроено. Однако важно убедиться в корректности работы.

1. Просмотрим информацию по BGP. Выполняем на ВМ c VPN.

frrouting:

sudo vtysh -c 'show bgp summary' sudo vtysh -c 'show bgp ipv4 all' sudo vtysh -c 'show ip route bgp'

bird2:

sudo birdc show route sudo birdc show status

2. Развернем тестовый echoserver. На управляющей машине выполним:

cat <<EOF | kubectl apply -f - apiVersion: apps/v1 kind: Deployment metadata:   name: cilium-echoserver   namespace: default spec:   replicas: 3   selector:     matchLabels:       app: cilium-echoserver   template:     metadata:       labels:         app: cilium-echoserver     spec:       containers:         - name: cilium-echoserver           image: cilium/echoserver:latest           imagePullPolicy: IfNotPresent           ports:             - containerPort: 8088               protocol: TCP           env:             - name: PORT               value: "8088"       affinity:         podAntiAffinity:           requiredDuringSchedulingIgnoredDuringExecution:             - labelSelector:                 matchExpressions:                   - key: "app"                     operator: In                     values:                       - cilium-echoserver               topologyKey: "kubernetes.io/hostname" EOF

Обратите внимание, что для развертывания было указано три реплики.

3. Проверим:

kubectl get pods -o json | jq '.items[]|pick(.kind, .metadata.name, .status.hostIP, .status.podIP)'   {   "kind": "Pod",   "metadata": { "name": "cilium-echoserver-795b4455-47v9k"   },   "status": { "hostIP": "10.15.1.13", "podIP": "10.10.224.45"   } } {   "kind": "Pod",   "metadata": { "name": "cilium-echoserver-795b4455-xcnhb"   },   "status": { "hostIP": "10.15.1.11", "podIP": "10.10.73.249"   } } {   "kind": "Pod",   "metadata": { "name": "cilium-echoserver-795b4455-xf5r4"   },   "status": { "hostIP": "10.15.1.12", "podIP": "10.10.113.3"   } }

Вывод подтверждает: поды распределены по воркер-нодам «один к одному». Если увеличить количество подов, часть будет в статусе Pending из‑за anti-affinity.

4. Опубликуем сервис. 

Простая императивная публикация:

kubectl expose deploy cilium-echoserver --name cilium-echo-svc --type ClusterIP

Публикация с политикой:

cat <<EOF | kubectl apply -f - apiVersion: v1 kind: Service metadata:   name: cilium-echo-svc-local spec:   internalTrafficPolicy: Local   ports:     - port: 80       protocol: TCP       targetPort: 8088   selector:     app: cilium-echoserver   sessionAffinity: None   type: ClusterIP EOF

Публикация NodePort:

cat <<EOF | kubectl apply -f - apiVersion: v1 kind: Service metadata:   name: cilium-echo-svc-np spec:   externalTrafficPolicy: Local   ports:     - port: 80       protocol: TCP       targetPort: 8088   selector:     app: cilium-echoserver   sessionAffinity: None   type: NodePort EOF

5. Еще раз проверим маршруты, полученные по BGP.

Bird2:

sudo birdc show route BIRD 2.17.1 ready. Table master4: 10.10.120.0/26       unicast [worker_node_1 14:43:11.514 from 10.15.1.13] * (100) [AS65065i]     via 10.15.1.13 on eth1                      unicast [worker_node_3 14:43:12.011] (100) [AS65065i]     via 10.15.1.13 on eth1                      unicast [worker_node_2 14:43:12.300 from 10.15.1.12] (100) [AS65065i]     via 10.15.1.13 on eth1 10.10.246.0/26       unicast [worker_node_1 14:43:11.514 from 10.15.1.13] * (100) [AS65065i]     via 10.15.1.12 on eth1                      unicast [worker_node_3 14:43:12.011 from 10.15.1.13] (100) [AS65065i]     via 10.15.1.12 on eth1                      unicast [worker_node_2 14:43:12.300] (100) [AS65065i]     via 10.15.1.12 on eth1 10.96.0.0/12         unicast [worker_node_1 14:43:11.514] * (100) [AS65065i]     via 10.15.1.13 on eth1                      unicast [worker_node_3 14:43:12.011] (100) [AS65065i]     via 10.15.1.13 on eth1                      unicast [worker_node_2 14:43:12.300] (100) [AS65065i]     via 10.15.1.12 on eth1 10.10.37.0/26        unicast [worker_node_1 14:43:11.514] * (100) [AS65065i]     via 10.15.1.13 on eth1                      unicast [worker_node_3 14:43:12.011 from 10.15.1.13] (100) [AS65065i]     via 10.15.1.13 on eth1                      unicast [worker_node_2 14:43:12.300 from 10.15.1.12] (100) [AS65065i]     via 10.15.1.13 on eth1 10.107.30.248/32     unicast [worker_node_1 14:43:11.514] * (100) [AS65065i]     via 10.15.1.13 on eth1                      unicast [worker_node_3 14:43:12.011] (100) [AS65065i]     via 10.15.1.13 on eth1                      unicast [worker_node_2 14:43:12.300] (100) [AS65065i]     via 10.15.1.12 on eth1 

FRRouting:

sudo vtysh -c 'show ip route bgp' Codes: K - kernel route, C - connected, L - local, S - static,    R - RIP, O - OSPF, I - IS-IS, B - BGP, E - EIGRP, N - NHRP,    T - Table, v - VNC, V - VNC-Direct, A - Babel, F - PBR,    f - OpenFabric, t - Table-Direct,    > - selected route, * - FIB route, q - queued, r - rejected, b - backup    t - trapped, o - offload failure   IPv4 unicast VRF default: B>* 10.10.37.0/26 [20/0] via 10.15.1.13, eth1, weight 1, 00:04:39                      via 10.15.1.13, eth1, weight 1, 00:04:39                      via 10.15.1.13, eth1, weight 1, 00:04:39                      via 10.15.1.13, eth1, weight 1, 00:04:39                      via 10.15.1.13, eth1, weight 1, 00:04:39                      via 10.15.1.13, eth1, weight 1, 00:04:39                      via 10.15.1.13, eth1, weight 1, 00:04:39 B>* 10.10.120.0/26 [20/0] via 10.15.1.13, eth1, weight 1, 00:04:39                       via 10.15.1.13, eth1, weight 1, 00:04:39                       via 10.15.1.13, eth1, weight 1, 00:04:39                       via 10.15.1.13, eth1, weight 1, 00:04:39                       via 10.15.1.13, eth1, weight 1, 00:04:39                       via 10.15.1.13, eth1, weight 1, 00:04:39                       via 10.15.1.13, eth1, weight 1, 00:04:39 B>* 10.10.246.0/26 [20/0] via 10.15.1.12, eth1, weight 1, 00:04:39                       via 10.15.1.12, eth1, weight 1, 00:04:39                       via 10.15.1.12, eth1, weight 1, 00:04:39                       via 10.15.1.12, eth1, weight 1, 00:04:39                       via 10.15.1.12, eth1, weight 1, 00:04:39                       via 10.15.1.12, eth1, weight 1, 00:04:39                       via 10.15.1.12, eth1, weight 1, 00:04:39 B>* 10.96.0.0/12 [20/0] via 10.15.1.13, eth1, weight 1, 00:04:39                     via 10.15.1.13, eth1, weight 1, 00:04:39                     via 10.15.1.13, eth1, weight 1, 00:04:39                     via 10.15.1.13, eth1, weight 1, 00:04:39                     via 10.15.1.13, eth1, weight 1, 00:04:39                     via 10.15.1.13, eth1, weight 1, 00:04:39                     via 10.15.1.13, eth1, weight 1, 00:04:39   *                 via 10.15.1.12, eth1, weight 1, 00:04:39   *                 via 10.15.1.13, eth1, weight 1, 00:04:39 B>* 10.107.30.248/32 [20/0] via 10.15.1.13, eth1, weight 1, 00:04:39                         via 10.15.1.13, eth1, weight 1, 00:04:39                         via 10.15.1.13, eth1, weight 1, 00:04:39                         via 10.15.1.13, eth1, weight 1, 00:04:39                         via 10.15.1.13, eth1, weight 1, 00:04:39                         via 10.15.1.13, eth1, weight 1, 00:04:39                         via 10.15.1.13, eth1, weight 1, 00:04:39   *                     via 10.15.1.12, eth1, weight 1, 00:04:39   *                     via 10.15.1.13, eth1, weight 1, 00:04:39

Так как маршруты появятся в основной таблице маршрутизации, для их просмотра можно использовать iproute2-утилиты.

Пример для bird2:

ip ro li | grep -E 'bird|bgp' 10.10.37.0/26 via 10.15.1.13 dev eth1 proto bird metric 32 10.10.120.0/26 via 10.15.1.13 dev eth1 proto bird metric 32 10.10.246.0/26 via 10.15.1.12 dev eth1 proto bird metric 32 10.15.1.0/24 dev eth1 proto kernel scope link src 10.15.1.50 10.96.0.0/12 via 10.15.1.13 dev eth1 proto bird metric 32 10.107.30.248 via 10.15.1.13 dev eth1 proto bird metric 32 10.222.7.0/24 dev eth0 proto kernel scope link src 10.222.7.3

Пример для frrouting:

10.10.37.0/26 nhid 40 via 10.15.1.13 dev eth1 proto bgp metric 20 10.10.120.0/26 nhid 45 via 10.15.1.13 dev eth1 proto bgp metric 20 10.10.246.0/26 nhid 46 via 10.15.1.12 dev eth1 proto bgp metric 20 10.96.0.0/12 nhid 42 proto bgp metric 20 10.107.30.248 nhid 42 proto bgp metric 20

Обратите внимание, что при выставлении спецификации сервиса externalTrafficPolicy: Local по BGP анонсируется префикс /32.

6. Проверим маршруты на воркер-нодах кластера с помощью node-shell.

На управляющей машине:

kubectl node-shell $(kubectl get nodes -o name | head -1)

В консоли воркер-ноды:

ip ro li | grep 10.15.1.50 172.19.7.0/24 via 10.15.1.50 dev eth0 proto bird 

Как видим, анонсированный маршрут до подсети VPN есть в таблице маршрутизации.

4. Проверим доступ к подам и сервисам. Выполняем шаг на виртуальной машине с VPN-сервером, не забывая при этом заменить IP-адреса на полученные сервисами:

svc_ips="10.10.246.2 10.10.120.2 10.10.37.3" for ip in $svc_ips; do curl -sS http://${ip}:8088 ; done curl -sS http://10.104.145.219:8088 curl -sS http://10.96.239.8 dig @10.96.0.10 selectel.org curl -k -sS https://10.96.0.1

Пример успешного вывода:

dig @10.96.0.10 selectel.ru   ; <<>> DiG 9.18.30-0ubuntu0.22.04.2-Ubuntu <<>> @10.96.0.10 selectel.ru ; (1 server found) ;; global options: +cmd ;; Got answer: ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 32703 ;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1   ;; OPT PSEUDOSECTION: ; EDNS: version: 0, flags:; udp: 1232 ; COOKIE: d73752b62ecce39e (echoed) ;; QUESTION SECTION: ;selectel.ru.               IN  A   ;; ANSWER SECTION: selectel.ru.         30  IN  A   85.119.149.3   ;; Query time: 4 msec ;; SERVER: 10.96.0.10#53(10.96.0.10) (UDP) ;; WHEN: Wed May 14 16:12:41 MSK 2025 ;; MSG SIZE  rcvd: 79   host kubernetes.default.svc.cluster.local 10.96.0.10 Using domain server: Name: 10.96.0.10 Address: 10.96.0.10#53 Aliases:   kubernetes.default.svc.cluster.local has address 10.96.0.1  curl -ksS https://10.96.0.1 {   "kind": "Status",   "apiVersion": "v1",   "metadata": {},   "status": "Failure",   "message": "forbidden: User \"system:anonymous\" cannot get path \"/\"",   "reason": "Forbidden",   "details": {},   "code": 403 }

Заключение

Что можно улучшить в описанном решении? Во-первых, добавить поддержку BFD — это ускорит сходимость BGP и сделает маршрутизацию более устойчивой. Во-вторых, при желании можно внедрить фильтрацию входящих префиксов в Calico — это поможет исключить влияние человеческих ошибок.

И все же, чего мы добились? Настроили облачную инфраструктуру так, чтобы маршруты до подов автоматически появлялись на нужных серверах. Разработчики теперь могут подключаться по VPN и обращаться к сервисам в Kubernetes напрямую по IP-адресам, без ручного добавления маршрутов. Это уже серьезный шаг к автоматизации и удобству.

А дальше — еще интереснее. Если вам полезен такой подход, можно отдельно разобрать:

  • как подключить BFD для повышения отказоустойчивости BGP-сессий;

  • как организовать разрешение имен вида <namespace>.svc.cluster.local через dnsmasq и unbound для внешних сервисов, чтобы обращаться к подам по DNS, а не по IP.

Если такой разбор был бы интересен — дайте знать в комментариях!


ссылка на оригинал статьи https://habr.com/ru/articles/932696/