Для сетевиков от сетевика

Среда

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

Так как мы норм пацаны, то:

  1. Всё сэмулируем прямо в PNETLAB-е

  2. Мы будем использовать топологию Leaf-Spine

  3. Мы начитались Заметок сетевого архитектора, и у нас нет никаких вланов, просто чистый L3 - все линки в сторону серверов маршрутизируемые

    • Между свитчами мы подымем OSFP, чтобы все сервачки могли друг с дружкой общаться. Здорово будет.

  4. Мы будем юзать какую-то убунту (первое что под руку попалось, кароче):

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!

Ладно, как я ни старался оттягивать этот момент, пытаясь погрузиться в детали того, как тут всё устроено - уже пора к финишу подходить. Всё равно - чем дальше копаешь, тем больше понимаешь что нихуя не понимаешь. А я не перфекционист - работу не сабботирую.

Поэтому лучше приступим к сетевиковской практике наконец-то

  1. Поменяем пулы с дефолтных /26 на хотя бы /24

  2. Отключим нафиг туннели и NAT, потому что…

  3. Настроим 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

Вот наш план на дальнейшие действия:

  1. Создадим глобальную конфигурацию BGP

  2. Поправим calico-объекты типа node

  3. Создадим точечные инструкции для каждой ноды как и с кем пириться.

  4. Разберём дефолтный full-mesh и отключим туннели.

  5. ???

  6. 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

Вывод

Вот так за пару минут мы разобрались как сделать плоскую маршрутизируемую сеть в кубе )

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