Сталкивались ли вы с ситуацией, когда нужно получить доступ к сети подов или сервисов в кластере 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.

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

Комментарии (1)


  1. SiGGthror
    31.07.2025 14:23

    Вы вначале статьи пишете про автоскейлер, а потом в ручном режиме конфигурируете порты. Я мог упустить какой-то момент в статье, но как это автоматизируется по итогу?