Отказоустойчивый Kubernetes на BGP. Часть 4: kube-vip в режиме BGP и первый мастер

Роутер готов ждать соседей, узлы подготовлены, пора создавать кластер. В этой части поднимем VIP для API-сервера с помощью kube-vip в режиме BGP, инициализируем первый мастер и присоединим остальные два.

Серия «Отказоустойчивый Kubernetes на BGP»

  1. Схема и план
  2. Готовим узлы Ubuntu 24.04
  3. BGP на роутере (FRR)
  4. kube-vip в режиме BGP и первый мастер (эта статья)
  5. Calico с eBPF и рабочие узлы
  6. Адреса LoadBalancer из Calico по BGP
  7. Traefik за BGP и externalTrafficPolicy Local
  8. Проверка отказоустойчивости и разбор полётов

Как работает kube-vip в режиме BGP

kube-vip запускается статическим подом на каждом мастере: манифест лежит в /etc/kubernetes/manifests/kube-vip.yaml, и kubelet поднимает его сам — до и независимо от apiserver. Ни Helm, ни kubectl apply тут не участвуют.

В режиме BGP:

  1. kube-vip вешает VIP (192.168.20.1/32) на интерфейс lo — не на eth0. Адрес не должен отвечать на ARP, а loopback для этого штатное место.
  2. Внутри kube-vip работает BGP-спикер (на базе GoBGP), и он открывает eBGP-сессию с роутером.
  3. Анонсируют все три мастера одновременно. Лидер не выбирается, lease не нужен.
  4. Вместо переезда адреса работает 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, чтобы у кластера появилась сеть, и присоединяем рабочие узлы.

Оставить комментарий могут только зарегистрированные пользователи.

Войдите на сайт или зарегистрируйтесь, чтобы оставить комментарий.