Вот какой вопрос я задаю очень часто на собеседовании:
- Если вы подняли на дефолтных настройках, по мануалам из интернета, кластер с помощью 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
И тут происходит что-то прекрасное:
-
Kubelet видит наш манифест.
-
Запускает Kube-Vip.
-
Kube-Vip поднимает VIP на интерфейсе.
-
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+ уже есть) .
Если же у вас версия древняя, то вот ручной фикс:
-
Найдите в своём инвентаре манифест Kube-Vip (обычно в /etc/kubernetes/manifests/)
-
В секции volumes замените admin.conf на super-admin.conf только на первом мастере
-
Перезапустите 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 один раз правильно. И пусть ваши пятничные вечера будут посвящены пиву, а не восстановлению кластера.