Вот какой вопрос я задаю очень часто на собеседовании:

- Если вы подняли на дефолтных настройках, по мануалам из интернета, кластер с помощью kubespray, что будет, если выключить первый мастер?

Все будет хорошо! Ведь это Kubernetes!

- Кластер превратится в тыкву. Давайте попробую объяснить...

И, после этого, я пробую объяснить. Но, лучше, опишу это в статье.

Знаете эту боль? Вы хотите собрать крутой отказоустойчивый кластер на голом железе. Всё честно: три мастера, никаких облаков, никаких «ай-ай-ай». А потом бац - и понимаете, что API-серверу нужен балансировщик, а балансировщику не встать без API-сервера. Классический замкнутый круг, достойный сериала по роману «Песнь льда и пламени».

И тут на белом коне приезжает Kube-Vip.

Мы его сейчас научим работать до того, как вы скажете «kubeadm init». Без магии, но с правильными заклинаниями в терминале.

Почему мы не будем использовать DaemonSet (спойлер: не взлетит)

DaemonSet - это, конечно, хорошо. Но DaemonSet создаёт API-сервер. А API-сервера пока нет. Вы поняли. Поэтому мы пойдём другим путём - сделаем статический под (Static Pod). Kubelet сам его подхватит с диска, даже если вокруг пустота и тишина.

Что у вас должно быть ДО старта (чек-лист)

Проверьте по списку, как заправский пилот перед вылетом:

  • kubeadm, kubelet, containerd стоят на всех control-plane узлах.

  • swap выключен (да-да, swapoff -a и в /etc/fstab закомментирован, мы не в 2010 году).

  • Модули ядра br_netfilter и overlay загружены.

  • Есть заветный IP-адрес (VIP) — статический, который не раздаётся DHCP. Например, 192.168.1.100.

  • Все узлы знают этот VIP либо через /etc/hosts, либо через нормальную DNS.

Если всё зелёное — поехали.

Шаг 1. Генерируем манифест

Современный подход - не писать YAML руками, а родить его из образа Kube-Vip. Потому что мы люди ленивые и правильные.

Задаём переменные (экономим ресурс клавиатуры):

export VIP=192.168.1.100
export INTERFACE=eth0
export KVVERSION=$(curl -sL https://api.github.com/repos/kube-vip/kube-vip/releases | jq -r ".[0].name")

Теперь магия. Если у вас Docker (ладно, сочувствую, бывает):

sudo docker run --network host --rm ghcr.io/kube-vip/kube-vip:$KVVERSION manifest pod \
    --interface $INTERFACE \
    --vip $VIP \
    --controlplane \
    --services \
    --arp \
    --leaderElection | sudo tee /etc/kubernetes/manifests/kube-vip.yaml

А если у вас containerd (а он у вас должен быть, это же 2026 год):

sudo ctr image pull ghcr.io/kube-vip/kube-vip:$KVVERSION

sudo ctr run --rm --net-host ghcr.io/kube-vip/kube-vip:$KVVERSION vip /kube-vip manifest pod \
    --interface $INTERFACE \
    --vip $VIP \
    --controlplane \
    --services \
    --arp \
    --leaderElection > /etc/kubernetes/manifests/kube-vip.yaml

Что мы сделали? Правильно -  положили файлик в /etc/kubernetes/manifests/. Kubelet уже навострил уши и ждёт.

Шаг 2. Заглянем в манифест

Там внутри три важных тезиса:

  • hostNetwork: true - под не стесняется, живёт в сети хоста.

  • Капы NET_ADMIN, NET_RAW — чтобы VIP мог рулить интерфейсами.

  • Переменные vip_arp, cp_enable, svc_enable - говорят сами за себя.

Мы довольны, как слоны.

Шаг 3. ВАЖНЕЙШИЙ НЮАНС для Kubernetes 1.29+ (или как не выстрелить себе в ногу)

Слушайте внимательно. Если у вас K8S версии 1.29 или новее, Kube-Vip по умолчанию полезет за admin.conf. А прав у него в момент старта - кот наплакал. И kubeadm init тихо зависнет в неопределённости.

Лечение: лезем в /etc/kubernetes/manifests/kube-vip.yaml и меняем там:

volumeMounts:
- mountPath: /etc/kubernetes/super-admin.conf
  name: kubeconfig

и в секции volumes:

volumes:
- hostPath:
    path: /etc/kubernetes/super-admin.conf
  name: kubeconfig

Да, вместо admin.conf пишем super-admin.conf. Маленькая буква, а спасает задницу от возгараний, когда все повисне.

Шаг 4. Тот самый «kubeadm init»

Теперь главное. Команда kubeadm init обязана смотреть на VIP:

sudo kubeadm init --control-plane-endpoint $VIP:6443 --upload-certs

И тут происходит что-то прекрасное:

  1. Kubelet видит наш манифест.

  2. Запускает Kube-Vip.

  3. Kube-Vip поднимает VIP на интерфейсе.

  4. kubeadm стучится на https://192.168.1.100:6443 - и о чудо! API-сервер отвечает.

Курица и яйцо больше не спорят. Все отлично.

Шаг 5. Добавляем остальных мастеров

После успешного init на первом узле — несите манифест на другие master-узлы. Вот так:

# На первом узле
sudo cat /etc/kubernetes/manifests/kube-vip.yaml

# На втором узле
sudo mkdir -p /etc/kubernetes/manifests
sudo vi /etc/kubernetes/manifests/kube-vip.yaml
# вставляем содержимое

И подключаем второй узел командой kubeadm join с флагом --control-plane:

sudo kubeadm join 192.168.1.100:6443 --token ... --discovery-token-ca-cert-hash ... --control-plane --certificate-key ...

Kubelet на втором узле тут же поднимет Kube-Vip, и теперь у нас есть:

  • активный мастер с VIP.

  • второй мастер в режиме «пам-парам, жду, когда ты упадёшь».

Шаг 6. Не забываем про сеть

После инициализации кластера CoreDNS будет висеть в Pending. Не пугайтесь. Это он ждёт, когда мы поставим нормальную сеть.

Ставим CNI:

kubectl apply -f https://raw.githubusercontent.com/projectcalico/calico/v3.27/manifests/calico.yaml

Всё. Теперь VIP может крутить не только control plane, но и сервисы типа LoadBalancer.

Два режима Kube-Vip

Режим Как работает Для кого
ARP (L2) Только лидер анонсирует VIP. Упал лидер — VIP уехал к другому. Для простых сетей и тех, кто не хочет парить мозг.
BGP (L3) Все мастера анонсируют VIP, роутер сам раздаёт трафик. Для серьёзных дядек с нормальной сетевой инфраструктурой.

Почему в статье я показываю ARP? Потому что не у каждого дома стоит BGP-роутер.

А теперь - факапы: типичные ошибки

«kubeadm init» упал по таймауту
-> Kube-Vip не поднялся. Идите в crictl ps -a, смотрите, есть ли контейнер. Дальше crictl logs <id>. Там всё написано, просто не поленитесь прочитать.

VIP не пингуется
-> Загляните в манифест: hostNetwork: true? vip_interface указан правильно? Это не lo, а ваш рабочий интерфейс, например eth0.

K8s 1.29+ завис на старте
-> Вы проигнорировали шаг 3 с super-admin.conf. Ну что ж, бывает. Идите исправляйте. Мы все через это проходили.

Итог (он же «живите теперь спокойно»)

Kube-Vip в режиме статического пода - это не просто хак, это стандарт де-факто для bare-metal HA-кластеров на kubeadm. Вы разрываете циклическую зависимость, получаете полноценный балансировщик нагрузки на этапе инициализации и спите спокойно.

И да, теперь вы можете гордо говорить: «Я настроил HA-кластер без облаков и без боли».

Эпилог: А как же бедные Kubespray-страдальцы?

Знаете, есть категория людей, которые любят себе жизнь... Скажем так, делать интереснее. Вместо того чтобы руками настроить Kube-Vip и понять, как всё работает, они берут Kubespray. Этот могучий комбайн, который "сам всё сделает". И знаете что? 80% времени - действительно делает. Но есть одно НО.

Я сейчас скажу страшную вещь. Присядьте.

В Kubespray первый мастер-узел - священная корова. На нём генерируются сертификаты, на нём происходит магия инициализации. И пока всё хорошо - вы счастливы. Но в один прекрасный день (обычно в 3 часа ночи в пятницу) этот первый мастер берёт и умирает. Жёстко. Безвозвратно. И тут выясняется, что ваш HA-кластер... не очень-то и HA.

Почему? Потому что Kubespray в стандартной настройке генерирует манифест Kube-Vip, который на первом мастере использует admin.conf. А когда этот мастер уходит в digital Valhalla, остальные мастера с тупым выражением лица пытаются понять, кто теперь главный. И не могут.

Но не спешите рвать на себе волосы при падающем проде. Есть способ не налажать.

Как не выстрелить себе в ногу в Kubespray

Современные версии Kubespray (смотрите пул-реквест #11422, если не верите) уже умеют правильно готовить манифест . Логика там такая умная, что аж слеза пробивает:

# Kubespray делает так:
- Если узел - первый в группе kube_control_plane
- И если существует super-admin.conf (или kubeadm ещё не запускался)
- ТО используем super-admin.conf
- А на всех остальных узлах - обычный admin.conf

Выглядит это в коде примерно так :

- name: Kube-vip | Set admin.conf for first Control Plane
  set_fact:
    kube_vip_admin_conf: super-admin.conf
  when:
    - inventory_hostname == groups['kube_control_plane'] | first
    - (stat_kube_vip_super_admin.stat.exists and stat_kube_vip_super_admin.stat.isreg) 
       or (not kubeadm_already_run.stat.exists)

Что это значит для вас? Ровно один пункт в чек-листе: используйте актуальную версию Kubespray (смотрите релизы, там где kube-vip 1.0.3+ уже есть) .

Если же у вас версия древняя, то вот ручной фикс:

  1. Найдите в своём инвентаре манифест Kube-Vip (обычно в /etc/kubernetes/manifests/)

  2. В секции volumes замените admin.conf на super-admin.conf только на первом мастере

  3. Перезапустите kubelet

Но лучше обновите Kubespray. Ваши нервы дороже.

Для тех, кто читает между строк - технический бонус

Если вы хотите, чтобы ваши контрольные узлы были реально заменяемыми, добавьте в your inventory эти параметры :

# Включить локальный режим kubelet (чтобы не зависеть от VIP при старте)
kubelet_local_mode: true

# А это чтобы kube-proxy не мешал Kube-Vip работать с ARP
kube_proxy_strict_arp: true

И да, убедитесь, что у вас kube_vip_cidr не стоит голым числом 32. Этот баг уже починили, но на старых версиях он ломает генерацию YAML .

Итог - теперь окончательный

Мы с вами сегодня:

  • Расколдовали запуск Kube-Vip до инициализации кластера

  • Научили его правильно лопать super-admin.conf на новых версиях Kubernetes

  • Посочувствовали пользователям Kubespray (и дали им волшебный пендель)

Помните главное правило DevOps-инженера: автоматизация должна работать правильно, а не просто работать. А если ваш инструмент автоматизации ломает кластер при падении первого мастера - значит, это не автоматизация, это "автотрындец".

Настройте Kube-Vip один раз правильно. И пусть ваши пятничные вечера будут посвящены пиву, а не восстановлению кластера.