Отказоустойчивый Kubernetes на BGP. Часть 4: kube-vip в режиме BGP и первый мастер
Роутер готов ждать соседей, узлы подготовлены, пора создавать кластер. В этой части поднимем VIP для API-сервера с помощью kube-vip в режиме BGP, инициализируем первый мастер и присоединим остальные два.
Серия «Отказоустойчивый Kubernetes на BGP»
- Схема и план
- Готовим узлы Ubuntu 24.04
- BGP на роутере (FRR)
- kube-vip в режиме BGP и первый мастер (эта статья)
- Calico с eBPF и рабочие узлы
- Адреса LoadBalancer из Calico по BGP
- Traefik за BGP и externalTrafficPolicy Local
- Проверка отказоустойчивости и разбор полётов
Как работает kube-vip в режиме BGP
kube-vip запускается статическим подом на каждом мастере: манифест лежит в /etc/kubernetes/manifests/kube-vip.yaml, и kubelet поднимает его сам — до и независимо от apiserver. Ни Helm, ни kubectl apply тут не участвуют.
В режиме BGP:
- kube-vip вешает VIP (
192.168.20.1/32) на интерфейсlo— не наeth0. Адрес не должен отвечать на ARP, а loopback для этого штатное место. - Внутри kube-vip работает BGP-спикер (на базе GoBGP), и он открывает eBGP-сессию с роутером.
- Анонсируют все три мастера одновременно. Лидер не выбирается, lease не нужен.
- Вместо переезда адреса работает health-check: kube-vip опрашивает локальный apiserver, и если он не отвечает, снимает анонс. Роутер убирает мастера из ECMP.
Про health-check стоит сказать особо. Раз анонсируют все, роутер раскладывает запросы между мастерами. Если на одном узле apiserver умрёт, а kubelet и под kube-vip останутся живыми, узел продолжит анонсировать маршрут, и треть запросов будет уходить в чёрную дыру. Поэтому в манифесте health-check включён: при трёх неудачных проверках подряд анонс снимается.
DNS для API
Создаём A-запись control.k8s.example.com → 192.168.20.1 в DNS и, как договорились в части 2, дублируем её в /etc/hosts на каждом узле.
Манифест kube-vip
Первый мастер. На других мастерах отличаются ровно два значения: bgp_routerid (адрес самого узла) и control_plane_health_check_address (имя узла).
apiVersion: v1
kind: Pod
metadata:
name: kube-vip
namespace: kube-system
spec:
containers:
- args:
- manager
env:
- name: address
value: "192.168.20.1"
- name: port
value: "6443"
- name: vip_subnet
value: "32"
- name: vip_interface
value: lo
- name: vip_arp
value: "false"
- name: vip_nodename
valueFrom:
fieldRef:
fieldPath: spec.nodeName
- name: cp_enable
value: "true"
- name: cp_namespace
value: kube-system
- name: bgp_enable
value: "true"
- name: bgp_as
value: "65020"
- name: bgp_routerid
value: "192.168.10.17"
- name: bgp_peeraddress
value: "192.168.10.1"
- name: bgp_peeras
value: "65010"
- name: control_plane_health_check_address
value: "https://k8s-master-1.example.com:6443/livez"
- name: control_plane_health_check_ca_path
value: /etc/kubernetes/pki/ca.crt
- name: control_plane_health_check_period_seconds
value: "5"
- name: control_plane_health_check_timeout_seconds
value: "3"
- name: control_plane_health_check_failure_threshold
value: "3"
- name: prometheus_server
value: :2112
image: ghcr.io/kube-vip/kube-vip:v1.2.4
imagePullPolicy: IfNotPresent
name: kube-vip
resources:
requests:
memory: 64Mi
limits:
memory: 512Mi
securityContext:
capabilities:
add:
- NET_ADMIN
- NET_RAW
drop:
- ALL
volumeMounts:
- mountPath: /etc/kubernetes/admin.conf
name: kubeconfig
- mountPath: /etc/kubernetes/pki/ca.crt
name: ca-cert
readOnly: true
hostAliases:
- hostnames:
- kubernetes
ip: 127.0.0.1
hostNetwork: true
volumes:
- hostPath:
path: /etc/kubernetes/super-admin.conf # только на время kubeadm init, см. ниже
name: kubeconfig
- hostPath:
path: /etc/kubernetes/pki/ca.crt
type: File
name: ca-cert
Параметры
| Параметр | Значение | Смысл |
|---|---|---|
address |
192.168.20.1 |
VIP. Живёт в подсети, которой физически не существует |
vip_interface |
lo |
в BGP адрес не должен отвечать на ARP |
vip_arp |
"false" |
ARP-объявления выключены |
cp_enable |
"true" |
режим control plane |
bgp_as |
"65020" |
наша AS (AS мастеров) |
bgp_routerid |
адрес узла | Router-ID локального спикера, у каждого свой |
bgp_peeraddress, bgp_peeras |
192.168.10.1, 65010 |
роутер, сессия eBGP |
control_plane_health_check_* |
защита от чёрной дыры | |
prometheus_server |
:2112 |
метрики kube-vip |
Чего в манифесте нет. Если вы начнёте с примера для ARP-режима, уберите из него vip_leaderelection, vip_leasename, vip_leaseduration, vip_renewdeadline и vip_retryperiod: это механизм выбора одного владельца адреса, в BGP он не нужен. Туда же — dhcp_mode и dns_mode.
Адрес health-check — имя узла, а не localhost. В SAN сертификата apiserver localhost нет, и проверка TLS по нему не пройдёт. По имени узла анонимный /livez отдаёт 200: kubeadm заводит для этого роль system:public-info-viewer. CA подкладывается отдельным hostPath-томом.
Лимит памяти. В версиях kube-vip v1.2.1–v1.2.3 в режиме BGP текла память — около 18 МБ в час на каждом мастере, потому что в цикле health-check defer resp.Body.Close() никогда не срабатывал. Причину я нашёл и описал в kube-vip#1778; исправление вышло в v1.2.4. Ставьте эту версию или новее. Лимит 512Mi я оставил как страховку: на исправленной версии он недостижим (рост 0,03 МБ в час), но если утечка вернётся, контейнер упрётся в лимит, kubelet поднимет его за секунды, а VIP в это время анонсируют два других мастера.
Загвоздка первого мастера: super-admin.conf
kube-vip нужен kubeconfig с правами администратора. В манифесте это /etc/kubernetes/admin.conf. Но на первом мастере его ещё не существует: kubeadm init создаёт его в самом конце. А с версии 1.29 kubeadm создаёт ещё и super-admin.conf, который появляется ещё на этапе генерации kubeconfig-ов.
Поэтому на время kubeadm init в манифесте стоит super-admin.conf (что я и показал выше), а после инициализации мы вернём admin.conf. Это нужно сделать только на первом мастере.
Кладём манифест до запуска kubeadm init:
sudo mkdir -p /etc/kubernetes/manifests
sudo cp kube-vip.yaml /etc/kubernetes/manifests/kube-vip.yaml
kubelet его пока не видит: он ещё не настроен и не запущен, а подберёт манифест, как только kubeadm его стартует.
kubeadm init
Конфигурация инициализации:
# kubeadm-init.yaml
apiVersion: kubeadm.k8s.io/v1beta4
kind: InitConfiguration
localAPIEndpoint:
advertiseAddress: 192.168.10.17
nodeRegistration:
criSocket: unix:///run/containerd/containerd.sock
ignorePreflightErrors:
- DirAvailable--etc-kubernetes-manifests # там уже лежит манифест kube-vip
---
apiVersion: kubeadm.k8s.io/v1beta4
kind: ClusterConfiguration
kubernetesVersion: v1.36.4
controlPlaneEndpoint: control.k8s.example.com:6443
networking:
serviceSubnet: 10.96.0.0/12
proxy:
disabled: true
Несколько замечаний:
controlPlaneEndpoint— именно имя, не IP-адрес. Оно попадает в сертификаты и kubeconfig-и. Самого VIP в SAN нет, и это даёт свободу: адрес можно менять без перевыпуска сертификатов (хотя/etc/hostsи DNS придётся поправить).proxy.disabled: true— в кластере не будетkube-proxy: сервисы обслуживает Calico в режиме eBPF (часть 5). Поле работает и приkubeadm upgrade: он сообщитSkipping the addon/kube-proxy phaseи не вернётkube-proxyобратно.podSubnetне задан намеренно. Calico использует собственный IPAM, и пул подов мы зададим вInstallation(иначе Calico создаст стоковый192.168.0.0/16, и он пересечётся с домашней сетью).advertiseAddress— реальный адрес узла, не VIP.
Запускаем:
sudo kubeadm init --config kubeadm-init.yaml --upload-certs
--upload-certs загружает сертификаты control plane в секрет, и остальные мастера смогут их забрать при join. Ключ шифрования (certificate-key) действует 2 часа; потом kubeadm init phase upload-certs --upload-certs выдаст новый.
Если инициализация надолго остановилась на ожидании control plane, заглядываем в журнал kube-vip: sudo crictl logs $(sudo crictl ps -a --name kube-vip -q | head -1). Часть шагов kubeadm обращается к кластеру по имени control.k8s.example.com, то есть к VIP, а он появляется не сразу: сначала должен подняться apiserver и пройти health-check, потом — установиться BGP-сессия с роутером. Обычно на это уходит несколько секунд.
После завершения kubeadm напечатает команды join для мастеров и воркеров — сохраните их.
Возвращаем admin.conf
mkdir -p $HOME/.kube
sudo cp /etc/kubernetes/admin.conf $HOME/.kube/config
sudo chown $(id -u):$(id -g) $HOME/.kube/config
sudo sed -i 's#/etc/kubernetes/super-admin.conf#/etc/kubernetes/admin.conf#' /etc/kubernetes/manifests/kube-vip.yaml
Правка манифеста — это и есть деплой: kubelet следит за каталогом и пересоздаёт под сам. Через несколько секунд:
ip -brief addr show lo # 192.168.20.1/32 на lo
sudo crictl ps --name kube-vip
В журнале kube-vip должны быть строки BGP health check passed, announcing route (с cidr=192.168.20.1/32) и состояние сессии BGP_FSM_ESTABLISHED. На роутере:
sudo vtysh -c 'show bgp summary'
ip route show 192.168.20.1
Сосед 192.168.10.17 должен получить число в колонке State/PfxRcd (1), а в таблице ядра должен появиться маршрут 192.168.20.1 через 192.168.10.17. Проверяем доступ с любой машины сети:
curl -sk https://192.168.20.1:6443/livez ; echo
Второй и третий мастера
На первом мастере (или где лежит команда из вывода kubeadm init):
sudo kubeadm join control.k8s.example.com:6443 \
--token <token> \
--discovery-token-ca-cert-hash sha256:<hash> \
--control-plane --certificate-key <key> \
--apiserver-advertise-address 192.168.10.18
Здесь --apiserver-advertise-address — адрес самого присоединяемого узла.
kubeadm join --control-plane создаёт admin.conf на новом мастере в самом конце присоединения, поэтому манифест kube-vip здесь кладём после join. Пока kube-vip на новом мастере не запущен, VIP спокойно обслуживают остальные: лидера нет, и никто никого не ждёт.
Берём манифест первого мастера в его окончательном виде (с hostPath на admin.conf, после правки sed выше), копируем на новый мастер и меняем два значения: адрес в bgp_routerid и имя узла в адресе health-check:
sudo sed -e 's/192.168.10.17/192.168.10.18/' -e 's/k8s-master-1/k8s-master-2/' kube-vip-master-1.yaml \
| sudo tee /etc/kubernetes/manifests/kube-vip.yaml
Повторяем для третьего (.19, k8s-master-3). Результат на роутере:
ip route show 192.168.20.1
192.168.20.1 proto bgp metric 20
nexthop via 192.168.10.17 dev eth1 weight 1
nexthop via 192.168.10.18 dev eth1 weight 1
nexthop via 192.168.10.19 dev eth1 weight 1
Три nexthop — это и есть ECMP: роутер раскладывает запросы к API между тремя мастерами.
Что дальше
Узлы пока NotReady, поды CoreDNS в Pending: в кластере нет сети. Мастера по умолчанию с taint NoSchedule, и я его не снимаю — пользовательская нагрузка живёт только на воркерах.
kubectl get nodes
Проверьте, что etcd собрался в три члена:
kubectl -n kube-system get pods -l component=etcd -o wide
Что важно знать
kubectl delete podне перезапускает статический под. kubelet лишь пересоздаёт «зеркальный» объект, а контейнер продолжает работать. Перезапуск — только на узле:sudo crictl stop $(sudo crictl ps --name kube-vip -q).- Статический под не может ссылаться на ConfigMap и Secret, только на downward API и
hostPath. - Обновление kube-vip — поднять тег в манифесте на узле, по одному мастеру за раз, и дождаться, пока под перейдёт в
Readyи в журнале снова появятсяannouncing routeиPeer Up. Между мастерами VIP не пропадает: анонсируют оставшиеся два.
В части 5 ставим Calico с eBPF, чтобы у кластера появилась сеть, и присоединяем рабочие узлы.
Оставить комментарий могут только зарегистрированные пользователи.
Войдите на сайт или зарегистрируйтесь, чтобы оставить комментарий.