
Сталкивались ли вы с ситуацией, когда нужно получить доступ к сети подов или сервисов в кластере 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, jq, OpenStack 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 можно использовать два подхода к настройке динамической маршрутизации:
«Установка на не-кластерный узел» (например, в Docker-контейнере).
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.
Если такой разбор был бы интересен — дайте знать в комментариях!
SiGGthror
Вы вначале статьи пишете про автоскейлер, а потом в ручном режиме конфигурируете порты. Я мог упустить какой-то момент в статье, но как это автоматизируется по итогу?