Для сетевиков от сетевика
Среда
Как нормальные пацаны, мы будем делать эмуляцию Kubernetes-а в виде “baremetal-а”, а по факту, конечно, этот самый голый метал будет представлять из себя виртуалку с убунтой, запущеной внутри среды виртуализации, которая тоже виртуализована. Накидаем такую топологию:

Так как мы норм пацаны, то:
Всё сэмулируем прямо в PNETLAB-е
Мы будем использовать топологию Leaf-Spine
-
Мы начитались Заметок сетевого архитектора, и у нас нет никаких вланов, просто чистый L3 - все линки в сторону серверов маршрутизируемые
Между свитчами мы подымем OSFP, чтобы все сервачки могли друг с дружкой общаться. Здорово будет.
Мы будем юзать какую-то убунту (первое что под руку попалось, кароче):
root@K-Master:~# lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description: Ubuntu 22.04.2 LTS
Release: 22.04
Codename: jammy
Базовая настройка сети
Намутим какую-нибудь максимально запутанную схему адресации, например такую и проверим что ноды пингают друг друга:

У “нижнего” устройства первый IP, у “верхнего” - нулевой. Между лифами и спайнами сделаем простенький оэспээфчик, для серверов лифы - шлюзы по умолчанию:
На первом лифе делаем так:
interface Ethernet1
no switchport
ip address 10.11.99.1/31
!
interface Ethernet2
no switchport
ip address 10.0.11.0/31
!
interface Ethernet3
no switchport
ip address 10.1.11.0/31
На мастере делаем так:
root@K-Master:~# ip addr add 10.0.11.1/31 dev ens3
# пингуетцо:
root@K-Master:~# ping 10.0.11.0
PING 10.0.11.0 (10.0.11.0) 56(84) bytes of data.
64 bytes from 10.0.11.0: icmp_seq=1 ttl=64 time=32.3 ms
64 bytes from 10.0.11.0: icmp_seq=2 ttl=64 time=3.05 ms
# ну шлюз ещё пропишем
root@K-Master:~# ip r add default via 10.0.11.0 dev ens3
root@K-Master:~# ip r
default via 10.0.11.0 dev ens3
10.0.11.0/31 dev ens3 proto kernel scope link src 10.0.11.1
Но так, конечно, дело не пойдёт, потому что кубернетес вещь не надёжная - ноды ребутаются постоянно без всякой на то причины и, конечно, надо заперсистить конфигу как-то, например через netplan:
root@K-Master:~# cat /etc/netplan/01-KubeBase.yaml
network:
ethernets:
ens3:
addresses:
- 10.0.11.1/31
dhcp4: false
routes:
- to: default
via: 10.0.11.0
version: 2
Теперь ОТМАСШТАБИРУЕМ это всё
Лиф2:
interface Ethernet1
no switchport
ip address 10.22.99.1/31
!
interface Ethernet2
no switchport
ip address 10.2.22.0/31
!
Лиф3:
interface Ethernet1
no switchport
ip address 10.33.99.1/31
!
interface Ethernet2
no switchport
ip address 10.3.33.0/31
!
Воркер1:
root@k-w1:~#cat <<EOF > /etc/netplan/01-KubeBase.yaml
network:
ethernets:
ens3:
addresses:
- 10.1.11.1/31
dhcp4: false
routes:
- to: default
via: 10.1.11.0
version: 2
EOF
root@k-w1:~# netplan apply
Воркер2
root@k-w2:~#cat <<EOF > /etc/netplan/01-KubeBase.yaml
network:
ethernets:
ens3:
addresses:
- 10.2.22.1/31
dhcp4: false
routes:
- to: default
via: 10.2.22.0
version: 2
EOF
root@k-w2:~# netplan apply
Воркер 3
root@k-w3:~#cat <<EOF > /etc/netplan/01-KubeBase.yaml
network:
ethernets:
ens3:
addresses:
- 10.3.33.1/31
dhcp4: false
routes:
- to: default
via: 10.3.33.0
version: 2
EOF
root@k-w3:~# netplan apply
После этого всего воркер 1 может пингать мастера:
root@k-w1:~# ping 10.0.11.1
PING 10.0.11.1 (10.0.11.1) 56(84) bytes of data.
64 bytes from 10.0.11.1: icmp_seq=1 ttl=63 time=5.99 ms
64 bytes from 10.0.11.1: icmp_seq=2 ttl=63 time=24.9 ms
Но остальные пока не могут, потому чта нету рутинга через спайн. Как и обещал, мутим простой OSFP.
Настроим спайн:
interface Ethernet1
no switchport
ip address 10.11.99.0/31
!
interface Ethernet2
no switchport
ip address 10.22.99.0/31
!
interface Ethernet3
no switchport
ip address 10.33.99.0/31
И на всех свитчах просто запустим OSFP:
router ospf 1
network 0.0.0.0/0 area 0.0.0.0
(не надо так попустительски относится к настройками OSFP-а в проде, но для лабы подойдёт)
OSFP сходится:
Spine-1#show ip ospf neighbor
Neighbor ID Instance VRF Pri State Dead Time Address Interface
10.11.99.1 1 default 1 FULL/DR 00:00:29 10.11.99.1 Ethernet1
10.22.99.1 1 default 1 FULL/DR 00:00:30 10.22.99.1 Ethernet2
10.33.99.1 1 default 1 FULL/DR 00:00:35 10.33.99.1 Ethernet3
Spine-1#show ip ro ospf
O 10.0.11.0/31 [110/20] via 10.11.99.1, Ethernet1
O 10.1.11.0/31 [110/20] via 10.11.99.1, Ethernet1
O 10.2.22.0/31 [110/20] via 10.22.99.1, Ethernet2
O 10.3.33.0/31 [110/20] via 10.33.99.1, Ethernet3
Теперь все воркеры видят мастера и друг дружку, вот пруф:
root@k-w3:~# ping 10.0.11.1
PING 10.0.11.1 (10.0.11.1) 56(84) bytes of data.
64 bytes from 10.0.11.1: icmp_seq=1 ttl=61 time=13.6 ms
64 bytes from 10.0.11.1: icmp_seq=2 ttl=61 time=15.7 ms
root@k-w3:~# tracepath 10.0.11.1 -n
1?: [LOCALHOST] pmtu 1500
1: 10.3.33.0 3.027ms
1: 10.3.33.0 2.535ms
2: 10.33.99.0 6.473ms
3: 10.11.99.1 10.034ms
4: 10.0.11.1 11.929ms reached
Resume: pmtu 1500 hops 4 back 4
Всё! Всё да не всё - ubunutu у нас “голая”, скорее всего этот наш кубернетес потребуется доустановить, так что нужны какие-то Интернеты. В PNETLAB, если хост имеет доступ в Интернет, можно сделать специальное облачко с типом NAT, подключить туда какой-нибудь интерфейс какого-нибудь устройства, получить адрес по dhcp и наслаждаться. Так как я ярый противник подключения чего-либо кроме лифоф в спайны, то именно в спайн Интернет я и подключу:

Проверяем, не появился ли Интернет на какой-нибудь ноде:
user@k-w1:~$ ping 8.8.8.8
PING 8.8.8.8 (8.8.8.8) 56(84) bytes of data.
From 10.1.11.0 icmp_seq=1 Destination Net Unreachable
From 10.1.11.0 icmp_seq=2 Destination Net Unreachable
From 10.1.11.0 icmp_seq=3 Destination Net Unreachable
Ничё не работает, наверное надо что-то где-то настроить. На спайне получаем IP через DHCP на порту, и настраиваем дефолт через первый адрес в сети (чисто экспериментально выяснил):
# не работает:
Spine-1#ping 8.8.8.8
connect: Network is unreachable
# Чиним
Spine-1#conf t
Spine-1(config)#int ethernet 4
Spine-1(config-if-Et4)#no switchport
Spine-1(config-if-Et4)#ip address dhcp
Spine-1#show ip int ethernet 4 brief
Interface IP Address Status Protocol MTU Owner
-------------- -------------------- ----------- ------------- --------- -------
Ethernet4 10.0.137.189/24 up up 1500
Spine-1(config)#ip route 0.0.0.0 0.0.0.0 10.0.137.1
# Работает
Spine-1#ping 8.8.8.8
PING 8.8.8.8 (8.8.8.8) 72(100) bytes of data.
80 bytes from 8.8.8.8: icmp_seq=1 ttl=99 time=23.2 ms
80 bytes from 8.8.8.8: icmp_seq=2 ttl=99 time=20.6 ms
80 bytes from 8.8.8.8: icmp_seq=3 ttl=99 time=20.6 ms
80 bytes from 8.8.8.8: icmp_seq=4 ttl=99 time=21.2 ms
80 bytes from 8.8.8.8: icmp_seq=5 ttl=99 time=20.7 ms
# распространим дефолт по фабрике:
Spine-1(config)#router ospf 1
Spine-1(config-router-ospf)#redistribute static
# На лиф, к которому первый воркер подключен дефолт пришёл, и видёт на спайн:
Leaf-1#show ip ro 0.0.0.0
Gateway of last resort:
O E2 0.0.0.0/0 [110/1] via 10.11.99.0, Ethernet1
# Проверяем на воркере1:
user@k-w1:~$ ping 8.8.8.8
PING 8.8.8.8 (8.8.8.8) 56(84) bytes of data.
:(
#Трассировка ведёт куда надо:
user@k-w1:~$ tracepath -n 8.8.8.8
1?: [LOCALHOST] pmtu 1500
1: 10.1.11.0 4.290ms
1: 10.1.11.0 2.697ms
2: 10.11.99.0 8.022ms
3: no reply
И вероятно трассировка уходит в бридж с самой нодой, на котором настроен NAT в интернет. Но нода понятия не имеет ни про какие воркеры и сети, в которых они живут, так что кажется, что нам на выходном роутере (спайне) тоже нужно NAT настроить:
Spine-1(config)#ip access-list ACL_NAT
Spine-1(config-acl-ACL_NAT)# permit ip 10.0.0.0/8 any log
Spine-1(config)#int et4
Spine-1(config-if-Et4)#ip nat source dynamic access-list ACL_NAT overload
И это даже работает, но работает ппц как медленно - всё таки виртуальные устройства внутри pnetlab-а не предназначены для сколько-нибудь нормальной работы датаплейна. В общем, я изменил условие сделки и молитесь что бы в последний раз - просто добавим по дополнительному интерфейсу на каждый хост, воткнём их в это облачко с Интернетом, получим адрес по DHCP, получим дефолт, а в сторону свичей просто пропишем статику на сеть 10/8, что-то такое, в общем, что бы получилось:
user@k-w1:~$ ip r
default via 10.0.137.1 dev ens5 proto dhcp src 10.0.137.132 metric 100
10.0.0.0/8 via 10.1.11.0 dev ens3 proto static
10.0.137.0/24 dev ens5 proto kernel scope link src 10.0.137.132 metric 100
10.0.137.1 dev ens5 proto dhcp scope link src 10.0.137.132 metric 100
10.1.11.0/31 dev ens3 proto kernel scope link src 10.1.11.1
ens5 - это как раз новый интерфейс, воткнутый в облачко с Интернетами.
Наконец-то, kubernetes
Дальше я начинаю писать про то, в чём не разбираюсь вообще.
CNI
Напоминаю главное про сетевые технологии - сети сами по себе нахуй ни кому не упёрлись. Сети нужны для сервисов. Помните про это. И обратное верно - никакие современные сервисы не работаю без сетей, даже всемогущий кубернетес. За сети в кубе отвечает CNI - Container Network Interface. Задачи у CNI базово достаточно тривиальны:
выделить адреса подикам
обеспечить связь между подиками
обеспечить связь подиков с внешним миром
возможно, обеспечить немного безопасности подикам
А под копотом всего этого, конечно, какая-то магия.
А что ещё за “подики”? Ох, не хочется, конечно, идти в глубь всей этой подворотноштанной машинерии, потому тоже скажу коротко - под это набор контейнеров (но обычно - один) у которых общий выделенный сетевой неймспейс (а значит и IP адрес), общие ресурсы CPU\RAM (cgroup) и хранилище. При этом, это всё отделено от других таких же наборов контейнеров. В общем, это основной строительный материал, из которых лепят совеременные модные микросервисные приложения в кубах. Подикам нужна сеть, за сеть отвечает CNI. Сиэнаев всяких много. Самый простой, наверное Flannel, но он, кажется, использует под копотом бесячий VXLAN, а я хочу сделать сеть на чистом прозрачном роутинге. Из ещё популярного можно было бы копнуть Cillium - но кажется? он на столько крутой и модный, что для первого раза не подойдёт - сейчас бы ещё трейсы eBFP хуков собирать сидеть. В общем, я решил остановиться на некоем промежуточном варианте между фланнелем и силиумом - то бишь Calico, оно вроде как умеет работать без всяких мерзких оверлеев - можно запириться со свичём по BGP прямо с ноды! Ну и самое главное преимущество - то что я понятия не имею, как его настраивать, так что будет веселее.
Погнали, кароче. Базовая подготовка нод
Нулевое что надо сделать - это настроить разрешение имён в IP адреса, так как у нас никого внутреннего DNS-а и нету. В общем, делаем просто несколько записей в /etc/hosts, чтобы пацанчики наши могли общаться друг с другом по именам:
root@K-Master:/home/user# cat /etc/hosts
127.0.0.1 localhost
127.0.1.1 ubuntu
10.0.11.1 k-master
10.1.11.1 k-w1
10.2.22.1 k-w2
Первое, что всякие мануалы предлагаю сделать перед установкой куба - это отключить своп. Ну не любит куб своп и всё тут. Я так понял, суть проблемы в том, что куб просто не шарит что есть какой-то там своп - ему важен только общий объём известной памяти, а что именно это за “память” - божественная суперскоростная DIMM1 или просто HDD - он не разумеет, потому подики смело могу начать писать данные вместо RAM на жёсткий диск, приложения начнут деградировать, а Кубу пох будет. Нам такое не нужно в общем.
# Есть своп:
user@K-Master:~$ sudo free -h
total used free shared buff/cache available
Mem: 3.8Gi 182Mi 2.9Gi 4.0Mi 725Mi 3.4Gi
Swap: 3.8Gi 0B 3.8Gi
# Хуяк!
user@K-Master:~$ sudo swapoff -a
# и нет свопа:
user@K-Master:~$ free -h
total used free shared buff/cache available
Mem: 3.8Gi 188Mi 2.8Gi 4.0Mi 852Mi 3.4Gi
Swap: 0B 0B 0B
# Не забываем заперсистить:
sudo sed -i '/swap/ s/^\(.*\)$/#\1/g' /etc/fstab
#тут сами себе регулярку подберите - главное строчку со свапом заккоментить в /etc/fstab, то бы так було:
user@K-Master:~$ cat /etc/fstab | grep swap
#/swap.img none swap sw 0 0
Проворачиваем дельце это на всех хостах.
CRI
Далее нам нужен какой никакой движок, который бы запускал нам контейнеры на хостах. Этим занимается Container Runtime Interface (CRI), и сейчас наверное стандартном является containerd
# Ставим
user@K-Master:~$ sudo apt update
user@K-Master:~$ sudo apt install -y containerd
# Проверяем
user@K-Master:~$ sudo ctr version
Client:
Version: 1.7.27
Revision:
Go version: go1.22.2
Server:
Version: 1.7.27
Revision:
UUID: ea2054de-47e9-46bd-8243-b0afb0746cdd
# Первичная настройка
sudo mkdir -p /etc/containerd
sudo containerd config default | sudo tee /etc/containerd/config.toml
# По умолчанию зачем-то контейнерд использует файлы для управления сигруппами, но нам оно не нужно, пусть системд этим занимается
sudo sed -i 's/SystemdCgroup = false/SystemdCgroup = true/g' /etc/containerd/config.toml
sudo systemctl restart containerd
# Проверяем, что containerd работает
sudo systemctl status containerd # Должен быть "active (running)"
Обязательно надо поздороваться с миром!
# Качаем hello world
user@k-w1:~$ sudo ctr images pull docker.io/library/hello-world:latest
docker.io/library/hello-world:latest: resolved |++++++++++++++++++++++++++++++++++++++|
index-sha256:ec153840d1e635ac434fab5e377081f17e0e15afab27beb3f726c3265039cfff: done |++++++++++++++++++++++++++++++++++++++|
manifest-sha256:03b62250a3cb1abd125271d393fc08bf0cc713391eda6b57c02d1ef85efcc25c: done |++++++++++++++++++++++++++++++++++++++|
layer-sha256:e6590344b1a5dc518829d6ea1524fc12f8bcd14ee9a02aa6ad8360cce3a9a9e9: done |++++++++++++++++++++++++++++++++++++++|
config-sha256:74cc54e27dc41bb10dc4b2226072d469509f2f22f1a3ce74f4a59661a1d44602: done |++++++++++++++++++++++++++++++++++++++|
elapsed: 3.2 s total: 13.1 K (4.1 KiB/s)
unpacking linux/amd64 sha256:ec153840d1e635ac434fab5e377081f17e0e15afab27beb3f726c3265039cfff...
done: 50.983582ms
user@k-w1:~$
# Запускаем hello world
user@k-w1:~$ sudo ctr run --rm docker.io/library/hello-world:latest hello-world
Hello from Docker!
BLA BLA BLA
Ну и напоследок запустим НОРМ контейнер, провалимся в shell, оглядимся чё там:
user@K-Master:~$ sudo ctr images pull docker.io/library/alpine:latest
docker.io/library/alpine:latest: resolved |++++++++++++++++++++++++++++++++++++++|
index-sha256:4bcff63911fcb4448bd4fdacec207030997caf25e9bea4045fa6c8c44de311d1: done |++++++++++++++++++++++++++++++++++++++|
manifest-sha256:eafc1edb577d2e9b458664a15f23ea1c370214193226069eb22921169fc7e43f: done |++++++++++++++++++++++++++++++++++++++|
layer-sha256:9824c27679d3b27c5e1cb00a73adb6f4f8d556994111c12db3c5d61a0c843df8: done |++++++++++++++++++++++++++++++++++++++|
config-sha256:9234e8fb04c47cfe0f49931e4ac7eb76fa904e33b7f8576aec0501c085f02516: done |++++++++++++++++++++++++++++++++++++++|
elapsed: 1.3 s total: 0.0 B (0.0 B/s)
unpacking linux/amd64 sha256:4bcff63911fcb4448bd4fdacec207030997caf25e9bea4045fa6c8c44de311d1...
done: 13.453467ms
# запустим
user@k-w1:~$ sudo ctr run -t docker.io/library/alpine:latest alpine_test sh
######## Вот мы тут в контейнере теперь
/ ~ uname -a
Linux k-w1 5.15.0-69-generic #76-Ubuntu SMP Fri Mar 17 17:19:29 UTC 2023 x86_64 Linux
/ ~ cat /etc/os-release
NAME="Alpine Linux"
ID=alpine
VERSION_ID=3.22.1
PRETTY_NAME="Alpine Linux v3.22"
HOME_URL="https://alpinelinux.org/"
BUG_REPORT_URL="https://gitlab.alpinelinux.org/alpine/aports/-/issues"
/ ~
# Чё там по сети?
/~ ` ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
inet6 ::1/128 scope host
valid_lft forever preferred_lft forever
В общем и целом обычный контейнер - но как видно там никакой сетевой обвязки нет, не буду вдаваться в подробности как её сделать (я хз если честно) - буду надеяться, что всё это мне Calico будет разруливать.
kubeadm, kubelet, kubectl
Тут кажется всё просто :) На всех будущих мастерах и участниках движухи надо выполнить такое:
# 1. Добавляем репозиторий Kubernetes
sudo mkdir -p /etc/apt/keyrings
curl -fsSL https://pkgs.k8s.io/core:/stable:/v1.28/deb/Release.key | sudo gpg --dearmor -o /etc/apt/keyrings/kubernetes-apt-keyring.gpg
echo "deb [signed-by=/etc/apt/keyrings/kubernetes-apt-keyring.gpg] https://pkgs.k8s.io/core:/stable:/v1.28/deb/ /" | sudo tee /etc/apt/sources.list.d/kubernetes.list
# 2. Обновляем пакеты и устанавливаем компоненты
sudo apt update
sudo apt install -y kubeadm kubelet kubectl
# 3. Фиксируем версии
sudo apt-mark hold kubeadm kubelet kubectl
Чекаем что всё хорошо, что все наши на месте:
user@k-w1:~$ kubeadm version
kubeadm version: &version.Info{Major:"1", Minor:"28", GitVersion:"v1.28.15", GitCommit:"841856557ef0f6a399096c42635d114d6f2cf7f4", GitTreeState:"clean", BuildDate:"2024-10-22T20:33:16Z", GoVersion:"go1.22.8", Compiler:"gc", Platform:"linux/amd64"}
user@k-w1:~$ kubelet --version
Kubernetes v1.28.15
user@k-w1:~$ kubectl version --client
Client Version: v1.28.15
Kustomize Version: v5.0.4-0.20230601165947-6ce0bf390ce3
Делаем кластер
Аккумулируя мануалы с сайта https://kubernetes.io/ (что-то на английском) и советы LLM-ы, я решил сделать так:
sudo kubeadm init --pod-network-cidr=10.66.0.0/16 --control-endpoint=10.0.11.1
Ну решил и сделал:
user@K-Master:~$ sudo kubeadm init --pod-network-cidr=10.66.0.0/16 --control-plane-endpoint=10.0.11.1
I0729 04:40:12.098048 170736 version.go:256] remote version is much newer: v1.33.3; falling back to: stable-1.28
[init] Using Kubernetes version: v1.28.15
[preflight] Running pre-flight checks
error execution phase preflight: [preflight] Some fatal errors occurred:
[ERROR FileContent--proc-sys-net-bridge-bridge-nf-call-iptables]: /proc/sys/net/bridge/bridge-nf-call-iptables does not exist
[ERROR FileContent--proc-sys-net-ipv4-ip_forward]: /proc/sys/net/ipv4/ip_forward contents are not set to 1
[preflight] If you know what you are doing, you can make a check non-fatal with `--ignore-preflight-errors=...`
To see the stack trace of this error execute with --v=5 or higher
Предполётная подгтовка не пройдена - надо включить пару опций ядра - рутинг (ip_forward) и процессинг нетфильтром пакетов пролетающих через бридж. БРИИИИДЖ?! Кто сказал “бридж”? В моей картине мира никаких бриджей у нас появится не должно (юзаем чистый роутинг + veth пары до подиков). Ну чтож, кубернетес же не знает то, чего знаю я - он понятия не имеет буду я использовать бриджы или нет, не будем его смущать:
Включаем bridge-nf-call-iptables:
# модуль ядра
user@K-Master:~$ sudo modprobe br_netfilter
# перепроверим за собой
user@K-Master:~$ lsmod | grep br_netfilter
br_netfilter 32768 0
bridge 307200 1 br_netfilter
# персистим
user@K-Master:~$ echo "br_netfilter" | sudo tee /etc/modules-load.d/br_netfilter.conf
br_netfilter
Ну а как включить просто ip forwarding знает каждый сетевик. Так как не каждый сетевик читает это сейчас,то делаем это например так:
user@K-Master:~$ sudo sysctl -w net.ipv4.ip_forward=1
net.ipv4.ip_forward = 1
Пробуем ещё разок, посмотрим какие ошибки нас ждут теперь! Чтож, кажется я накаркал и никаких ошибок нет. kubeadm сделал много работы:
I0730 03:53:56.693629 171936 version.go:256] remote version is much newer: v1.33.3; falling back to: stable-1.28
[init] Using Kubernetes version: v1.28.15
[preflight] Running pre-flight checks
[preflight] Pulling images required for setting up a Kubernetes cluster
[preflight] This might take a minute or two, depending on the speed of your internet connection
[preflight] You can also perform this action in beforehand using 'kubeadm config images pull'
W0730 03:54:19.733292 171936 checks.go:835] detected that the sandbox image "registry.k8s.io/pause:3.8" of the container runtime is inconsistent with that used by kubeadm. It is recommended that using "registry.k8s.io/pause:3.9" as the CRI sandbox image.
[certs] Using certificateDir folder "/etc/kubernetes/pki"
[certs] Generating "ca" certificate and key
[certs] Generating "apiserver" certificate and key
[certs] apiserver serving cert is signed for DNS names [k-master kubernetes kubernetes.default kubernetes.default.svc kubernetes.default.svc.cluster.local] and IPs [10.96.0.1 10.0.137.12 10.0.11.1]
[certs] Generating "apiserver-kubelet-client" certificate and key
[certs] Generating "front-proxy-ca" certificate and key
[certs] Generating "front-proxy-client" certificate and key
[certs] Generating "etcd/ca" certificate and key
[certs] Generating "etcd/server" certificate and key
[certs] etcd/server serving cert is signed for DNS names [k-master localhost] and IPs [10.0.137.12 127.0.0.1 ::1]
[certs] Generating "etcd/peer" certificate and key
[certs] etcd/peer serving cert is signed for DNS names [k-master localhost] and IPs [10.0.137.12 127.0.0.1 ::1]
[certs] Generating "etcd/healthcheck-client" certificate and key
[certs] Generating "apiserver-etcd-client" certificate and key
[certs] Generating "sa" key and public key
[kubeconfig] Using kubeconfig folder "/etc/kubernetes"
[kubeconfig] Writing "admin.conf" kubeconfig file
[kubeconfig] Writing "kubelet.conf" kubeconfig file
[kubeconfig] Writing "controller-manager.conf" kubeconfig file
[kubeconfig] Writing "scheduler.conf" kubeconfig file
[etcd] Creating static Pod manifest for local etcd in "/etc/kubernetes/manifests"
[control-plane] Using manifest folder "/etc/kubernetes/manifests"
[control-plane] Creating static Pod manifest for "kube-apiserver"
[control-plane] Creating static Pod manifest for "kube-controller-manager"
[control-plane] Creating static Pod manifest for "kube-scheduler"
[kubelet-start] Writing kubelet environment file with flags to file "/var/lib/kubelet/kubeadm-flags.env"
[kubelet-start] Writing kubelet configuration to file "/var/lib/kubelet/config.yaml"
[kubelet-start] Starting the kubelet
[wait-control-plane] Waiting for the kubelet to boot up the control plane as static Pods from directory "/etc/kubernetes/manifests". This can take up to 4m0s
[apiclient] All control plane components are healthy after 11.505483 seconds
[upload-config] Storing the configuration used in ConfigMap "kubeadm-config" in the "kube-system" Namespace
[kubelet] Creating a ConfigMap "kubelet-config" in namespace kube-system with the configuration for the kubelets in the cluster
[upload-certs] Skipping phase. Please see --upload-certs
[mark-control-plane] Marking the node k-master as control-plane by adding the labels: [node-role.kubernetes.io/control-plane node.kubernetes.io/exclude-from-external-load-balancers]
[mark-control-plane] Marking the node k-master as control-plane by adding the taints [node-role.kubernetes.io/control-plane:NoSchedule]
[bootstrap-token] Using token: 4glzzt.3b96mmozrsum72he
[bootstrap-token] Configuring bootstrap tokens, cluster-info ConfigMap, RBAC Roles
[bootstrap-token] Configured RBAC rules to allow Node Bootstrap tokens to get nodes
[bootstrap-token] Configured RBAC rules to allow Node Bootstrap tokens to post CSRs in order for nodes to get long term certificate credentials
[bootstrap-token] Configured RBAC rules to allow the csrapprover controller automatically approve CSRs from a Node Bootstrap Token
[bootstrap-token] Configured RBAC rules to allow certificate rotation for all node client certificates in the cluster
[bootstrap-token] Creating the "cluster-info" ConfigMap in the "kube-public" namespace
[kubelet-finalize] Updating "/etc/kubernetes/kubelet.conf" to point to a rotatable kubelet client certificate and key
[addons] Applied essential addon: CoreDNS
[addons] Applied essential addon: kube-proxy
Your Kubernetes control-plane has initialized successfully!
successfully! Он очень мил и рассказывает, что же делать дальше - как быть простому пользователю и как нам заджойнить другие ноды
To start using your cluster, you need to run the following as a regular user:
mkdir -p $HOME/.kube
sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
sudo chown $(id -u):$(id -g) $HOME/.kube/config
Alternatively, if you are the root user, you can run:
export KUBECONFIG=/etc/kubernetes/admin.conf
You should now deploy a pod network to the cluster.
Run "kubectl apply -f [podnetwork].yaml" with one of the options listed at:
https://kubernetes.io/docs/concepts/cluster-administration/addons/
You can now join any number of control-plane nodes by copying certificate authorities
and service account keys on each node and then running the following as root:
kubeadm join 10.0.11.1:6443 --token 4glzzt.3b96mmozrsum72he \
--discovery-token-ca-cert-hash sha256:557100ad9c340873e4d2d4e329fd303ba274548f1188030ad9c6569a2f745e42 \
--control-plane
Then you can join any number of worker nodes by running the following on each as root:
kubeadm join 10.0.11.1:6443 --token 4glzzt.3b96mmozrsum72he \
--discovery-token-ca-cert-hash sha256:557100ad9c340873e4d2d4e329fd303ba274548f1188030ad9c6569a2f745e42
Сразу чешутся руки сделать kubectl get pods
:)
user@K-Master:~$ kubectl get pods
E0730 04:20:56.695021 173012 memcache.go:265] couldn't get current server API group list: Get "http://localhost:8080/api?timeout=32s": dial tcp 127.0.0.1:8080: connect: connection refused
E0730 04:20:56.695309 173012 memcache.go:265] couldn't get current server API group list: Get "http://localhost:8080/api?timeout=32s": dial tcp 127.0.0.1:8080: connect: connection refused
E0730 04:20:56.696762 173012 memcache.go:265] couldn't get current server API group list: Get "http://localhost:8080/api?timeout=32s": dial tcp 127.0.0.1:8080: connect: connection refused
E0730 04:20:56.697186 173012 memcache.go:265] couldn't get current server API group list: Get "http://localhost:8080/api?timeout=32s": dial tcp 127.0.0.1:8080: connect: connection refused
E0730 04:20:56.698598 173012 memcache.go:265] couldn't get current server API group list: Get "http://localhost:8080/api?timeout=32s": dial tcp 127.0.0.1:8080: connect: connection refused
The connection to the server localhost:8080 was refused - did you specify the right host or port?
Куда-то явно не туда ломится :( Всё потому, что я не читаю что мне консоль пишет, хотя явно было:
To start using your cluster, you need to run the following as a regular user:
mkdir -p $HOME/.kube
sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
sudo chown $(id -u):$(id -g) $HOME/.kube/config
Уже лучше:
user@K-Master:~$ kubectl get pods
No resources found in default namespace.
Ну, по крайней мере, он что-то ответил. Что собственно произошло? Ну я просто скопировал файл, который получился в результате инциализации (/etc/kubernetes/admin.conf) в каталог $HOME/.kube/. Утилита kubectl
используется для общения с кубернетес кластером через некое API. Адрес и какие-никакие креды для этого самого API нужно откуда-то взять. kubectl
по дефолту ищет его как раз в файле ~/.kube/config
Выглядит он так (ключики я подотру чтобы не тратить лишний раз пиксели на экране):
apiVersion: v1
clusters:
- cluster:
certificate-authority-data:
LS0tLS1CRUdJTBLA-BLA-BLA
server: https://10.0.11.1:6443
name: kubernetes
contexts:
- context:
cluster: kubernetes
user: kubernetes-admin
name: kubernetes-admin@kubernetes
current-context: kubernetes-admin@kubernetes
kind: Config
preferences: {}
users:
- name: kubernetes-admin
user:
client-certificate-data:
BLA BLA
client-key-data:
LS0tLBLA BL BLA
Ну либо содержимое файла можно выцепить с помощью команды kubectl config view
- покажет примерно тоже самое Тут важно сказать - файл используется утилитой kubectl, а kubectl это просто некий “клиент” с помощью которого вы по API куда-то там подключаетесь и можете общаться с удалённым кластером. Я просто запускаю его на мастере, где кластер развернул, чтобы консоль не переключать, а так - файл можно скопировать куда угодно где есть утилита kubectl и запускать оттуда - главное чтобы сетевой доступ до мастера был. В общем, зайдём с любого воркера на мастер и спиздим файл себе!
# сначала ничего не работает:
user@k-w1:~$ kubectl get pods
E0801 05:56:06.934129 186485 memcache.go:265] couldn't get current server API group list: Get "http://localhost:8080/api?timeout=32s": dial tcp 127.0.0.1:8080: connect: connection refused
E0801 05:56:06.935916 186485 memcache.go:265] couldn't get current server API group list: Get "http://localhost:8080/api?timeout=32s": dial tcp 127.0.0.1:8080: connect: connection refused
E0801 05:56:06.936529 186485 memcache.go:265] couldn't get current server API group list: Get "http://localhost:8080/api?timeout=32s": dial tcp 127.0.0.1:8080: connect: connection refused
E0801 05:56:06.938000 186485 memcache.go:265] couldn't get current server API group list: Get "http://localhost:8080/api?timeout=32s": dial tcp 127.0.0.1:8080: connect: connection refused
E0801 05:56:06.938450 186485 memcache.go:265] couldn't get current server API group list: Get "http://localhost:8080/api?timeout=32s": dial tcp 127.0.0.1:8080: connect: connection refused
The connection to the server localhost:8080 was refused - did you specify the right host or port? '
# пиздим файл!
user@k-w1:~$ sftp user@K-Master
user@k-master's password:
Connected to K-Master.
sftp> cd .kube/
sftp> ls -la
drwxrwxr-x 3 user user 4096 Jul 30 04:23 .
drwxr-x--- 6 user user 4096 Aug 1 05:25 ..
drwxr-x--- 4 user user 4096 Jul 30 04:23 cache
-rw------- 1 user user 5641 Jul 30 04:22 config
sftp> get config
Fetching /home/user/.kube/config to config
config 100% 5641 18.2KB/s 00:00
sftp>
sftp>
sftp> exit
user@k-w1:~$ mkdir .kube
user@k-w1:~$ mv config .kube/config
# проверим ещё раз:
user@k-w1:~$ kubectl get pods
No resources found in default namespace.
В общем, запустили kubectl, он обратился к нашему локальному файлу kubeconfig, взял из локального файла строчку https://10.0.11.1:6443
и пошёл туда общаться. Ну стоит упомянуть ещё - в файле конфига, конечно, может быть не один кластер и вы можете переключаться между ними. Вот пример с моего домашнего компа, в моём файле конфига куба 65 строчек содержащих слово “server”:
kubectl config view | Select-String "server" | Measure-Object -Line
Lines Words Characters Property
----- ----- ---------- --------
65
Список контекстов, то есть “доступных” для вас кластеров на базе вашего конфига можно получить командой kubectl config get-contexts
. Понять где вы сейчас можно с помощью kubectl config current-context
, переключаться между контекстами можно с помощью kubectl config use-context <ВАШ_ЖЕЛАННЫЙ_КОНТЕКСТ>
, ну какие-тто умники ещё kubectx написали, что бы побыстрее это всё - https://github.com/ahmetb/kubectx
Кароче, фиг с ними контекстами-то! У нас тут проблема посерьёзнее же - подов тонет!
user@k-w1:~$ kubectl get pods
No resources found in default namespace.
В целом можно подумать, что это потому что мы ничего не создавали пока, но неужели такой сложный механизм как кубернетес ничего сам себе не создал?
Давайте попробуем вот так:
user@k-w1:~$ kubectl get pods -A
NAMESPACE NAME READY STATUS RESTARTS AGE
kube-system coredns-5dd5756b68-hjxxk 0/1 Pending 0 2d2h
kube-system coredns-5dd5756b68-mx2x2 0/1 Pending 0 2d2h
kube-system etcd-k-master 1/1 Running 0 2d2h
kube-system kube-apiserver-k-master 1/1 Running 0 2d2h
kube-system kube-controller-manager-k-master 1/1 Running 0 2d2h
kube-system kube-proxy-dsf2f 1/1 Running 0 2d2h
kube-system kube-scheduler-k-master 1/1 Running 0 2d2h
Ага, чёто есть. Ключик -A
как будто бэ намекает нашему kubectl-у - ты давай покажи не только в текущем неймспейсе, а вообще во всех, чё там у тебя есть. Ну и вот в выводе у нас появляется колоночка NAMESPACE, чтобы понятно было. Про неймспейсы в кубе можно почитать туть. Сейчас важно понимать две вещи:
неймспейсы это некие инструменты изоляции ресурсов кластера (эдакие тенанты) - сделал кластер - Васе неймспейс сделал, где он балуется, и Пете сделал, потому что Петя не балуется, а делами занят.
это не тоже самое что неймспейсы в Linux - абстракция сия в кубе гораздо более высокоуровневая нежели в Linux
Посмотреть какие есть неймспейсы можно так:
user@k-w1:~$ kubectl get ns
NAME STATUS AGE
default Active 2d5h
kube-node-lease Active 2d5h
kube-public Active 2d5h
kube-system Active 2d5h
Джойним ноды в кластер
Так, мне всё-таки надо добавить воркеров в наш кластер, потому что пока что я вижу только мастера:
user@k-w1:~$ kubectl get nodes
NAME STATUS ROLES AGE VERSION
k-master NotReady control-plane 2d5h v1.28.15
Если вернуться немного к результату инициализации кластера, то напомню, что мастер нам прямо сказал что нужно делать, чтобы присоединить ноду к мастеру. Попробуем:
user@k-w1:~$ sudo kubeadm join 10.0.11.1:6443 \
--token 4glzzt.3b96mmozrsum72he \
--discovery-token-ca-cert-hash sha256:557100ad9c340873e4d2d4e329fd303ba274548f1188030ad9c6569a2f745e42
[sudo] password for user:
[preflight] Running pre-flight checks
error execution phase preflight: [preflight] Some fatal errors occurred:
[ERROR FileContent--proc-sys-net-bridge-bridge-nf-call-iptables]: /proc/sys/net/bridge/bridge-nf-call-iptables does not exist
[ERROR FileContent--proc-sys-net-ipv4-ip_forward]: /proc/sys/net/ipv4/ip_forward contents are not set to 1
[preflight] If you know what you are doing, you can make a check non-fatal with `--ignore-preflight-errors=...`
To see the stack trace of this error execute with --v=5 or higher
Опять двадцать пять! Исправляемся:
user@k-w1:~$ sudo modprobe br_netfilter
user@k-w1:~$ echo "br_netfilter" | sudo tee /etc/modules-load.d/br_netfilter.conf
br_netfilter
user@k-w1:~$ sudo sysctl -w net.ipv4.ip_forward=1
net.ipv4.ip_forward = 1
Пробуем ещё раз. Ввёл команду на Join, сижу, жду…минуту, две, подозрительно. Решил проверить, идёт ли дело. Куда смотреть - я хз, поэтому как обычный сетевик решил посмотреть, есть ли вообще какое-то взаимодействие:
user@k-w1:~$ sudo tcpdump -i any -n host 10.0.11.1
tcpdump: data link type LINUX_SLL2
tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
listening on any, link-type LINUX_SLL2 (Linux cooked v2), snapshot length 262144 bytes
09:34:46.678488 ens3 Out IP 10.1.11.1.47196 > 10.0.11.1.6443: Flags [P.], seq 3173377766:3173377804, ack 458422059, win 501, options [nop,nop,TS val 785132122 ecr 906488627], length 38
09:34:46.687859 ens3 In IP 10.0.11.1.6443 > 10.1.11.1.47196: Flags [P.], seq 1:91, ack 38, win 507, options [nop,nop,TS val 906494937 ecr 785132122], length 90
09:34:46.687875 ens3 Out IP 10.1.11.1.47196 > 10.0.11.1.6443: Flags [.], ack 91, win 501, options [nop,nop,TS val 785132131 ecr 906494937], length 0
09:34:46.739604 ens3 In IP 10.0.11.1.6443 > 10.1.11.1.47196: Flags [P.], seq 1539:2226, ack 38, win 507, options [nop,nop,TS val 906494988 ecr 785132131], length 687
09:34:46.739633 ens3 Out IP 10.1.11.1.47196 > 10.0.11.1.6443: Flags [.], ack 91, win 501, options [nop,nop,TS val 785132183 ecr 906494937,nop,nop,sack 1 {1539:2226}], length 0
09:34:46.745540 ens3 In IP 10.0.11.1.6443 > 10.1.11.1.47196: Flags [.], seq 91:1539, ack 38, win 507, options [nop,nop,TS val 906494995 ecr 785132183], length 1448
09:34:46.745551 ens3 Out IP 10.1.11.1.47196 > 10.0.11.1.6443: Flags [.], ack 2226, win 497, options [nop,nop,TS val 785132189 ecr 906494995], length 0
09:34:46.745705 ens3 Out IP 10.1.11.1.47196 > 10.0.11.1.6443: Flags [P.], seq 38:73, ack 2226, win 501, options [nop,nop,TS val 785132189 ecr 906494995], length 35
09:34:46.796413 ens3 In IP 10.0.11.1.6443 > 10.1.11.1.47196: Flags [.], ack 73, win 507, options [nop,nop,TS val 906495044 ecr 785132189], length 0
В общем, чем-то они там занимаются, мешать не буду. Подожду.
Дождался:
[preflight] Running pre-flight checks
error execution phase preflight: couldn't validate the identity of the API Server: could not find a JWS signature in the cluster-info ConfigMap for token ID "4glzzt"
To see the stack trace of this error execute with --v=5 or higher
Сначала я подумал что он сагрился на символ точки в токене - потому что в ошибке используется “4glzzt”, а в команде --token 4glzzt.3b96mmozrsum72he
, но это конечно полная херня, поэтому пришлось погуглить и поговорить с нейросетью на этот счёт. Как оказалось - мой токен протух - по умолчанию он живёт 24 часа, а инициализацию я сделал пару дней назад уже, а потом пошёл работать ) В списке живых токенов его нет:
user@K-Master:~$ sudo kubeadm token list
user@K-Master:~$
Сделаем новый токен:
user@K-Master:~$ sudo kubeadm token create
ya36sc.cregu6et22m9j7q5
user@K-Master:~$ sudo kubeadm token list
TOKEN TTL EXPIRES USAGES DESCRIPTION EXTRA GROUPS
ya36sc.cregu6et22m9j7q5 23h 2025-08-02T09:47:10Z authentication,signing <none> system:bootstrappers:kubeadm:default-node-token
Второй параметр в kubeadm join
хеш сертификата мастера, он у нас не менялся. Пробуем так:
user@k-w1:~$ sudo kubeadm join 10.0.11.1:6443 \
--token ya36sc.cregu6et22m9j7q5 \
--discovery-token-ca-cert-hash sha256:557100ad9c340873e4d2d4e329fd303ba274548f1188030ad9c6569a2f745e42
Сразу попёрло:
[preflight] Running pre-flight checks
[preflight] Reading configuration from the cluster...
[preflight] FYI: You can look at this config file with 'kubectl -n kube-system get cm kubeadm-config -o yaml'
[kubelet-start] Writing kubelet configuration to file "/var/lib/kubelet/config.yaml"
[kubelet-start] Writing kubelet environment file with flags to file "/var/lib/kubelet/kubeadm-flags.env"
[kubelet-start] Starting the kubelet
[kubelet-start] Waiting for the kubelet to perform the TLS Bootstrap...
This node has joined the cluster:
* Certificate signing request was sent to apiserver and a response was received.
* The Kubelet was informed of the new secure connection details.
Run 'kubectl get nodes' on the control-plane to see this node join the cluster.
Теперь видно нашего воркера!
user@k-w1:~$ kubectl get nodes
NAME STATUS ROLES AGE VERSION
k-master NotReady control-plane 2d5h v1.28.15
k-w1 NotReady <none> 49s v1.28.15
После проделаем тоже самое на воркере-2 и воркере-3
user@k-w1:~$ kubectl get nodes
NAME STATUS ROLES AGE VERSION
k-master NotReady control-plane 2d5h v1.28.15
k-w1 NotReady <none> 3m31s v1.28.15
k-w2 NotReady <none> 14s v1.28.15
k-w3 NotReady <none> 5s v1.28.15
Итак - как и планировали - один мастер и три воркера. Однако все они в NotReady состоянии, и не готовы на себе, соответственно, никакую полезную нагрузку нести. В общем, как говорил один известный Иннокентий - “Давайте разбираться!”
Самый лучший друг в попытке получить максимальное количество информации о как-либо объекте Kubernetes-а - это kubectl describe
- тут вот можно почитать
Попробуем получить информацию о нашей ноде любой :
user@K-Master:~$ kubectl describe node k-master
Name: k-master
Roles: control-plane
Labels: beta.kubernetes.io/arch=amd64
beta.kubernetes.io/os=linux
kubernetes.io/arch=amd64
kubernetes.io/hostname=k-master
kubernetes.io/os=linux
node-role.kubernetes.io/control-plane=
node.kubernetes.io/exclude-from-external-load-balancers=
Annotations: kubeadm.alpha.kubernetes.io/cri-socket: unix:///var/run/containerd/containerd.sock
node.alpha.kubernetes.io/ttl: 0
volumes.kubernetes.io/controller-managed-attach-detach: true
CreationTimestamp: Wed, 30 Jul 2025 03:54:44 +0000
Taints: node-role.kubernetes.io/control-plane:NoSchedule
node.kubernetes.io/not-ready:NoSchedule
Unschedulable: false
Lease:
HolderIdentity: k-master
AcquireTime: <unset>
RenewTime: Fri, 01 Aug 2025 10:20:47 +0000
Conditions:
Type Status LastHeartbeatTime LastTransitionTime Reason Message
---- ------ ----------------- ------------------ ------ -------
MemoryPressure False Fri, 01 Aug 2025 10:20:49 +0000 Wed, 30 Jul 2025 03:54:42 +0000 KubeletHasSufficientMemory kubelet has sufficient memory available
DiskPressure False Fri, 01 Aug 2025 10:20:49 +0000 Wed, 30 Jul 2025 03:54:42 +0000 KubeletHasNoDiskPressure kubelet has no disk pressure
PIDPressure False Fri, 01 Aug 2025 10:20:49 +0000 Wed, 30 Jul 2025 03:54:42 +0000 KubeletHasSufficientPID kubelet has sufficient PID available
Ready False Fri, 01 Aug 2025 10:20:49 +0000 Wed, 30 Jul 2025 03:54:42 +0000 KubeletNotReady container runtime network not ready: NetworkReady=false reason:NetworkPluginNotReady message:Network plugin returns error: cni plugin not initialized
Addresses:
InternalIP: 10.0.11.1
Hostname: k-master
Capacity:
cpu: 2
ephemeral-storage: 59543468Ki
hugepages-2Mi: 0
memory: 4018140Ki
pods: 110
Allocatable:
cpu: 2
ephemeral-storage: 54875260018
hugepages-2Mi: 0
memory: 3915740Ki
pods: 110
System Info:
Machine ID: 9b501691e27e441fa1ddadcbde6948b8
System UUID: adb6f6f8-3ebd-4da3-bb18-15d69dfd3393
Boot ID: 3c2d4346-5c1b-425e-8463-164b41d90f0c
Kernel Version: 5.15.0-69-generic
OS Image: Ubuntu 22.04.2 LTS
Operating System: linux
Architecture: amd64
Container Runtime Version: containerd://1.7.27
Kubelet Version: v1.28.15
Kube-Proxy Version: v1.28.15
PodCIDR: 10.66.0.0/24
PodCIDRs: 10.66.0.0/24
Non-terminated Pods: (5 in total)
Namespace Name CPU Requests CPU Limits Memory Requests Memory Limits Age
--------- ---- ------------ ---------- --------------- ------------- ---
kube-system etcd-k-master 100m (5%) 0 (0%) 100Mi (2%) 0 (0%) 2d6h
kube-system kube-apiserver-k-master 250m (12%) 0 (0%) 0 (0%) 0 (0%) 2d6h
kube-system kube-controller-manager-k-master 200m (10%) 0 (0%) 0 (0%) 0 (0%) 2d6h
kube-system kube-proxy-dsf2f 0 (0%) 0 (0%) 0 (0%) 0 (0%) 2d6h
kube-system kube-scheduler-k-master 100m (5%) 0 (0%) 0 (0%) 0 (0%) 2d6h
Allocated resources:
(Total limits may be over 100 percent, i.e., overcommitted.)
Resource Requests Limits
-------- -------- ------
cpu 650m (32%) 0 (0%)
memory 100Mi (2%) 0 (0%)
ephemeral-storage 0 (0%) 0 (0%)
hugepages-2Mi 0 (0%) 0 (0%)
Events: <none>
Нам тут интересен раздел Conditions, где есть один из типов кондишна - “Ready” Ну и вот там явно видно почему нода не Ready, собственно - “container runtime network not ready: NetworkReady=false reason:NetworkPluginNotReady message:Network plugin returns error: cni plugin not initialized”
Альтерантивно, эту информацию в виде красивого json-а можно извлечь так (ну тут, конечно, надо понимать структуру - то есть понимать куда и как тыкать):
user@K-Master:~$ kubectl get node k-master -o jsonpath='{.status.conditions}' | jq '.[] | select(.type == "Ready")'
{
"lastHeartbeatTime": "2025-08-01T10:31:02Z",
"lastTransitionTime": "2025-07-30T03:54:42Z",
"message": "container runtime network not ready: NetworkReady=false reason:NetworkPluginNotReady message:Network plugin returns error: cni plugin not initialized",
"reason": "KubeletNotReady",
"status": "False",
"type": "Ready"
}
В общем, как видно - опять виноваты сетевики! - NetworkReady=false
- сеть не готова, ёпта! А не готова, она потому что Network plugin returns error: cni plugin not initialized
! Забыли мы про CNI кароче.
Calico
Напомню, что я решил остановиться на Calico, на нём и остановлюсь!
Устанавливаем калику одной строчкой:
user@K-Master:~$ kubectl apply -f https://docs.projectcalico.org/manifests/calico.yaml
poddisruptionbudget.policy/calico-kube-controllers created
serviceaccount/calico-kube-controllers created
serviceaccount/calico-node created
configmap/calico-config created
customresourcedefinition.apiextensions.k8s.io/bgpconfigurations.crd.projectcalico.org created
customresourcedefinition.apiextensions.k8s.io/bgppeers.crd.projectcalico.org created
customresourcedefinition.apiextensions.k8s.io/blockaffinities.crd.projectcalico.org created
customresourcedefinition.apiextensions.k8s.io/caliconodestatuses.crd.projectcalico.org created
customresourcedefinition.apiextensions.k8s.io/clusterinformations.crd.projectcalico.org created
customresourcedefinition.apiextensions.k8s.io/felixconfigurations.crd.projectcalico.org created
customresourcedefinition.apiextensions.k8s.io/globalnetworkpolicies.crd.projectcalico.org created
customresourcedefinition.apiextensions.k8s.io/globalnetworksets.crd.projectcalico.org created
customresourcedefinition.apiextensions.k8s.io/hostendpoints.crd.projectcalico.org created
customresourcedefinition.apiextensions.k8s.io/ipamblocks.crd.projectcalico.org created
customresourcedefinition.apiextensions.k8s.io/ipamconfigs.crd.projectcalico.org created
customresourcedefinition.apiextensions.k8s.io/ipamhandles.crd.projectcalico.org created
customresourcedefinition.apiextensions.k8s.io/ippools.crd.projectcalico.org created
customresourcedefinition.apiextensions.k8s.io/ipreservations.crd.projectcalico.org created
customresourcedefinition.apiextensions.k8s.io/kubecontrollersconfigurations.crd.projectcalico.org created
customresourcedefinition.apiextensions.k8s.io/networkpolicies.crd.projectcalico.org created
customresourcedefinition.apiextensions.k8s.io/networksets.crd.projectcalico.org created
clusterrole.rbac.authorization.k8s.io/calico-kube-controllers created
clusterrole.rbac.authorization.k8s.io/calico-node created
clusterrolebinding.rbac.authorization.k8s.io/calico-kube-controllers created
clusterrolebinding.rbac.authorization.k8s.io/calico-node created
daemonset.apps/calico-node created
deployment.apps/calico-kube-controllers created
Скачали манифест (https://docs.projectcalico.org/manifests/calico.yaml) с сайта проекта и установили калику!
Ноды, кстати, сразу перешли в Ready:
user@k-w1:~$ kubectl get nodes
NAME STATUS ROLES AGE VERSION
k-master Ready control-plane 2d7h v1.28.15
k-w1 Ready <none> 82m v1.28.15
k-w2 Ready <none> 79m v1.28.15
k-w3 Ready <none> 79m v1.28.15
На всех нодах появились какие-то подики как то связанные с Calico:
user@K-Master:~$ kubectl get pods -n kube-system -o wide | grep -E 'calico|NAME'
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
calico-kube-controllers-658d97c59c-kq2ld 1/1 Running 0 17m 10.66.207.66 k-w1 <none> <none>
calico-node-b2rgf 1/1 Running 0 17m 10.2.22.1 k-w2 <none> <none>
calico-node-lfbfg 1/1 Running 0 17m 10.3.33.1 k-w3 <none> <none>
calico-node-ncqcp 1/1 Running 0 17m 10.0.11.1 k-master <none> <none>
calico-node-wpw78 1/1 Running 0 17m 10.1.11.1 k-w1 <none> <none>
Вот тут я сразу обращаю внимание на подик calico-kube-controllers-658d97c59c-kq2ld
- очевидно что это некий контроллер нашего SDN-а, который делает так, что бы сеть работала - даёт команды на ноды, сообщает, что надо сделать и так далее. Классика кароче. Но меня зацепил его IP-адрес - 10.66.207.66. Да, в момент инициализации кластера мы указали что CIDR для подов будет 10.66.0.0/16, но почему именно 10.66.207.66? Давайте посмотрим, что этот контроллер себе позволил:
***** смотрим, на настроенные CNI-ем IP Pool-ы *****
user@K-Master:~$ kubectl get ippools -o yaml
apiVersion: v1
items:
- apiVersion: crd.projectcalico.org/v1
kind: IPPool
metadata:
annotations:
projectcalico.org/metadata: '{"uid":"7c7ea10d-5b11-44bc-996f-762f043ddaf7","creationTimestamp":"2025-08-01T10:58:00Z"}'
creationTimestamp: "2025-08-01T10:58:00Z"
generation: 1
name: default-ipv4-ippool
resourceVersion: "260252"
uid: d85f0e55-8dde-49c6-9d80-a8ed5c66419f
spec:
allowedUses:
- Workload
- Tunnel
blockSize: 26
cidr: 10.66.0.0/16
ipipMode: Always
natOutgoing: true
nodeSelector: all()
vxlanMode: Never
kind: List
metadata:
resourceVersion: ""
Самое интересно для нас - в разделе spec:
cidr: 10.66.0.0/16
- понятно, это то что мы задали на этапе инициализацииblockSize: 26
- это дефолтная маска с которой будет выделена подсетка. То есть из 10.66.0.0/16 берётся подсетка /26 и отдаётся ноде, дальше калико будет назначать IP-адреса на поды согласно этой сеткеvxlanMode: Never
- приятно порадовало )ipipMode: Always
- интересное… по умолчанию, получается, трафик между подиками на разных нодах будет запаковываться в IPIP - чтож, это логично - Калико же пока не догадывается, что мы тут делаем плоскую маршрутизируемую сеть, а трафик между подами ему как-то доставлять надоnatOutgoing: true
- ситуация аналогичная - опция нужна для выхода из пода “наружу” - например в Интернет или куда-то ещё за пределы кластера. Тоже поменяем, не нужОн нам этот NAT
Ладно, это мы выяснили про некую глобальную настройку на уровне кластера. А как узнать какая подсеть на какую ноду выделилась? А вот так, смотрим какие тут CIDR-ы наш калико кому назначил:
user@k-w1:~$ kubectl get blockaffinities -o yaml
apiVersion: v1
items:
- apiVersion: crd.projectcalico.org/v1
kind: BlockAffinity
metadata:
annotations:
projectcalico.org/metadata: '{"creationTimestamp":null}'
creationTimestamp: "2025-08-01T10:58:00Z"
generation: 2
name: k-master-10-66-73-128-26
resourceVersion: "260260"
uid: 0a22823f-17ac-4fa7-bc5a-31ca8d68c0d3
spec:
cidr: 10.66.73.128/26
deleted: "false"
node: k-master
state: confirmed
- apiVersion: crd.projectcalico.org/v1
kind: BlockAffinity
metadata:
annotations:
projectcalico.org/metadata: '{"creationTimestamp":null}'
creationTimestamp: "2025-08-01T10:58:04Z"
generation: 2
name: k-w1-10-66-207-64-26
resourceVersion: "260300"
uid: 4a6a748b-ff9e-4a16-a17e-417a116937a2
spec:
cidr: 10.66.207.64/26
deleted: "false"
node: k-w1
state: confirmed
- apiVersion: crd.projectcalico.org/v1
kind: BlockAffinity
metadata:
annotations:
projectcalico.org/metadata: '{"creationTimestamp":null}'
creationTimestamp: "2025-08-01T10:58:07Z"
generation: 2
name: k-w2-10-66-53-192-26
resourceVersion: "260363"
uid: aeb048ac-7494-4bd7-bc9e-ae8e52a5f3e5
spec:
cidr: 10.66.53.192/26
deleted: "false"
node: k-w2
state: confirmed
- apiVersion: crd.projectcalico.org/v1
kind: BlockAffinity
metadata:
annotations:
projectcalico.org/metadata: '{"creationTimestamp":null}'
creationTimestamp: "2025-08-01T10:58:07Z"
generation: 2
name: k-w3-10-66-122-192-26
resourceVersion: "260341"
uid: e1a233b4-afa5-406e-9292-27da0fc4d4ba
spec:
cidr: 10.66.122.192/26
deleted: "false"
node: k-w3
state: confirmed
kind: List
metadata:
resourceVersion: ""
Ну или лучше отфильтруем по kw-1 (там наш контроллер засел):
user@k-w1:~$ kubectl get blockaffinities -o json | jq '.items[] | select(.spec.node == "k-w1") |.spec'
{
"cidr": "10.66.207.64/26",
"deleted": "false",
"node": "k-w1",
"state": "confirmed"
}
Ну теперь понятно - адрес 10.66.207.66 вполне себе входит в CIDR “10.66.207.64/26”.
Что там с туннелями то?
Напомню нашу схему (ну я на неё ещё облачка с подовыми сидрами нанёс для наглядности)

И вот у нас подик с контроллером живёт на первом воркере в сети 10.66.207.64/26, будет ли у него связь с подом где нибудь на другой ноде? Особенно, с учётом того что Underlay таких маршрутов не знает:
#вывод таблицы роутинга с коммутатора Leaf3:
Leaf-3#show ip ro 10.66.207.66
Gateway of last resort is not set
Leaf-3#show ip ro 10.66.122.194
Gateway of last resort is not set
# Вот всё что есть:
Leaf-3#show ip ro
Gateway of last resort is not set
O 10.0.11.0/31 [110/30] via 10.33.99.0, Ethernet1
O 10.1.11.0/31 [110/30] via 10.33.99.0, Ethernet1
O 10.2.22.0/31 [110/30] via 10.33.99.0, Ethernet1
C 10.3.33.0/31 is directly connected, Ethernet2
O 10.11.99.0/31 [110/20] via 10.33.99.0, Ethernet1
O 10.22.99.0/31 [110/20] via 10.33.99.0, Ethernet1
C 10.33.99.0/31 is directly connected, Ethernet1
Ну чтож, давайте проверим, создадим простой альпийский контейнер, зайдём в shell И попингуем наш контроллер:
user@k-w2:~$ kubectl run test-pod --image=alpine --restart=Never --rm -it -- sh
If you don't see a command prompt, try pressing enter.
/ #
/ # ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
inet6 ::1/128 scope host
valid_lft forever preferred_lft forever
2: tunl0@NONE: <NOARP> mtu 1480 qdisc noop state DOWN qlen 1000
link/ipip 0.0.0.0 brd 0.0.0.0
4: eth0@if9: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1480 qdisc noqueue state UP
link/ether 02:0a:bb:9f:e4:55 brd ff:ff:ff:ff:ff:ff
inet 10.66.122.194/32 scope global eth0
valid_lft forever preferred_lft forever
inet6 fe80::a:bbff:fe9f:e455/64 scope link
valid_lft forever preferred_lft forever
IP мы вычислили - 10.66.122.194, это входит в пул kw3 (10.66.122.192/26), убедимся что под запустился именно там:
user@k-w1:~$ kubectl get pods -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
test-pod 1/1 Running 0 10m 10.66.122.194 k-w3 <none> <none>
Нода наша, IP наш. Кстати, как видно из вывода выше - на самом контейнере есть интерфейс 4: eth0@if9
и судя по наличию собаки - это верный признак veth-пары. И где же его дружок-пирожок? Логично предположить, что за форвардинг трафика из пода куда бы то ни было, ещё должна отвечать его мамка (хостовая система), соответственно и дружка надо искать там:
user@k-w3:~$ ip link show
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
2: ens3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP mode DEFAULT group default qlen 1000
link/ether 50:52:0b:00:6d:00 brd ff:ff:ff:ff:ff:ff
altname enp0s3
3: ens4: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP mode DEFAULT group default qlen 1000
link/ether 50:52:0b:00:6d:01 brd ff:ff:ff:ff:ff:ff
altname enp0s4
4: ens5: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP mode DEFAULT group default qlen 1000
link/ether 50:52:0b:00:6d:02 brd ff:ff:ff:ff:ff:ff
altname enp0s5
5: tunl0@NONE: <NOARP,UP,LOWER_UP> mtu 1480 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000
link/ipip 0.0.0.0 brd 0.0.0.0
9: cali7fba7a35b74@if4: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1480 qdisc noqueue state UP mode DEFAULT group default
link/ether ee:ee:ee:ee:ee:ee brd ff:ff:ff:ff:ff:ff link-netns cni-62e568eb-e285-550c-1c3a-5e86eb5dd440
А, ну вот он, поглядите - как раз с номером 9 сидит - 9: cali7fba7a35b74@if4
Ок, то есть трафик из пода через veth пару выниривает из неймспейса пода в корневой неймспейс, а дальше что? А дальше классика - трафик надо куда-то сфорвардить (не зря я включал net.ipv4.ip_forward=1
) согласно локальной таблицы маршрутизации на хосте. А там что? Если трафику с нашего пода (10.66.122.194) нужно будет пойти на адрес пода на kw-1 (10.66.207.66), то он вот так пойдёт:
user@k-w3:~$ ip r get 10.66.207.66
10.66.207.66 via 10.0.137.15 dev tunl0 src 10.66.122.192 uid 1000
cache
А, ну вот и туннель. Какие ещё маршруты через него есть?
user@k-w3:~$ ip r | grep tunl0
10.66.53.192/26 via 10.0.137.112 dev tunl0 proto bird onlink
10.66.73.128/26 via 10.0.137.12 dev tunl0 proto bird onlink
10.66.207.64/26 via 10.0.137.15 dev tunl0 proto bird onlink
То есть наша k-w3 нода знает, что трафик до других трёх товарищей (двух воркеров и мастера) надо отправить в туннель
Со стороны мастера, например, будет выглядеть так:
user@K-Master:~$ ip r | grep tunl0
10.66.53.192/26 via 10.0.137.112 dev tunl0 proto bird onlink
10.66.122.192/26 via 10.0.137.142 dev tunl0 proto bird onlink
10.66.207.64/26 via 10.0.137.15 dev tunl0 proto bird onlink
Тут маршруты до всех трёх воркеров
Правда, тут калико немного ссамовольничал, и для построения тунелей выбрал неожиданный для меня путь - он использует интерфейсы которые я подкостылил, для того что бы в Интернет с нод ходить, а не через наш красивый Underlay. Ну Бог ему судья, это не важно сейчас, так как туннели мы разберём. В общем если подампать трафик на физическом интерфейсе ноды, через который строится туннель, а потом сделать пинги от пода на k-w3 до пода на k-w1, то мы увидем тот самый IPIP трафик:
user@k-w3:~$ sudo tcpdump -i ens5 -n
tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
listening on ens5, link-type EN10MB (Ethernet), snapshot length 262144 bytes
17:59:59.484554 IP 10.0.137.142 > 10.0.137.15: IP 10.66.122.194 > 10.66.207.66: ICMP echo request, id 18, seq 72, length 64
17:59:59.485009 IP 10.0.137.15 > 10.0.137.142: IP 10.66.207.66 > 10.66.122.194: ICMP echo reply, id 18, seq 72, length 64
18:00:00.484888 IP 10.0.137.142 > 10.0.137.15: IP 10.66.122.194 > 10.66.207.66: ICMP echo request, id 18, seq 73, length 64
18:00:00.485324 IP 10.0.137.15 > 10.0.137.142: IP 10.66.207.66 > 10.66.122.194: ICMP echo reply, id 18, seq 73, length 64
18:00:01.485194 IP 10.0.137.142 > 10.0.137.15: IP 10.66.122.194 > 10.66.207.6
Такая вот схемка нарисовалась:

Птица
Ещё интересный момент - то, как нода узнала про эти маршруты. По таблице маршрутизации видно, что ядру про него рассказал bird, Карл! 10.66.207.64/26 via 10.0.137.15 dev tunl0 proto bird onlink
То есть, где-то в недрах всей этой машинрии живёт бёрд. Попробую найти гуано этой птички. Не буду далеко ходить от k-w3 и посмотрю что нибудь там. Для начала надо понять какой под там отвечает за калико:
Вот оно
user@K-Master:~$ kubectl get pods -A -o wide | grep calico | grep k-w3
kube-system calico-node-lfbfg 1/1 Running 0 7h48m 10.3.33.1 k-w3 <none> <none>
Попробуем провалится в его shell и выполнить birdc show protocols
:
kubectl exec -it calico-node-lfbfg -n kube-system -- sh
Defaulted container "calico-node" out of: calico-node, upgrade-ipam (init), install-cni (init), mount-bpffs (init)
sh-4.4# birdc
sh: birdc: command not found
Тут я сначала растроился, а потом погуглил и узнал, что оказывается у birdc есть “облегчённая версия” - birdcl
В общем, так работает:
***** видно что демон запущен
sh-4.4# birdcl show status
BIRD v0.3.3+birdv1.6.8 ready.
BIRD v0.3.3+birdv1.6.8
Router ID is 10.0.137.142
Current server time is 2025-08-03 04:54:44
Last reboot on 2025-08-01 10:58:09
Last reconfiguration on 2025-08-01 10:58:09
Daemon is up and running
***** Интерфейсы видны:
sh-4.4# birdcl show interface
BIRD v0.3.3+birdv1.6.8 ready.
lo up (index=1)
MultiAccess AdminUp LinkUp Loopback Ignored MTU=65536
127.0.0.1/8 (Primary, scope host)
ens3 up (index=2)
MultiAccess Broadcast Multicast AdminUp LinkUp MTU=1500
10.3.33.1/31 (Primary, opposite 10.3.33.0, scope site)
ens4 DOWN (index=3)
MultiAccess Broadcast Multicast AdminUp LinkUp MTU=1500
ens5 up (index=4)
MultiAccess Broadcast Multicast AdminUp LinkUp MTU=1500
10.0.137.142/24 (Primary, scope site)
tunl0 up (index=5)
MultiAccess AdminUp LinkUp MTU=1480
10.66.122.192/32 (Primary, scope site)
cali7fba7a35b74 DOWN (index=9)
MultiAccess Broadcast Multicast AdminUp LinkUp MTU=1480
***** сесии установлены
sh-4.4# birdcl show protocols | grep BGP
Mesh_10_0_137_12 BGP master up 2025-08-01 Established
Mesh_10_0_137_15 BGP master up 2025-08-01 Established
Mesh_10_0_137_112 BGP master up 2025-08-01 Established
***** маршруты долетели:
sh-4.4# birdcl show route
BIRD v0.3.3+birdv1.6.8 ready.
10.66.53.192/26 via 10.0.137.112 on ens5 [Mesh_10_0_137_112 2025-08-01] * (100/0) [i]
10.66.73.128/26 via 10.0.137.12 on ens5 [Mesh_10_0_137_12 2025-08-01] * (100/0) [i]
10.66.207.64/26 via 10.0.137.15 on ens5 [Mesh_10_0_137_15 2025-08-01] * (100/0) [i]
В целом, кажется, логика простая - Calico организует некую full mesh iBGP связанность между всеми нодам, в рамках которой ноды дают знать своим друзяшкам о своих подовых сетях.
Если интересны глубины глубин под спойлером - содержимое конфигов bird-а, который генерируется внутренними механизмами Calico, на примере всё той же k-w3
function apply_communities ()
{
}
# Generated by confd
include "bird_aggr.cfg";
include "bird_ipam.cfg";
router id 10.0.137.142;
# Configure synchronization between routing tables and kernel.
protocol kernel {
learn; # Learn all alien routes from the kernel
persist; # Don't remove routes on bird shutdown
scan time 2; # Scan kernel routing table every 2 seconds
import all;
export filter calico_kernel_programming; # Default is export none
graceful restart; # Turn on graceful restart to reduce potential flaps in
# routes when reloading BIRD configuration. With a full
# automatic mesh, there is no way to prevent BGP from
# flapping since multiple nodes update their BGP
# configuration at the same time, GR is not guaranteed to
# work correctly in this scenario.
merge paths on; # Allow export multipath routes (ECMP)
}
# Watch interface up/down events.
protocol device {
debug { states };
scan time 2; # Scan interfaces every 2 seconds
}
protocol direct {
debug { states };
interface -"cali*", -"kube-ipvs*", "*"; # Exclude cali* and kube-ipvs* but
# include everything else. In
# IPVS-mode, kube-proxy creates a
# kube-ipvs0 interface. We exclude
# kube-ipvs0 because this interface
# gets an address for every in use
# cluster IP. We use static routes
# for when we legitimately want to
# export cluster IPs.
}
# Template for all BGP clients
template bgp bgp_template {
debug { states };
description "Connection to BGP peer";
local as 64512;
gateway recursive; # This should be the default, but just in case.
import all; # Import all routes, since we don't know what the upstream
# topology is and therefore have to trust the ToR/RR.
export filter calico_export_to_bgp_peers; # Only want to export routes for workloads.
add paths on;
graceful restart; # See comment in kernel section about graceful restart.
connect delay time 2;
connect retry time 5;
error wait time 5,30;
}
# ------------- Node-to-node mesh -------------
# For peer /host/k-master/ip_addr_v4
protocol bgp Mesh_10_0_137_12 from bgp_template {
multihop;
ttl security off;
neighbor 10.0.137.12 as 64512;
source address 10.0.137.142; # The local address we use for the TCP connection
}
# For peer /host/k-w1/ip_addr_v4
protocol bgp Mesh_10_0_137_15 from bgp_template {
multihop;
ttl security off;
neighbor 10.0.137.15 as 64512;
source address 10.0.137.142; # The local address we use for the TCP connection
passive on; # Mesh is unidirectional, peer will connect to us.
}
# For peer /host/k-w2/ip_addr_v4
protocol bgp Mesh_10_0_137_112 from bgp_template {
multihop;
ttl security off;
neighbor 10.0.137.112 as 64512;
source address 10.0.137.142; # The local address we use for the TCP connection
}
# For peer /host/k-w3/ip_addr_v4
# Skipping ourselves (10.0.137.142)
# ------------- Global peers -------------
# No global peers configured.
# ------------- Node-specific peers -------------
# No node-specific peers configured.
Тут, конечно, можно уйти ещё глубже и расказывать про то КАК ИМЕННО эти конфиги появляются на подиках калико, но не хочу - если в кратце, то там некий confd ходит куда-то в единое хранилище (ну конечно etcd, куда ему ещё ходить) и генерит на основе шаблонов конфиги.
calicoctl
Блин, пора уже настраивать сеть давно и отказываться от туннелей, но перед этим просто необходимо упомянуть, что есть ещё софтина calicoctl из названия которой должно быть понятна, что она управляет всем этим набором json-ов и прочих ямлов на более высоком уровне абстракции. Ну то есть если вы ещё не прониклись духом DevOps, и вам нужен болие-лименее понятный инструмент управления своим CNI - то надо юзать, его. Вот, например, можно IPAM текущий глянуть:
# Какой у нас CIDR на кластер
user@K-Master:~$ calicoctl ipam show
+----------+--------------+-----------+------------+--------------+
| GROUPING | CIDR | IPS TOTAL | IPS IN USE | IPS FREE |
+----------+--------------+-----------+------------+--------------+
| IP Pool | 10.66.0.0/16 | 65536 | 8 (0%) | 65528 (100%) |
+----------+--------------+-----------+------------+--------------+
# как префиксы распределены
user@k-w1:~$ calicoctl ipam show --show-blocks
+----------+------------------+-----------+------------+--------------+
| GROUPING | CIDR | IPS TOTAL | IPS IN USE | IPS FREE |
+----------+------------------+-----------+------------+--------------+
| IP Pool | 10.66.0.0/16 | 65536 | 8 (0%) | 65528 (100%) |
| Block | 10.66.122.192/26 | 64 | 2 (3%) | 62 (97%) |
| Block | 10.66.207.64/26 | 64 | 4 (6%) | 60 (94%) |
| Block | 10.66.53.192/26 | 64 | 1 (2%) | 63 (98%) |
| Block | 10.66.73.128/26 | 64 | 1 (2%) | 63 (98%) |
+----------+------------------+-----------+------------+--------------+
Или посмотреть достаточно подробную информацию о том, что вообще происходит:
user@K-Master:~$ calicoctl ipam check --show-all-ips
Checking IPAM for inconsistencies...
Loading all IPAM blocks...
Found 4 IPAM blocks.
IPAM block 10.66.122.192/26 affinity=host:k-w3:
10.66.122.192 allocated; attrs Main:ipip-tunnel-addr-k-w3 Extra:node=k-w3,type=ipipTunnelAddress
10.66.122.194 allocated; attrs Main:k8s-pod-network.a859801be9f0f3d1f827d5e0468b4d49892d9f664cf3bad7840d81e2e1d5275c Extra:namespace=default,node=k-w3,pod=test-pod,timestamp=2025-08-01 15:46:03.586215975 +0000 UTC
IPAM block 10.66.207.64/26 affinity=host:k-w1:
10.66.207.64 allocated; attrs Main:ipip-tunnel-addr-k-w1 Extra:node=k-w1,type=ipipTunnelAddress
10.66.207.65 allocated; attrs Main:k8s-pod-network.fa46412671b665c9f465419e2438c53ff2e8f507e19f1c1e0ec46df242d46943 Extra:namespace=kube-system,node=k-w1,pod=coredns-5dd5756b68-mx2x2,timestamp=2025-08-01 10:58:05.534449903 +0000 UTC
10.66.207.66 allocated; attrs Main:k8s-pod-network.0385664c41a266d42b7dc0a16a9e9e78e093ac2686324b4bc9e099751cdf2e8f Extra:namespace=kube-system,node=k-w1,pod=calico-kube-controllers-658d97c59c-kq2ld,timestamp=2025-08-01 10:58:05.562415379 +0000 UTC
10.66.207.67 allocated; attrs Main:k8s-pod-network.9412bb8836cb7d82c57056120496ffcc34cb92acc86ac2668a6429208acbe505 Extra:namespace=kube-system,node=k-w1,pod=coredns-5dd5756b68-hjxxk,timestamp=2025-08-01 10:58:05.637413164 +0000 UTC
IPAM block 10.66.53.192/26 affinity=host:k-w2:
10.66.53.192 allocated; attrs Main:ipip-tunnel-addr-k-w2 Extra:node=k-w2,type=ipipTunnelAddress
IPAM block 10.66.73.128/26 affinity=host:k-master:
10.66.73.128 allocated; attrs Main:ipip-tunnel-addr-k-master Extra:node=k-master,type=ipipTunnelAddress
IPAM blocks record 8 allocations.
Loading all IPAM pools...
10.66.0.0/16
Found 1 active IP pools.
Loading all nodes.
10.66.73.128 belongs to Node(k-master)
10.66.207.64 belongs to Node(k-w1)
10.66.53.192 belongs to Node(k-w2)
10.66.122.192 belongs to Node(k-w3)
Found 4 node tunnel IPs.
Loading all workload endpoints.
10.66.122.194 belongs to Workload(default/k--w3-k8s-test--pod-eth0)
10.66.207.66 belongs to Workload(kube-system/k--w1-k8s-calico--kube--controllers--658d97c59c--kq2ld-eth0)
10.66.207.67 belongs to Workload(kube-system/k--w1-k8s-coredns--5dd5756b68--hjxxk-eth0)
10.66.207.65 belongs to Workload(kube-system/k--w1-k8s-coredns--5dd5756b68--mx2x2-eth0)
Found 4 workload IPs.
Workloads and nodes are using 8 IPs.
Loading all handles
Looking for top (up to 20) nodes by allocations...
k-w1 has 4 allocations
k-w3 has 2 allocations
k-w2 has 1 allocations
k-master has 1 allocations
Node with most allocations has 4; median is 1
Scanning for IPs that are allocated but not actually in use...
Found 0 IPs that are allocated in IPAM but not actually in use.
Scanning for IPs that are in use by a workload or node but not allocated in IPAM...
Found 0 in-use IPs that are not in active IP pools.
Found 0 in-use IPs that are in active IP pools but have no corresponding IPAM allocation.
Scanning for IPAM handles with no matching IPs...
Found 0 handles with no matching IPs (and 8 handles with matches).
Scanning for IPs with missing handle...
Found 0 handles mentioned in blocks with no matching handle resource.
Check complete; found 0 problems.
По выводу можно увидеть какие IP вообще куда заалоцировались, какими делами занимаются. А можно просто глянуть состояние BGP:
user@k-w1:~$ sudo calicoctl node status
Calico process is running.
IPv4 BGP status
+--------------+-------------------+-------+------------+-------------+
| PEER ADDRESS | PEER TYPE | STATE | SINCE | INFO |
+--------------+-------------------+-------+------------+-------------+
| 10.0.137.12 | node-to-node mesh | up | 2025-08-01 | Established |
| 10.0.137.112 | node-to-node mesh | up | 2025-08-01 | Established |
| 10.0.137.142 | node-to-node mesh | up | 2025-08-01 | Established |
+--------------+-------------------+-------+------------+-------------+
IPv6 BGP status
С помощью calicoctl можно не только снимать статусную информацию какую-то, но и изменять конфигурацию. Ещё раз - calicotl это просто некая абстракция над всем этим ворохом конфигурационных файлов.
BGP до Underlay!
Ладно, как я ни старался оттягивать этот момент, пытаясь погрузиться в детали того, как тут всё устроено - уже пора к финишу подходить. Всё равно - чем дальше копаешь, тем больше понимаешь что нихуя не понимаешь. А я не перфекционист - работу не сабботирую.
Поэтому лучше приступим к сетевиковской практике наконец-то
Поменяем пулы с дефолтных /26 на хотя бы /24
Отключим нафиг туннели и NAT, потому что…
Настроим BGP в сторону ToR-ов и сделаем плоскую маршртузируемую сеть
Какой в этом вообще смысл? Да смысл простой - я хочу свои поды прозрачно видеть по сети из “внешнего” мира, вот, например, с этой специально созданной юзерской машины:

Сейчас, оттуда я могу видеть свой шлюз:
user@Kuber-Puper-User:~$ ping 10.4.44.0
PING 10.4.44.0 (10.4.44.0) 56(84) bytes of data.
64 bytes from 10.4.44.0: icmp_seq=1 ttl=64 time=4.05 ms
64 bytes from 10.4.44.0: icmp_seq=2 ttl=64 time=3.11 ms
и даже ноды:
user@Kuber-Puper-User:~$ ping 10.1.11.1
PING 10.1.11.1 (10.1.11.1) 56(84) bytes of data.
64 bytes from 10.1.11.1: icmp_seq=1 ttl=61 time=22.9 ms
64 bytes from 10.1.11.1: icmp_seq=2 ttl=61 time=19.0 ms
^C
--- 10.1.11.1 ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 1000ms
rtt min/avg/max/mdev = 18.951/20.915/22.880/1.964 ms
user@Kuber-Puper-User:~$ ssh user@10.1.11.1
The authenticity of host '10.1.11.1 (10.1.11.1)' can't be established.
ED25519 key fingerprint is SHA256:hiK+HiiCH4w8qfsES+m5m33FCm4+/a+aE9Nko69S7Us.
This key is not known by any other names
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added '10.1.11.1' (ED25519) to the list of known hosts.
user@10.1.11.1's password:
user@k-w1:~$
Но не подики. Блин, забыл какие там IP-адреса у подиков:
user@k-w1:~$ kubectl get pods -A \
-o custom-columns="NAMESPACE:.metadata.namespace,NAME:.metadata.name,IP:.status.podIP" \
--no-headers \
| grep -E "test-pod|calico-kube"
default test-pod 10.66.122.194
kube-system calico-kube-controllers-658d97c59c-kq2ld 10.66.207.66
А, точно. В общем, не пингается с юзерской ноды ничего :(
user@Kuber-Puper-User:~$ ping 10.66.122.194
PING 10.66.122.194 (10.66.122.194) 56(84) bytes of data.
From 10.4.44.0 icmp_seq=1 Destination Net Unreachable
From 10.4.44.0 icmp_seq=2 Destination Net Unreachable
^C
--- 10.66.122.194 ping statistics ---
2 packets transmitted, 0 received, +2 errors, 100% packet loss, time 1002ms
user@Kuber-Puper-User:~$ ping 10.66.207.66
PING 10.66.207.66 (10.66.207.66) 56(84) bytes of data.
From 10.4.44.0 icmp_seq=1 Destination Net Unreachable
From 10.4.44.0 icmp_seq=2 Destination Net Unreachable
^C
Свич нам отвечает, что понятия не имеет куда трафик отправлять. Свич не врёт, всё так и есть - маршрута, напомню, в андерлее нет:
Leaf-3#show ip ro 10.66.122.194
Leaf-3#show ip ro 10.66.207.66
#Вот всё что есть:
Leaf-3#show ip ro
Gateway of last resort is not set
O 10.0.11.0/31 [110/30] via 10.33.99.0, Ethernet1
O 10.1.11.0/31 [110/30] via 10.33.99.0, Ethernet1
O 10.2.22.0/31 [110/30] via 10.33.99.0, Ethernet1
C 10.3.33.0/31 is directly connected, Ethernet2
C 10.4.44.0/31 is directly connected, Ethernet3
O 10.11.99.0/31 [110/20] via 10.33.99.0, Ethernet1
O 10.22.99.0/31 [110/20] via 10.33.99.0, Ethernet1
C 10.33.99.0/31 is directly connected, Ethernet1
При этом подики могут по прежнему видеть друг друга:
user@k-w1:~$ kubectl exec -it test-pod -- sh
/ # ping 10.66.207.66
PING 10.66.207.66 (10.66.207.66): 56 data bytes
64 bytes from 10.66.207.66: seq=0 ttl=62 time=0.822 ms
64 bytes from 10.66.207.66: seq=1 ttl=62 time=0.808 ms
^C
--- 10.66.207.66 ping statistics ---
2 packets transmitted, 2 packets received, 0% packet loss
round-trip min/avg/max = 0.808/0.815/0.822 ms
Меняем маску для подовых сетей
Напомню, что IP-пулы, описываются объектами типа IPPools, посмотрим какие у нас есть объекты:
user@K-Master:~$ kubectl get ippools
NAME AGE
default-ipv4-ippool 9d
Какой-то дефолтный, а поподробнее (фильтруем по секции spec - там суть):
user@K-Master:~$ kubectl get ippools -o json | jq '.items[0].spec'
{
"allowedUses": [
"Workload",
"Tunnel"
],
"blockSize": 26,
"cidr": "10.66.0.0/16",
"ipipMode": "Always",
"natOutgoing": true,
"nodeSelector": "all()",
"vxlanMode": "Never"
}
Ага, ну собственно на этом этапе нам надо создать новый IPPool, поменять маску в нём и как-то сказать нодам, чтобы использовался новый пул. Создам такой yaml-ик:
apiVersion: projectcalico.org/v3
kind: IPPool
metadata:
name: new-ippool-24
spec:
cidr: 10.66.0.0/16
blockSize: 24 # Тут меняем блок сайз
natOutgoing: true # Пока оставляем NAT
ipipMode: Always # Пока оставляем туннели
nodeSelector: all()
Это, кстати, кажется наш первый МАНИФЕСТ - то есть торжественный акт кубернетес админа, оповещающий ноды об издании законов чрезвычайной важности или об особо важных событиях в кубернетесе (https://kubernetes.io/docs/concepts/workloads/management/)
Применим манифест вот такой командой, обогатив вывод диагностикой:
kubectl apply -f NewWonderfulIPPool -v=6
user@K-Master:~$ kubectl apply -f NewWonderfulIPPool -v=6
I0811 05:12:04.944183 517662 loader.go:395] Config loaded from file: /home/user/.kube/config
I0811 05:12:05.000107 517662 round_trippers.go:553] GET https://10.0.11.1:6443/openapi/v2?timeout=32s 200 OK in 54 milliseconds
I0811 05:12:05.086473 517662 round_trippers.go:553] GET https://10.0.11.1:6443/openapi/v3?timeout=32s 200 OK in 2 milliseconds
I0811 05:12:05.092453 517662 round_trippers.go:553] GET https://10.0.11.1:6443/openapi/v3/apis/crd.projectcalico.org/v1?hash=F43628490716A6E186AC29F7729CFA9CA0B163FD10DE74A6B6E2BF9F05F3E2B12896E1B7EE2B009151FC11D1C697F0DB73483BF1AC7DD8C1E097532557302D58&timeout=32s 200 OK in 4 milliseconds
I0811 05:12:05.124300 517662 round_trippers.go:553] GET https://10.0.11.1:6443/apis/crd.projectcalico.org/v1/ippools/new-wonderful-pool 200 OK in 4 milliseconds
ippool.crd.projectcalico.org/new-wonderful-pool unchanged
I0811 05:12:05.128478 517662 apply.go:535] Running apply post-processor function
Ок, как будто бы всё хорошо, наш пул появился в списке пулов:
user@K-Master:~$ kubectl get ippools
NAME AGE
default-ipv4-ippool 9d
new-wonderful-pool 31s
Но распределение блоков не изменилось, блоки старые:
user@K-Master:~$ kubectl get blockaffinities -o json | jq '.items[].spec'
{
"cidr": "10.66.73.128/26",
"deleted": "false",
"node": "k-master",
"state": "confirmed"
}
{
"cidr": "10.66.207.64/26",
"deleted": "false",
"node": "k-w1",
"state": "confirmed"
}
{
"cidr": "10.66.53.192/26",
"deleted": "false",
"node": "k-w2",
"state": "confirmed"
}
{
"cidr": "10.66.122.192/26",
"deleted": "false",
"node": "k-w3",
"state": "confirmed"
}
Ну в целом куб тут руководствуется простым прицнипом - “Работает - не трогай!”, зачем что-то менять если оно и так работает? Адреса всё те же, пинги ходят:
user@k-w1:~$ kubectl get pods -A \
-o custom-columns="NAMESPACE:.metadata.namespace,NAME:.metadata.name,IP:.status.podIP" \
--no-headers \
| grep -E "test-pod|calico-kube"
default test-pod 10.66.122.194
kube-system calico-kube-controllers-658d97c59c-kq2ld 10.66.207.66
По совету из гугла мне надо в спецификации старого пула добавить ключ-значение Дизаблед = True
, вот так: kubectl patch ippool default-ipv4-ippool --type='merge' -p '{"spec":{"disabled":true}}'
После того как пул будет “пропатчен” по логике он более не должен использоваться
Ну давайте убьём под test-pod, созадим его заного и посмотрим что получится:
user@k-w1:~$ kubectl delete pod test-pod
pod "test-pod" deleted
user@k-w1:~$ kubectl run test-pod --image=alpine --restart=Never --rm -it -- ip a | grep inet
inet 127.0.0.1/8 scope host lo
inet6 ::1/128 scope host
inet 10.66.53.196/32 scope global eth0
inet6 fe80::4490:81ff:fecd:4c55/64 scope link
Пока выдал IP из старого блока Ок, поступим кардинально и грохнем наш калеко-контроллер:
# гд он там
user@K-Master:~$ kubectl get pods -n kube-system | grep calico
calico-kube-controllers-658d97c59c-kq2ld 1/1 Running 0 10d
calico-node-b2rgf 1/1 Running 0 10d
calico-node-lfbfg 1/1 Running 0 10d
calico-node-ncqcp 1/1 Running 0 10d
calico-node-wpw78 1/1 Running 0 10d
user@K-Master:~$
user@K-Master:~$
user@K-Master:~$ kubectl delete pod calico-kube-controllers-658d97c59c-kq2ld
Error from server (NotFound): pods "calico-kube-controllers-658d97c59c-kq2ld" not found
user@K-Master:~$ kubectl delete pod calico-kube-controllers-658d97c59c-kq2ld -n kube-system
pod "calico-kube-controllers-658d97c59c-kq2ld" deleted
Что удивительно, после смерти он сразу ожил:
user@K-Master:~$ kubectl get pods -n kube-system | grep calico-kube-controllers
calico-kube-controllers-658d97c59c-pk8wx 1/1 Running 0 29s
На самом деле нихуя удивительного, ведь наш контроллер это не просто под, это целый Deployment:
user@K-Master:~$ kubectl get deployments -n kube-system
NAME READY UP-TO-DATE AVAILABLE AGE
calico-kube-controllers 1/1 1 1 10d
coredns 2/2 2 2 13d
Видите, их тут два таких - шото про DNS, и наш контроллер. Если оооочень кратко, деплоймент - это некая абстракция над подами, которая управляет их жизненым циклом… Эм… вернее деплоймент - это абстрация над ReplicaSet, которая является абстракцией над подами. Я говорю кубу - хочу чтобы моё приложение жило в виде 10-ти экземпляров (ведь одно из преимущество кубера - это как раз масштабируемость) - и ReplicaSet это делает. А деплоймент более интеллектуально управляет репликасетом (гармонично управляет всем жизненным циклом релиза приложения). Если вы владелец Ко-Ко пиццы и делаете пиццу маргариту, вам нужны пекари-поды - поток клиентов бесконечный, маргарит нужно делать много, поэтому пекарей у вас 10. Нужен бригадир-пекарь (replica set), чтобы следить - если один пекарь сгорел в печи, а другой утонул в томатной пасте - надо достать новых двух пекарей из жёлтого автобуса. Тут врывается сумасшедший взъеорошенный шеф-повар со своей очередной гениальной идеей и говорит - “Теперь делаем пеперонни! Вот рецепт” и убегает во тьму. Всё производство надо переделать на Пеперонни - но останавливать приготовление маргариты вы не можете - клиенты ждут, да и пекари пока не научились делать пеперони, катиться тут надо постепенно - сначала один пекарь начинает печь пеперонни, потом второй и так далее (rolling update). В какой-то момент посетители пицеррии прочухали что в рецепт пеперони шеф-повар добавил буквально говно и начали жаловаться - надо откатываться обратно на маргариту (rollback) - всем этим процессом как раз и управляем Deployment. Так что там с нашей пиццей калекой-контроллером? Вот так описывается его Deployment:
user@K-Master:~$ kubectl describe deployment calico-kube-controllers -n kube-system
Name: calico-kube-controllers
Namespace: kube-system
CreationTimestamp: Fri, 01 Aug 2025 10:57:23 +0000
Labels: k8s-app=calico-kube-controllers
Annotations: deployment.kubernetes.io/revision: 1
Selector: k8s-app=calico-kube-controllers
Replicas: 1 desired | 1 updated | 1 total | 1 available | 0 unavailable
StrategyType: Recreate
MinReadySeconds: 0
Pod Template:
Labels: k8s-app=calico-kube-controllers
Service Account: calico-kube-controllers
Containers:
calico-kube-controllers:
Image: docker.io/calico/kube-controllers:v3.25.0
Port: <none>
Host Port: <none>
Liveness: exec [/usr/bin/check-status -l] delay=10s timeout=10s period=10s #success=1 #failure=6
Readiness: exec [/usr/bin/check-status -r] delay=0s timeout=1s period=10s #success=1 #failure=3
Environment:
ENABLED_CONTROLLERS: node
DATASTORE_TYPE: kubernetes
Mounts: <none>
Volumes: <none>
Priority Class Name: system-cluster-critical
Conditions:
Type Status Reason
---- ------ ------
Progressing True NewReplicaSetAvailable
Available True MinimumReplicasAvailable
OldReplicaSets: <none>
NewReplicaSet: calico-kube-controllers-658d97c59c (1/1 replicas created)
Events: <none>
В общем, важно помнить, что как бы мы не старались убить поды, кубернетес будет всегда их восстанавливать до количества равному значения replicas
Ок, контроллер мы переустановили, попробуем ещё раз пересоздать под:
user@k-w1:~$ kubectl run test-pod --image=alpine --restart=Never --rm -it -- ip a | grep inet
inet 127.0.0.1/8 scope host lo
inet6 ::1/128 scope host
inet 10.66.122.197/32 scope global eth0
inet6 fe80::487c:aaff:fe2b:3874/64 scope link
Всё по старенькому, kubectl get blockaffinities -o json | jq '.items[].spec'
выдаёт всё теже пулы. даже сам контроллер живёт в старом пуле до сих пор:
user@K-Master:~$ kubectl get pod -n kube-system -o wide | grep calico-kube-controllers
calico-kube-controllers-658d97c59c-pk8wx 1/1 Running 0 73m 10.66.53.197 k-w2 <none> <none>
Хм… Ну ок, попробую удалить старый пул вообще
new-wonderful-pool 24h
user@k-w1:~$ kubectl delete ippool default-ipv4-ippool
ippool.crd.projectcalico.org "default-ipv4-ippool" deleted
user@k-w1:~$
user@k-w1:~$ kubectl get ippool
NAME AGE
new-wonderful-pool 24h
Не помогает, блоки все те же:
user@k-w1:~$ kubectl get blockaffinities -o json | jq '.items[].spec'
{
"cidr": "10.66.73.128/26",
"deleted": "false",
"node": "k-master",
"state": "confirmed"
}
{
"cidr": "10.66.207.64/26",
"deleted": "false",
"node": "k-w1",
"state": "confirmed"
}
{
"cidr": "10.66.53.192/26",
"deleted": "false",
"node": "k-w2",
"state": "confirmed"
}
{
"cidr": "10.66.122.192/26",
"deleted": "false",
"node": "k-w3",
"state": "confirmed"
Но вот у блоков явно есть пометка, что они не удалены - "deleted": "false",
и возможно их надо просто удалить? Ну ок:
user@k-w1:~$ kubectl delete blockaffinities --all -v=6
blockaffinity.crd.projectcalico.org "k-master-10-66-73-128-26" deleted
blockaffinity.crd.projectcalico.org "k-w1-10-66-207-64-26" deleted
blockaffinity.crd.projectcalico.org "k-w2-10-66-53-192-26" deleted
blockaffinity.crd.projectcalico.org "k-w3-10-66-122-192-26" deleted
Теперь я хочу сделать тест - проверить, осталась ли связанность между подами, пытаюсь посмотреть какие на подах адреса, но вижу что контроллер CNI у меня ушёл куда-то погулять и закрашлупился
user@k-w1:~$ kubectl get pods -o wide | grep calico-kube-controllers
NAMESPACE NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
kube-system calico-kube-controllers-658d97c59c-kxtqc 0/1 CrashLoopBackOff 325 (53s ago) 21h 10.66.53.202 k-w2 <none> <none>
В логах особо ничего понятного нет - просто вижно что контроллер не может связаться с kubeapi, правда не понятно откуда он берёт такой IP для кубе-апи,но дело его
user@k-w1:~$ kubectl logs -n kube-system -p calico-kube-controllers-658d97c59c-kxtqc
2025-08-13 03:52:49.481 [INFO][1] main.go 107: Loaded configuration from environment config=&config.Config{LogLevel:"info", WorkloadEndpointWorkers:1, ProfileWorkers:1, PolicyWorkers:1, NodeWorkers:1, Kubeconfig:"", DatastoreType:"kubernetes"}
W0813 03:52:49.483540 1 client_config.go:617] Neither --kubeconfig nor --master was specified. Using the inClusterConfig. This might not work.
2025-08-13 03:52:49.483 [INFO][1] main.go 131: Ensuring Calico datastore is initialized
2025-08-13 03:53:19.485 [ERROR][1] client.go 290: Error getting cluster information config ClusterInformation="default" error=Get "https://10.96.0.1:443/apis/crd.projectcalico.org/v1/clusterinformations/default": dial tcp 10.96.0.1:443: i/o timeout
2025-08-13 03:53:19.485 [INFO][1] main.go 138: Failed to initialize datastore error=Get "https://10.96.0.1:443/apis/crd.projectcalico.org/v1/clusterinformations/default": dial tcp 10.96.0.1:443: i/o timeout
2025-08-13 03:53:49.509 [ERROR][1] client.go 290: Error getting cluster information config ClusterInformation="default" error=Get "https://10.96.0.1:443/apis/crd.projectcalico.org/v1/clusterinformations/default": context deadline exceeded
2025-08-13 03:53:49.509 [INFO][1] main.go 138: Failed to initialize datastore error=Get "https://10.96.0.1:443/apis/crd.projectcalico.org/v1/clusterinformations/default": context deadline exceeded
2025-08-13 03:53:49.510 [FATAL][1] main.go 151: Failed to initialize Calico datastore
В общем, тут кажется очевидным, что надо грохнуть контроллер в очередной раз и посмотреть, что из этого получится:
user@k-w1:~$ kubectl delete pod calico-kube-controllers-658d97c59c-kxtqc -n kube-system
pod "calico-kube-controllers-658d97c59c-kxtqc" delete
Получилось? Всё работает теперь? Кажется, что да, под жив:
user@k-w1:~$ kubectl get pods -n kube-system
NAME READY STATUS RESTARTS AGE
calico-kube-controllers-658d97c59c-sjk2n 0/1 Running 0 23s
А на самом деле - хуй, потому что 0/1
в столбце READY
. а через некоторое время видим что под-рестартится:
NAME READY STATUS RESTARTS AGE
calico-kube-controllers-658d97c59c-sjk2n 0/1 Running 7 (5m34s ago) 15m
Если заглянуть в описание подика командой kubectl describe pod calico-kube-controllers-658d97c59c-sjk2n -n kube-system
можно увидеть всякие события, например такие:
Events:
Type Reason Age From Message
Normal Scheduled 14m default-scheduler Successfully assigned kube-system/calico-kube-controllers-658d97c59c-sjk2n to k-w3
Warning Unhealthy 13m (x3 over 13m) kubelet Readiness probe failed: Error initializing datastore: Get "https://10.96.0.1:443/apis/crd.projectcalico.org/v1/clusterinformations/default": dial tcp 10.96.0.1:443: i/o timeout
Warning Unhealthy 13m (x3 over 13m) kubelet Liveness probe failed: Error initializing datastore: Get "https://10.96.0.1:443/apis/crd.projectcalico.org/v1/clusterinformations/default": dial tcp 10.96.0.1:443: i/o timeout
Normal Pulled 13m (x2 over 14m) kubelet Container image "docker.io/calico/kube-controllers:v3.25.0" already present on machine
Normal Created 13m (x2 over 14m) kubelet Created container calico-kube-controllers
Normal Started 13m (x2 over 14m) kubelet Started container calico-kube-controllers
Warning Unhealthy 13m (x9 over 14m) kubelet Readiness probe failed: initialized to false
Warning Unhealthy 12m (x3 over 13m) kubelet Liveness probe failed: initialized to false
Normal Killing 12m kubelet Container calico-kube-controllers failed liveness probe, will be restarted
Warning BackOff 4m13s (x24 over 9m55s) kubelet Back-off restarting failed container calico-kube-controllers in pod calico-kube-controllers-658d97c59c-sjk2n_kube-system(fdf19539-da56-4c81-83bb-849d6da081eb
Ну то есть под рестартится потому что не проходит проба на живость контейнера (по-русски - liveness probe). Проба на живость - это признак того что некие условия для начала работы пода выполнены и под может переходить к следующей проверке - проверке на готовность принимать трафик (readiness probe). В describe нашего пода есть такое ещё:
Liveness: exec [/usr/bin/check-status -l] delay=10s timeout=10s period=10s #success=1 #failure=6
Readiness: exec [/usr/bin/check-status -r] delay=0s timeout=1s period=10s #success=1 #failure=3
То есть для того, что б куб понял что с подом всё ок - команда /usr/bin/check-status -l
должна выполниться без ошибок (подозреваю что должна вернуть код 0), но кажется, выполняется с ошибкой и из логов понятно, что не может выполнить запросик Get "
https://10.96.0.1:443/apis/crd.projectcalico.org/v1/clusterinformations/default
"
Что ж это за адрес-то такой всё-таки? Из доков самого куба процитирую и разойдёмся на этом - The default Service, in this case, uses the ClusterIP 10.96.0.1
, а что за default Service? - The well-known kubernetes Service, that exposes the kube-apiserver endpoint to the Pods
. В общем если к сути, то 10.96.0.1 - это адрес, на который ходят все подики для общения с нашим мозгом всего куба - kube-api, ClusterIP - потому что IP шаренный на весь кластер, мастеров может быть много в кластере и все готовы будут принять трафик на этот IP. Как именно под отправляет трафик на этот адрес дело очень мутное и если начну туда закапываться, то ещё на пару десятков лет надо откладывать выпуск этого чудесного материала. Попробуем разобраться позже (нет), в целом сейчас понятно что для того, чтобы нам запустить контроллер отвечающий за сеть - нам нужна рабочая сеть, работу которой обеспечивает контроллер. Но точно ли нам нужен контроллер для того, чтобы работал dataplane? Что там с самими сетевыми нодами? А там тоже чёто ничего хорошего:
user@K-Master:~$ kubectl get pods -n kube-system -o wide | grep calico
calico-kube-controllers-658d97c59c-jz69l 0/1 CrashLoopBackOff 16 (70s ago) 49m 10.66.217.0 k-w2 <none> <none>
calico-node-5k5bk 0/1 Running 0 46h 10.1.11.1 k-w1 <none> <none>
calico-node-cr77p 0/1 Running 0 46h 10.2.22.1 k-w2 <none> <none>
calico-node-mvrbb 0/1 Running 0 46h 10.0.11.1 k-master <none> <none>
calico-node-w9m5m 0/1 Running 0 46h 10.3.33.1 k-w3 <none> <none>
Никто ни к чему не готов :( В описании подика видно, что проверка на готовность не проходит:
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Warning Unhealthy 4m6s (x18714 over 46h) kubelet (combined from similar events): Readiness probe failed: 2025-08-14 04:29:04.978 [INFO][2834665] confd/health.go 180: Number of node(s) with BGP peering established = 3
calico/node is not ready: felix is not ready: readiness probe reporting 503
Как-будто что-то с BGP, но не очевидно. В общем, грохнем их всех от греха подальше, и они восстановятся - так как управляются ДемонСетом - это такой вид распределения нагрузки, который должен быть запущен в одном экземпяре на каждой ноде. И куб следит за этим. Вот так мы их и убьём, просто выберем все поды у которых есть меточка, говорящая о том что они - калеко-ноды:
user@K-Master:~$ kubectl delete pods -n kube-system -l k8s-app=calico-node
pod "calico-node-5k5bk" deleted
pod "calico-node-cr77p" deleted
pod "calico-node-mvrbb" deleted
pod "calico-node-w9m5m" deleted
TLDR - это тоже не помогло. Ноды восстановились, но связаности особо не появилось :( Тут я психанул и решил поехать на работу - как-то слишком много эмоциональных сил я трачу на, казалось бы, простую задачу - сменить маску для подовых CIDR-ов. Пока ехал у меня появилась следующая логическая цепочка - сеть не работает, потому что не работает толком контроллер; контроллер не работает, потому что не может связаться с Kube API; не может связаться, потому что калеко-ноды не ready совсем. Может быть стоит попробовать разместить сам контроллер в непосредственной близости к Kube API - прямо на мастере - быть может находясь там ему не нужно будет строить никакие оверлеи и через какие-то внутренние механизмы он сможет достучаться до kube api и рассказать всем остальным как жить? Через пару дней, вернувшись к написанию статьи, я обнаружил свою лабу не рабочей по причине того, что на PNETLAB место на виртуальном hdd было выжрано в ноль - файлы этой лабы в папке /opt/unetlab/tmp заняли аж 70G какого-то хуя, остановив всю работу. В общем, ноды пришлось погасить чтобы подчистить место. Ну и вы сами знаете что произошло дальше… Помните этого чувака?

Вероятно Вселенная услышала моё подсознательное желание не разбираться во всём этом калеко-дерьме и выжрала всё место на жёстком диске, вынудив меня перезапустить лабу. После перезапуска нод, контроллер чудесным образом оживает и не крашится
user@K-Master:~$ kubectl get pods -n kube-system | grep calico-kube-controllers
calico-kube-controllers-658d97c59c-pfstc 1/1 Running 427 (4d18h ago) 5d23h
Блоки нарезались правильным образом:
user@K-Master:~$ kubectl get blockaffinities -o json | jq '.items[].spec'
{
"cidr": "10.66.111.0/24",
"deleted": "false",
"node": "k-master",
"state": "confirmed"
}
{
"cidr": "10.66.95.0/24",
"deleted": "false",
"node": "k-w1",
"state": "confirmed"
}
{
"cidr": "10.66.217.0/24",
"deleted": "false",
"node": "k-w2",
"state": "confirmed"
}
{
"cidr": "10.66.52.0/24",
"deleted": "false",
"node": "k-w3",
"state": "confirmed"
}
И даже сам контроллер получил IP из правильного блока:
user@K-Master:~$ kubectl get pods -n kube-system -o wide | grep calico-kube-controllers
calico-kube-controllers-658d97c59c-pfstc 1/1 Running 427 (4d18h ago) 5d23h 10.66.52.3 k-w3 <none> <none>
На каждой ноде есть вот такой набор маршрутов, полученных через bird:
user@k-w3:~$ ip r | grep bird
blackhole 10.66.52.0/24 proto bird
10.66.95.0/24 via 10.0.137.15 dev ens5 proto bird
10.66.111.0/24 via 10.0.137.12 dev ens5 proto bird
10.66.217.0/24 via 10.0.137.112 dev ens5 proto bird
И если зайти внутрь какого-нибудь bird-а, то видно что сессии установлены и маршруты получены:
user@k-w3:~$ kubectl exec -it calico-node-9gl6c -n kube-system -- sh
Defaulted container "calico-node" out of: calico-node, upgrade-ipam (init), install-cni (init), mount-bpffs (init)
sh-4.4# birdcl show protocols | grep BGP
Mesh_10_0_137_12 BGP master up 03:52:40 Established
Mesh_10_0_137_15 BGP master up 03:52:40 Established
Mesh_10_0_137_112 BGP master up 03:52:40 Established
sh-4.4# birdcl show route
BIRD v0.3.3+birdv1.6.8 ready.
10.0.0.0/8 via 10.3.33.0 on ens3 [kernel1 03:52:39] * (10)
10.3.33.0/31 dev ens3 [direct1 03:52:38] * (240)
10.66.52.0/24 blackhole [static1 03:52:38] * (200)
10.66.52.3/32 dev cali718083fb40c [kernel1 03:52:40] * (10)
10.66.95.0/24 via 10.0.137.15 on ens5 [Mesh_10_0_137_15 03:52:40] * (100/0) [i]
10.66.111.0/24 via 10.0.137.12 on ens5 [Mesh_10_0_137_12 03:52:40] * (100/0) [i]
10.0.137.0/24 dev ens5 [direct1 03:52:38] * (240)
10.66.217.0/24 via 10.0.137.112 on ens5 [Mesh_10_0_137_112 03:52:40] * (100/0) [i]
Кажется, с контрол-плейном всё ок, работает ли датаплейн? Могут ли поды на разных ноды общаться друг с дружкой? Запустим alpine:
user@K-Master:~$ kubectl run test-pod --image=alpine --restart=Never --rm -it -- sh
If you don't see a command prompt, try pressing enter.
/ # ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
inet6 ::1/128 scope host
valid_lft forever preferred_lft forever
3: eth0@if5: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue state UP
link/ether b6:21:46:c9:bf:a2 brd ff:ff:ff:ff:ff:ff
inet 10.66.217.2/32 scope global eth0
valid_lft forever preferred_lft forever
inet6 fe80::b421:46ff:fec9:bfa2/64 scope link
valid_lft forever preferred_lft forever
Судя по IP видно, что это - воркер два, но можно ещё так убедится:
user@k-w1:~$ kubectl get pods -n kube-system -o wide | grep test-pod
test-pod 1/1 Running 0 2m5s 10.66.217.2 k-w2 <none> <none>
Ну и можно попробовать попингать тот же контроллер на воркере третьем - 10.66.52.3:
/ # ping 10.66.52.3
PING 10.66.52.3 (10.66.52.3): 56 data bytes
64 bytes from 10.66.52.3: seq=0 ttl=62 time=1.224 ms
64 bytes from 10.66.52.3: seq=1 ttl=62 time=0.410 ms
64 bytes from 10.66.52.3: seq=2 ttl=62 time=0.420 ms
Ну ок, со скрипом, но кажется, что задача “поменять маску для подовых сетей” решена. Подозреваю, что для реализации следующего этапа моего плана место на диске опять будет должно закончится.
Пирим с underlay, делаем плоскую сеть без overlay
В целом, я вообще не понимаю и не знаю что тут делать в деталях со стороны куба :( Друзей с calico у меня нет, а официальная документация не открывается с моего компьютера:

В общем, как в известном меме-кеке - нажимаешь - а там ХУЙ нарисован, даром что у меня не Корбина Телеком.
Будем, в общем, общаться с нейросетямя тогда! Попробовал дипсик, gpt-4, claude 4 и grok 4. Самым адекватным мне показался Claude - он накидал мне базовый план, попросил дополнительную диагностику и задал уточняющие вопросы. Мы с ним распланировали номера AS-ок и составили подробный план что делать. Не буду показывать мою с ним переписку (это личное), но просто постараюсь делать всё так, как мы с ним договорились. Картинка в плане распределения AS-ок у меня получилась такой:

Но сначала отселим контроллер
Перед ответственными работами, меня интуитвно не покидала мысль переместить calico-controller на мастер, чтобы в случае потери связанности он там как-то сам разобрался со своими проблемками.Claude подтвердил, что это хорошая идея, но вероятно он, как и все нейросети просто мне льстит. Сделать это предлагается путём добавления в спецификацию моего деплоймента, так называемого nodeSelector-а в целом из названия понятно что он делает - выбирает ноду )
Предлагается добавить такой патчик:
"spec": {
"template": {
"spec": {
"nodeSelector": {
"kubernetes.io/os": "linux",
"node-role.kubernetes.io/control-plane": ""
},
"tolerations": [
{
"key": "CriticalAddonsOnly",
"operator": "Exists"
},
{
"key": "node-role.kubernetes.io/control-plane",
"operator": "Exists",
"effect": "NoSchedule"
},
{
"key": "node-role.kubernetes.io/master",
"operator": "Exists",
"effect": "NoSchedule"
}
]
}
}
}
}'
Ну ещё немного пояснений, мы не только добавляем раздел nodeSelector
, указывая что мы явно хотим разместить под на мастере - "
node-role.kubernetes.io/control-plane
"
, но ещё и указываем некие толерейшны. Что это? Это просто концепция запрета на размещение нагрузки на каких-то нодах (taintы) и ВНЕЗАПНО обхода этих запретов (tolerations). Подробно можно почитать туть - https://kubernetes.io/docs/concepts/scheduling-eviction/taint-and-toleration/ Зачем нам это нужно? Потому что по дефолту все мастера куба живут с таким тейнтом:
user@K-Master:~$ kubectl describe node k-master | grep Taints -A 3
Taints: node-role.kubernetes.io/control-plane:NoSchedule
Что значит “Не надо мне тут ничего размещать вообще!!1”, этот тейнт мы и обходим вот такими конструкциями:
{
"key": "node-role.kubernetes.io/control-plane",
"operator": "Exists",
"effect": "NoSchedule"
}
Что значит - “Если контрол-плейн нода тебе говорит “Не надо мне тут ничего размещать вообще!!1” не обращай на это внимание”. А это нам и надо!
Итого, пробуем через kubectl patch deployment calico-kube-controllers -n kube-system -p [Тут текст патча]
:
Было так:
user@K-Master:~$ kubectl get pods -n kube-system -l k8s-app=calico-kube-controllers -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
calico-kube-controllers-658d97c59c-pfstc 1/1 Running 427 (5d18h ago) 6d23h 10.66.52.3 k-w3 <none> <none>
Стало так:
user@K-Master:~$ kubectl get pods -n kube-system -l k8s-app=calico-kube-controllers -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
calico-kube-controllers-6c879b76df-f5f44 1/1 Running 0 2m40s 10.66.111.1 k-master <none> <none>
Контроллер переехал, IP сменился, связь осталась из рандомного созданного пода:
user@K-Master:~$ kubectl run test-pod --image=alpine --restart=Never --rm -it -- ip -a & ping 10.66.111.1
[1] 1828972
PING 10.66.111.1 (10.66.111.1) 56(84) bytes of data.
64 bytes from 10.66.111.1: icmp_seq=1 ttl=64 time=1.59 ms
64 bytes from 10.66.111.1: icmp_seq=2 ttl=64 time=0.030 ms
64 bytes from 10.66.111.1: icmp_seq=3 ttl=64 time=0.047 ms
Ок, погнали дальше. Сначала базово настрою BGP на коммутаторах в сторону нод и сделаю редистрибьют в OSFP процесс, чтобы ушло на спайны:
#=== Leaf-1 ===*
router bgp 65011
neighbor 10.0.11.1 remote-as 65000
neighbor 10.1.11.1 remote-as 65001
!
address-family ipv4
neighbor 10.0.11.1 activate
neighbor 10.1.11.1 activate
router ospf 1
redistribute bgp
#=== Leaf-2 ===*
router bgp 65012
neighbor 10.2.22.1 remote-as 65002
!
address-family ipv4
neighbor 10.2.22.1 activate
router ospf 1
redistribute bgp
#=== Leaf-3 ===*
router bgp 65013
neighbor 10.3.33.1 remote-as 65003
!
address-family ipv4
neighbor 10.3.33.1 activate
router ospf 1
redistribute bgp
Вот наш план на дальнейшие действия:
Создадим глобальную конфигурацию BGP
Поправим calico-объекты типа node
Создадим точечные инструкции для каждой ноды как и с кем пириться.
Разберём дефолтный full-mesh и отключим туннели.
???
PROFIT!
Глобальная конфигурация BGP
Сейчас по-факту такой сущности нет. Вот такая команда ничего не показывает:
user@K-Master:~$ calicoctl get bgpconfig
NAME LOGSEVERITY MESHENABLED ASNUMBER
user@K-Master:~$
При этом сами BGP установлены - каждая нода с каждой:
user@K-Master:~$ sudo calicoctl node status
IPv4 BGP status
+--------------+-------------------+-------+------------+-------------+
| PEER ADDRESS | PEER TYPE | STATE | SINCE | INFO |
+--------------+-------------------+-------+------------+-------------+
| 10.0.137.15 | node-to-node mesh | up | 2025-08-20 | Established |
| 10.0.137.112 | node-to-node mesh | up | 2025-08-20 | Established |
| 10.0.137.142 | node-to-node mesh | up | 2025-08-20 | Established |
+--------------+-------------------+-------+------------+-------------+
Это некое дефолтное поведение Calico - при запуске происходит магическое автообнаружение соседей, которые пирятся друг с другом по iBGP в дефолтной AS-ке 64512. Я создам новый объект типа bgpconfig
через yaml вот с таким содержимым:
apiVersion: projectcalico.org/v3
kind: BGPConfiguration
metadata:
name: default
spec:
logSeverityScreen: Info
nodeToNodeMeshEnabled: true # Пока оставляем mesh включенным
asNumber: 65000 # Дефолтный AS, но каждая нода будет иметь свой
serviceClusterIPs:
- cidr: 10.96.0.0/12 # Стандартный CIDR для сервисов k8s
user@K-Master:~$ calicoctl apply -f bgp-config.yaml
Successfully applied 1 'BGPConfiguration' resource(s)
Теперь вывод команды показывает, что есть у меня некая конфигурация BGP:
user@K-Master:~$ calicoctl get bgpconfig
NAME LOGSEVERITY MESHENABLED ASNUMBER
default Info true 65000
На этом этапе ничего сломаться не должно - сессии остаются в up.
Правка node-объектов Calico
А вот тут мне кажется что-то сломается. Нейросеть предлагает патчить текущие объекты, добавляю в spec-у вот такое (на приамере master-а):
apiVersion: projectcalico.org/v3
kind: Node
metadata:
name: k-master
spec:
bgp:
asNumber: 65000
ipv4Address: 10.0.11.1/31
Сейчас, если текущее состояние посмотреть объекта, то там так:
user@K-Master:~$ calicoctl get node k-master -o yaml | grep spec -A 100
spec:
addresses:
- address: 10.0.137.12/24
type: CalicoNodeIP
- address: 10.0.11.1
type: InternalIP
bgp:
ipv4Address: 10.0.137.12/24
ipv4IPIPTunnelAddr: 10.66.111.0
orchRefs:
- nodeName: k-master
orchestrator: k8s
status:
podCIDRs:
- 10.66.0.0/24
То есть здесь мы меняем ipv4Address: 10.0.137.12
на ipv4Address: 10.0.11.1/31
. Напомню, что calico изначально меня не спрашивал и выбрал удобные ему адреса для построения BGP. В общем, имхо всё должно развалиться. Ну вот и проверим. Аплаем предложенный конфиг для k-master-а
user@K-Master:~$ calicoctl apply -f node-k-master-bgp.yaml
Successfully applied 1 'Node' resource(s)
Что произошло? Ну во-первых у меня изменилась ожидаемо спецификация ноды:
user@K-Master:~$ calicoctl get node k-master -o yaml | grep spec -A 100
spec:
addresses:
- address: 10.0.11.1/31
type: CalicoNodeIP
- address: 10.0.11.1
type: InternalIP
bgp:
asNumber: 65000
ipv4Address: 10.0.11.1/31
orchRefs:
- nodeName: k-master
orchestrator: k8s
Тут стоит заметить что поменялся не только параметр ipv4Address
в секции BGP, но ещё и CalicoNodeIP
address - он тоже сменился на 10.0.11.1
. Ну ок, calico виднее.
Связь, ожидаемо для меня сломалась. На тестовом поде контроллер не доступен:
user@k-w2:~$ kubectl run test-pod1 --image=alpine --restart=Never --rm -it -- ip a && ping 10.66.111.1
3: eth0@if15: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue state UP
link/ether 02:a9:74:b1:03:bd brd ff:ff:ff:ff:ff:ff
inet 10.66.95.11/32 scope global eth0
valid_lft forever preferred_lft forever
inet6 fe80::a9:74ff:feb1:3bd/64 scope link tentative
valid_lft forever preferred_lft forever
pod "test-pod1" deleted
PING 10.66.111.1 (10.66.111.1) 56(84) bytes of data.
From 10.2.22.0 icmp_seq=1 Destination Net Unreachable
From 10.2.22.0 icmp_seq=2 Destination Net Unreachable
From 10.2.22.0 icmp_seq=3 Destination Net Unreachable
From 10.2.22.0 icmp_seq=4 Destination Net Unreachable
При этом BGP по-прежнему в UP!
user@K-Master:~$ sudo calicoctl node status
Calico process is running.
IPv4 BGP status
+--------------+-------------------+-------+----------+-------------+
| PEER ADDRESS | PEER TYPE | STATE | SINCE | INFO |
+--------------+-------------------+-------+----------+-------------+
| 10.0.137.15 | node-to-node mesh | up | 04:30:59 | Established |
| 10.0.137.112 | node-to-node mesh | up | 04:30:59 | Established |
| 10.0.137.142 | node-to-node mesh | up | 04:30:59 | Established |
+--------------+-------------------+-------+----------+-------------+
А если смотреть со стороны любого воркера, то видно что bgp построился до нового IP:
user@k-w1:~$ sudo calicoctl node status
IPv4 BGP status
+--------------+-------------------+-------+----------+-------------+
| PEER ADDRESS | PEER TYPE | STATE | SINCE | INFO |
+--------------+-------------------+-------+----------+-------------+
| 10.0.137.112 | node-to-node mesh | up | 04:16:40 | Established |
| 10.0.137.142 | node-to-node mesh | up | 04:16:40 | Established |
| 10.0.11.1 | node-to-node mesh | up | 04:30:59 | Established |
+--------------+-------------------+-------+----------+-------------+
С точки зрения бёрда на воркере ситуация аналогичная, видим bgp до нового соседа
sh-4.4# birdcl show protocols | grep BGP
Mesh_10_0_137_112 BGP master up 04:16:40 Established
Mesh_10_0_137_142 BGP master up 04:16:40 Established
Mesh_10_0_11_1 BGP master up 04:30:59 Established
Смущает, конечно время жизни, но чтож. Будем считать что это bird-специфик вещь и смена IP у соседа не считается новым соседом )) Что интересно, перед тем как всё это делать я встал дампом вот на этот интерфейс:

И увидел там полноценное установление новой BGP сессии от первого воркера, например:

Обратных пакетов не видно, так как согласно роутингу пакеты до 10.0.137.15
уйдут через подкостыленный для работы Интернета интерфейс. Тут важен сам факт - калико рассказал всем остальным нодам, что у мастера поменялся IP и теперь BGP надо строить до нового адреса.
Туннели у нас развалились, связи между подами нет. Разбираться почему не очень хочу - опять уйду в исследование на пару недель, а со статьёй уже хочется покончить! В общем, в данный момент у нас сеть в несколько разобранном состоянии находится и надо поскорее её починить! Апплаем вот такие конфиги для остальных нод:
# k-w1 (AS 65001)
cat << EOF > node-k-w1-bgp.yaml
apiVersion: projectcalico.org/v3
kind: Node
metadata:
name: k-w1
spec:
bgp:
asNumber: 65001
ipv4Address: 10.1.11.1/31
EOF
# k-w2 (AS 65002)
cat << EOF > node-k-w2-bgp.yaml
apiVersion: projectcalico.org/v3
kind: Node
metadata:
name: k-w2
spec:
bgp:
asNumber: 65002
ipv4Address: 10.2.22.1/31
EOF
# k-w3 (AS 65003)
cat << EOF > node-k-w3-bgp.yaml
apiVersion: projectcalico.org/v3
kind: Node
metadata:
name: k-w3
spec:
bgp:
asNumber: 65003
ipv4Address: 10.3.33.1/31
EOF
# Применяем конфигурации
calicoctl apply -f node-k-w1-bgp.yaml
calicoctl apply -f node-k-w2-bgp.yaml
calicoctl apply -f node-k-w3-bgp.yaml
Тыц, и всё у нас построилось по новым адресам. Пока что full mesh:
user@K-Master:~$ sudo calicoctl node status
Calico process is running.
IPv4 BGP status
+--------------+-------------------+-------+----------+-------------+
| PEER ADDRESS | PEER TYPE | STATE | SINCE | INFO |
+--------------+-------------------+-------+----------+-------------+
| 10.1.11.1 | node-to-node mesh | up | 05:13:02 | Established |
| 10.2.22.1 | node-to-node mesh | up | 05:13:02 | Established |
| 10.3.33.1 | node-to-node mesh | up | 05:13:02 | Established |
+--------------+-------------------+-------+----------+-------------+
Связи по прежнему нет, в моём понимании должна появится через туннели - но Бог ей судья… Кароче, я не смог удержаться и не разобраться почему связи нет :( Если запустить тестовый под и попингать из него адрес того-же контроллера, можно увидеть такое:
user@k-w2:~$ kubectl run test-pod1 --image=alpine --restart=Never --rm -it -- ip a && ping 10.66.111.1
3: eth0@if16: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue state UP
link/ether aa:7d:90:55:12:47 brd ff:ff:ff:ff:ff:ff
inet 10.66.95.12/32 scope global eth0
valid_lft forever preferred_lft forever
inet6 fe80::a87d:90ff:fe55:1247/64 scope link tentative
valid_lft forever preferred_lft forever
pod "test-pod1" deleted
PING 10.66.111.1 (10.66.111.1) 56(84) bytes of data.
From 10.2.22.0 icmp_seq=1 Destination Net Unreachable
From 10.2.22.0 icmp_seq=2 Destination Net Unreachable
From 10.2.22.0 icmp_seq=3 Destination Net Unreachable
From 10.2.22.0 icmp_seq=1 Destination Net Unreachable
- нам свитч отвечает, что мол добраться до сети он не может! Когда мастер-нода установила BGP сесси до второго воркера, она послала ему такой Update, рассказывая о своей сети:

Типа - “если что, подруга, то для сети 10.66.111.0/24 нектс-хопом является 10.0.11.1”, что видно например в bird-е на второй ноде:
sh-4.4# birdcl show route 10.66.111.0/24 all
BIRD v0.3.3+birdv1.6.8 ready.
10.66.111.0/24 via 10.2.22.0 on ens3 [Mesh_10_0_11_1 05:13:02 from 10.0.11.1] * (100/?) [AS65000i]
Type: BGP unicast univ
BGP.origin: IGP
BGP.as_path: 65000
BGP.next_hop: 10.0.11.1
BGP.local_pref: 100
Но это ничего не значит. В какой интерфейс-то отправлять пакет надо? Рекурсивно разрешая next-hop, нода приходит к выводу что 10.66.111.0/24 via 10.2.22.0 on ens3
, и отправляя пакет коммутатору получает хуй в ответ, потому что коммутатор понятия не имеет ничего про эту сеть - ему ещё никто ничего не рассказал
Leaf-2# show ip ro 10.66.111.1
Gateway of last resort is not set
Leaf-2#
Для этого нам и нужен третий шаг - надо запирить ноды с коммутаторами, и ИМ рассказать про эту сеть, а не нодам-соседкам
BGP со свитчами
Нужно сделать калеко-объекты типа BGPPeer. Сейчас их нет:
user@K-Master:~$ calicoctl get bgppeer
NAME PEERIP NODE ASN
user@K-Master:~$
Нужно что бы были. Ну нужно, так нужно. Делаю такой ямлик:
apiVersion: projectcalico.org/v3
kind: BGPPeer
metadata:
name: k-master-to-switch
spec:
peerIP: 10.0.11.0 # свитч
asNumber: 65011 # его ASN
nodeSelector: kubernetes.io/hostname == "k-master"
Применяю, получаю:
со стороны куба:
user@K-Master:~$ calicoctl get bgppeer
NAME PEERIP NODE ASN
k-master-to-switch 10.0.11.0 kubernetes.io/hostname == "k-master" 65011
#Состояние сессий:
user@K-Master:~$ sudo calicoctl node status
IPv4 BGP status
+--------------+-------------------+-------+----------+-------------+
| PEER ADDRESS | PEER TYPE | STATE | SINCE | INFO |
+--------------+-------------------+-------+----------+-------------+
| 10.1.11.1 | node-to-node mesh | up | 05:13:02 | Established |
| 10.2.22.1 | node-to-node mesh | up | 05:13:02 | Established |
| 10.3.33.1 | node-to-node mesh | up | 05:13:02 | Established |
| 10.0.11.0 | node specific | up | 05:41:14 | Established |
+--------------+-------------------+-------+----------+-------------+
со стороны свитча сессия тоже поднялась:
Leaf-1# show ip bgp summary | inc 10.0.11.1|Nei
Neighbor Status Codes: m - Under maintenance
Neighbor V AS MsgRcvd MsgSent InQ OutQ Up/Down State PfxRcd PfxAcc
10.0.11.1 4 65000 135 550 0 0 01:41:53 Estab 32 32
32 префикса получаю, ого! Что же там:
* > 10.66.52.0/24 10.0.11.1 0 - 100 0 65000 65003 i
* 10.66.52.0/24 10.0.11.1 0 - 100 0 65000 65001 65003 i
* 10.66.52.0/24 10.0.11.1 0 - 100 0 65000 65002 65001 65003 i
* 10.66.52.0/24 10.0.11.1 0 - 100 0 65000 65001 65002 65003 i
* 10.66.52.0/24 10.0.11.1 0 - 100 0 65000 65002 65003 i
* > 10.66.95.0/24 10.0.11.1 0 - 100 0 65000 65001 i
* 10.66.95.0/24 10.0.11.1 0 - 100 0 65000 65003 65001 i
* 10.66.95.0/24 10.0.11.1 0 - 100 0 65000 65002 65001 i
* 10.66.95.0/24 10.0.11.1 0 - 100 0 65000 65003 65002 65001 i
* 10.66.95.0/24 10.0.11.1 0 - 100 0 65000 65002 65003 65001 i
* > 10.66.111.0/24 10.0.11.1 0 - 100 0 65000 i
* > 10.66.217.0/24 10.0.11.1 0 - 100 0 65000 65002 i
* 10.66.217.0/24 10.0.11.1 0 - 100 0 65000 65003 65002 i
* 10.66.217.0/24 10.0.11.1 0 - 100 0 65000 65001 65003 65002 i
* 10.66.217.0/24 10.0.11.1 0 - 100 0 65000 65003 65001 65002 i
* 10.66.217.0/24 10.0.11.1 0 - 100 0 65000 65001 65002 i
* > 10.96.0.0/12 10.0.11.1 0 - 100 0 65000 i
* 10.96.0.0/12 10.0.11.1 0 - 100 0 65000 65003 65002 i
* 10.96.0.0/12 10.0.11.1 0 - 100 0 65000 65003 65001 65002 i
* 10.96.0.0/12 10.0.11.1 0 - 100 0 65000 65002 65001 65003 i
* 10.96.0.0/12 10.0.11.1 0 - 100 0 65000 65001 65003 i
* 10.96.0.0/12 10.0.11.1 0 - 100 0 65000 65001 i
* 10.96.0.0/12 10.0.11.1 0 - 100 0 65000 65002 65003 i
* 10.96.0.0/12 10.0.11.1 0 - 100 0 65000 65002 65003 65001 i
* 10.96.0.0/12 10.0.11.1 0 - 100 0 65000 65002 65001 i
* 10.96.0.0/12 10.0.11.1 0 - 100 0 65000 65001 65002 i
* 10.96.0.0/12 10.0.11.1 0 - 100 0 65000 65001 65003 65002 i
* 10.96.0.0/12 10.0.11.1 0 - 100 0 65000 65002 i
* 10.96.0.0/12 10.0.11.1 0 - 100 0 65000 65003 i
* 10.96.0.0/12 10.0.11.1 0 - 100 0 65000 65003 65002 65001 i
* 10.96.0.0/12 10.0.11.1 0 - 100 0 65000 65001 65002 65003 i
* 10.96.0.0/12 10.0.11.1 0 - 100 0 65000 65003 65001 i
Ага, это нам аукаются существующие full-mesh сессии. Bird на мастере вместо того, чтобы выбрать какой-то один бестовый маршрут и прислать его к нам - присылает вообще всё, что они там себе в фул-меше насобирали. Из этого всего нам в момент важен только вот такой роут:
Leaf-1#show ip ro 10.66.111.0
B E 10.66.111.0/24 [200/0] via 10.0.11.1, Ethernet2
То есть подовая сеть мастера у нас оказалась маршрутизируема в Underlay. И теперь у нас должна работать вот такая связь:

Напомню, что “Client” - это просто линукс хост, который просто подключен к underlay. Пинги ходют:
user@ubuntu:~$ ping 10.66.111.1
PING 10.66.111.1 (10.66.111.1) 56(84) bytes of data.
64 bytes from 10.66.111.1: icmp_seq=1 ttl=60 time=11.6 ms
64 bytes from 10.66.111.1: icmp_seq=2 ttl=60 time=13.0 ms
64 bytes from 10.66.111.1: icmp_seq=3 ttl=60 time=15.0 ms
Трассировка бежит как должна:
user@ubuntu:~$ traceroute 10.66.111.1
traceroute to 10.66.111.1 (10.66.111.1), 30 hops max, 60 byte packets
1 10.4.44.0 (10.4.44.0) 5.134 ms 5.174 ms 7.521 ms
2 10.33.99.0 (10.33.99.0) 16.426 ms 16.529 ms 19.762 ms
3 10.11.99.1 (10.11.99.1) 25.497 ms 25.656 ms 27.368 ms
4 10.0.11.1 (10.0.11.1) 36.243 ms 36.421 ms 39.167 ms
5 10.66.111.1 (10.66.111.1) 39.321 ms 42.110 ms 42.218 ms
До кучи я ещё на мастере прямо в подовой сети размещу простенький веб сервер, ну вот так:
user@K-Master:~$ cat web-on-master
apiVersion: v1
kind: Pod
metadata:
name: test-web-master
labels:
app: test-web
spec:
nodeSelector:
kubernetes.io/hostname: k-master
tolerations:
- key: node-role.kubernetes.io/control-plane
operator: Exists
effect: NoSchedule
- key: node-role.kubernetes.io/master
operator: Exists
effect: NoSchedule
containers:
- name: web
image: nginx:alpine
ports:
- containerPort: 80
restartPolicy: Always
Апплаем манифест
user@K-Master:~$ kubectl apply -f web-on-master
pod/test-web-master created
* чекаем под:
user@K-Master:~$ kubectl get pods -o wide | grep web
test-web-master 1/1 Running 0 2m55s 10.66.111.2 k-master <none> <none>
Проверяем доступность из Client-а:
user@ubuntu:~$ curl -s http://10.66.111.2
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
html { color-scheme: light dark; }
body { width: 35em; margin: 0 auto;
font-family: Tahoma, Verdana, Arial, sans-serif; }
</style>
</head>
<body>
<h1>Welcome to nginx!</h1>
<p>If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.</p>
<p>For online documentation and support please refer to
<a href="http://nginx.org/">nginx.org</a>.<br/>
Commercial support is available at
<a href="http://nginx.com/">nginx.com</a>.</p>
<p><em>Thank you for using nginx.</em></p>
</body>
</html>
Пушка-бомба!
Доделаем пиринг на остальных нодах:
# BGP peer для k-w1
cat << EOF > bgppeer-k-w1.yaml
apiVersion: projectcalico.org/v3
kind: BGPPeer
metadata:
name: k-w1-to-switch
spec:
peerIP: 10.1.11.0
asNumber: 65011
nodeSelector: kubernetes.io/hostname == "k-w1"
EOF
# BGP peer для k-w2
cat << EOF > bgppeer-k-w2.yaml
apiVersion: projectcalico.org/v3
kind: BGPPeer
metadata:
name: k-w2-to-switch
spec:
peerIP: 10.2.22.0
asNumber: 65012
nodeSelector: kubernetes.io/hostname == "k-w2"
EOF
# BGP peer для k-w3
cat << EOF > bgppeer-k-w3.yaml
apiVersion: projectcalico.org/v3
kind: BGPPeer
metadata:
name: k-w3-to-switch
spec:
peerIP: 10.3.33.0
asNumber: 65013
nodeSelector: kubernetes.io/hostname == "k-w3"
EOF
# Применяем все BGP peers
calicoctl apply -f bgppeer-k-w1.yaml
calicoctl apply -f bgppeer-k-w2.yaml
calicoctl apply -f bgppeer-k-w3.yaml
Чекаем то все пиры есть:
user@K-Master:~$ calicoctl get bgppeer
NAME PEERIP NODE ASN
k-master-to-switch 10.0.11.0 kubernetes.io/hostname == "k-master" 65011
k-w1-to-switch 10.1.11.0 kubernetes.io/hostname == "k-w1" 65011
k-w2-to-switch 10.2.22.0 kubernetes.io/hostname == "k-w2" 65012
k-w3-to-switch 10.3.33.0 kubernetes.io/hostname == "k-w3" 65013
На свитчаех все сесси поднялись:
Leaf-1#show ip bgp su
Neighbor V AS MsgRcvd MsgSent InQ OutQ Up/Down State PfxRcd PfxAcc
10.0.11.1 4 65000 220 635 0 0 02:55:10 Estab 32 32
10.1.11.1 4 65001 22 503 0 0 00:02:56 Estab 32 32
Leaf-2#show ip bgp summary
Neighbor V AS MsgRcvd MsgSent InQ OutQ Up/Down State PfxRcd PfxAcc
10.2.22.1 4 65002 30 497 0 0 00:05:37 Estab 39 39
Leaf-3#show ip bgp summary
Neighbor V AS MsgRcvd MsgSent InQ OutQ Up/Down State PfxRcd PfxAcc
10.3.33.1 4 65003 30 496 0 0 00:05:53 Estab 39 39
Но по прежнему прилетает куча лишнего - надо бы разобрать full-mesh
Разбираем full-mesh и отключаем тунельный режим
Патчим наш пул, отключаем там IPIP тунели:
kubectl patch ippool new-wonderful-pool --type='merge' -p='{"spec":{"ipipMode":"Never"}}'
и NAT:
kubectl patch ippool new-wonderful-pool --type='merge' -p='{"spec":{"natOutgoing":false}}'
Рестартим calico-ноды через роллаут:
user@K-Master:~$ kubectl rollout restart daemonset/calico-node -n kube-system
daemonset.apps/calico-node restarted
user@K-Master:~$ kubectl rollout status daemonset/calico-node -n kube-system
Waiting for daemon set "calico-node" rollout to finish: 1 out of 4 new pods have been updated...
Waiting for daemon set "calico-node" rollout to finish: 1 out of 4 new pods have been updated...
Waiting for daemon set "calico-node" rollout to finish: 2 out of 4 new pods have been updated...
Waiting for daemon set "calico-node" rollout to finish: 2 out of 4 new pods have been updated...
Waiting for daemon set "calico-node" rollout to finish: 3 out of 4 new pods have been updated...
Waiting for daemon set "calico-node" rollout to finish: 3 out of 4 new pods have been updated...
Waiting for daemon set "calico-node" rollout to finish: 3 of 4 updated pods are available...
daemon set "calico-node" successfully rolled out
Пока ноды рестартились я пустил пинг от клиента до 10.66.111.1
, пару пакетов потерялось:
64 bytes from 10.66.111.1: icmp_seq=54 ttl=60 time=12.2 ms
64 bytes from 10.66.111.1: icmp_seq=55 ttl=60 time=11.5 ms
64 bytes from 10.66.111.1: icmp_seq=56 ttl=60 time=13.2 ms
From 10.4.44.0 icmp_seq=57 Destination Net Unreachable
From 10.4.44.0 icmp_seq=58 Destination Net Unreachable
From 10.4.44.0 icmp_seq=59 Destination Net Unreachable
From 10.4.44.0 icmp_seq=60 Destination Net Unreachable
From 10.4.44.0 icmp_seq=61 Destination Net Unreachable
64 bytes from 10.66.111.1: icmp_seq=62 ttl=60 time=13.7 ms
64 bytes from 10.66.111.1: icmp_seq=63 ttl=60 time=14.0 ms
Ну и что бы разобрать full-mesh bgp - надо пропатчить наш BGPConfig и full mesh должен пропасть:
user@K-Master:~$ calicoctl patch bgpconfig default --patch='{"spec":{"nodeToNodeMeshEnabled":false}}'
Successfully patched 1 'BGPConfiguration' resource
user@K-Master:~$ sudo calicoctl node status
Calico process is running.
IPv4 BGP status
+--------------+---------------+-------+----------+-------------+
| PEER ADDRESS | PEER TYPE | STATE | SINCE | INFO |
+--------------+---------------+-------+----------+-------------+
| 10.0.11.0 | node specific | up | 08:50:34 | Established |
+--------------+---------------+-------+----------+-------------+
* Ну и пример ещё с одной ноды:
user@k-w1:~$ sudo calicoctl node status
Calico process is running.
IPv4 BGP status
+--------------+---------------+-------+----------+-------------+
| PEER ADDRESS | PEER TYPE | STATE | SINCE | INFO |
+--------------+---------------+-------+----------+-------------+
| 10.1.11.0 | node specific | up | 09:03:20 | Established |
+--------------+---------------+-------+----------+-------------+
А вот что теперь с точки зрения свитчей:
Leaf-1#show ip bgp su
BGP summary information for VRF default
Router identifier 10.11.99.1, local AS number 65011
Neighbor Status Codes: m - Under maintenance
Neighbor V AS MsgRcvd MsgSent InQ OutQ Up/Down State PfxRcd PfxAcc
10.0.11.1 4 65000 334 694 0 0 00:02:13 Estab 2 2
10.1.11.1 4 65001 131 560 0 0 00:02:13 Estab 2 2
Префиксов стало гораздно меньше - два. Один для подовой сети, другой для сети 10.96.0.0/12 (Cluster Services)
С точки зрения каждого лифа, у него маршруты по BGP на подовые сети каждой ноды:
Leaf-1#show ip ro bgp | inc /24
B E 10.66.95.0/24 [200/0] via 10.1.11.1, Ethernet3
B E 10.66.111.0/24 [200/0] via 10.0.11.1, Ethernet2
Leaf-2#show ip ro bgp | inc /24
B E 10.66.217.0/24 [200/0] via 10.2.22.1, Ethernet2
Leaf-3#show ip ro bgp | inc /24
B E 10.66.52.0/24 [200/0] via 10.3.33.1, Ethernet2
А это именно то, что нам нужно было!
Пару последних проверок на конец:
Курл курлиться извне:
user@ubuntu:~$ curl -s http://10.66.111.2
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
И вгетится из другого пода:
user@k-w2:~$ kubectl run test-pod2 --image=alpine --restart=Never --rm -it -- sh
If you don't see a command prompt, try pressing enter.
/ # curl -s http://10.66.111.2
sh: curl: not found
/ # wget http://10.66.111.2 -S
Connecting to 10.66.111.2 (10.66.111.2:80)
HTTP/1.1 200 OK
Server: nginx/1.29.1
Date: Fri, 22 Aug 2025 09:11:48 GMT
Content-Type: text/html
Content-Length: 615
Last-Modified: Wed, 13 Aug 2025 15:10:23 GMT
Connection: close
ETag: "689caadf-267"
Accept-Ranges: bytes
saving to 'index.html'
index.html 100% |*****************************************************************************************| 615 0:00:00 ETA
'index.html' saved
Вывод
Вот так за пару минут мы разобрались как сделать плоскую маршрутизируемую сеть в кубе )