Отказоустойчивый Kubernetes на BGP. Часть 2: готовим узлы Ubuntu 24.04

Во второй части серии подготовим узлы. Всё в этой статье выполняется на каждом из восьми узлов (и на мастерах, и на воркерах), если не сказано иное.

Серия «Отказоустойчивый 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. Проверка отказоустойчивости и разбор полётов

Имя и адрес узла

Имя узла в Kubernetes берётся из hostname, поэтому сразу делаем его полным (FQDN) и одинаковым с DNS:

sudo hostnamectl set-hostname k8s-master-1.example.com

Статический адрес

В моём случае адреса сначала выдавались по DHCP, и это обернулось неприятностью. После обновления glibc пакет needrestart перезапустил systemd-networkd, мастер потерял аренду и не получил её обратно — узел пролежал недоступным 17 часов. С тех пор все адреса в кластере статические.

# /etc/netplan/50-cloud-init.yaml
network:
  version: 2
  ethernets:
    eth0:
      dhcp4: false
      dhcp6: false
      addresses: [192.168.10.17/24]    # у каждого узла свой
      routes:
        - to: default
          via: 192.168.10.1
          metric: 100

Если применяете файл на живом узле по SSH, делайте это с откатом, иначе рискуете остаться без доступа:

sudo cp /etc/netplan/50-cloud-init.yaml /root/netplan-backup.yaml
sudo netplan generate
sudo bash -c 'nohup bash -c "netplan apply; sleep 6; ping -c 2 -W 2 192.168.10.1 >/dev/null || { cp /root/netplan-backup.yaml /etc/netplan/50-cloud-init.yaml; netplan apply; }" >/root/netplan-apply.log 2>&1 &'

DNS

На узлах должно быть два DNS-сервера в /etc/resolv.conf, и сам файл должен быть обычным файлом, а не ссылкой в /run/systemd/resolve/. Если systemd-resolved выключен (так надо, если CoreDNS пересылает внешние имена в resolv.conf узла, — заглушка 127.0.0.53 создаст петлю), то ссылка после перезагрузки повиснет, и kubelet не сможет создать ни одного пода: Failed to create pod sandbox: open /etc/resolv.conf: no such file. Я наступил на это на одном из узлов при плановой перезагрузке.

sudo systemctl disable --now systemd-resolved
sudo rm /etc/resolv.conf
printf 'nameserver 192.168.10.15\nnameserver 192.168.10.10\n' | sudo tee /etc/resolv.conf

Имя control plane в /etc/hosts

Рабочие узлы подключаются к API по имени control.k8s.example.com. Если DNS пропадёт, kubelet не найдёт API, и узлы перейдут в NotReady — хотя сам кластер при этом цел. У меня это один раз случилось на самом деле: упал единственный DNS-сервер сайта, и семь узлов из восьми стали NotReady. Лечится одной строкой на каждом узле:

echo '192.168.20.1 control.k8s.example.com' | sudo tee -a /etc/hosts
echo '192.168.10.17 k8s-master-1.example.com k8s-master-1' | sudo tee -a /etc/hosts   # имя самого узла

Платить за это приходится одним: при смене адреса VIP запись надо поправить на всех узлах.


Swap, модули ядра, sysctl

С версии 1.32 kubelet не стартует при включённом swap, поэтому выключаем его сразу и навсегда:

sudo swapoff -a
sudo sed -i '/\sswap\s/ s/^/#/' /etc/fstab

Модули и параметры, которые проверяет kubeadm при предполётной проверке:

cat <<EOF | sudo tee /etc/modules-load.d/k8s.conf
overlay
br_netfilter
EOF
sudo modprobe overlay
sudo modprobe br_netfilter

cat <<EOF | sudo tee /etc/sysctl.d/99-kubernetes.conf
net.ipv4.ip_forward = 1
net.bridge.bridge-nf-call-iptables = 1
net.bridge.bridge-nf-call-ip6tables = 1
EOF
sudo sysctl --system

containerd

Ставлю containerd.io из репозитория Docker (в Ubuntu он заметно старше):

sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo $VERSION_CODENAME) stable" \
  | sudo tee /etc/apt/sources.list.d/docker.list
sudo apt-get update
sudo apt-get install -y containerd.io

Конфигурация по умолчанию, в которой включаем systemd в роли драйвера cgroup:

sudo mkdir -p /etc/containerd
containerd config default | sudo tee /etc/containerd/config.toml
sudo sed -i 's/SystemdCgroup = false/SystemdCgroup = true/' /etc/containerd/config.toml
sudo systemctl restart containerd

Образ pause (sandbox) в конфигурации должен совпадать с тем, который ожидает kubeadm; сверяем после установки пакетов Kubernetes:

kubeadm config images list | grep pause
grep -n sandbox /etc/containerd/config.toml

О том, как я когда-то ставил containerd, у меня есть и более ранние заметки: Готовим чистый Ubuntu Server к тому, чтобы быть узлом Kubernetes кластера с CRI: ContainerD.


kubeadm, kubelet, kubectl

Репозиторий Kubernetes привязан к минорной версии, поэтому номер версии есть прямо в URL:

sudo mkdir -p /etc/apt/keyrings
curl -fsSL https://pkgs.k8s.io/core:/stable:/v1.36/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.36/deb/ /' \
  | sudo tee /etc/apt/sources.list.d/kubernetes.list
sudo apt-get update
sudo apt-get install -y kubelet kubeadm kubectl
sudo apt-mark hold kubelet kubeadm kubectl containerd.io
sudo systemctl enable --now kubelet

apt-mark hold здесь не декорация. Когда у containerd.io hold нет, он обновляется вместе с остальными пакетами при первом же apt upgrade, и версии на узлах разъезжаются. Обновлять кластер надо по плану, по одной минорной версии: перед каждым шагом apt-mark unhold, после него — снова hold.


Время

etcd и сертификаты чувствительны к времени, а в домене Active Directory рассинхронизация ещё и ломает Kerberos. Я не стал полагаться на внешние серверы: три мастера — NTP-серверы кластера, воркеры берут время у них.

На мастерах — chrony, в отдельном файле, стоковый chrony.conf не трогаем:

# /etc/chrony/conf.d/cluster.conf на k8s-master-1
peer 192.168.10.18
peer 192.168.10.19
allow 192.168.10.0/24
local stratum 10 orphan

local stratum 10 orphan оставляет службу полезной при потере интернета: мастера договариваются между собой, вместо того чтобы разойтись. Ещё одна мелочь: в штатном юните chrony нет Restart=, и если OOM-killer убьёт chronyd, он сам не поднимется. Добавляем:

# /etc/systemd/system/chrony.service.d/override.conf
[Service]
Restart=always

На воркерах хватает systemd-timesyncd:

# /etc/systemd/timesyncd.conf.d/cluster.conf
[Time]
NTP=192.168.10.17 192.168.10.18 192.168.10.19
FallbackNTP=ntp.ubuntu.com

Автообновления: только списки пакетов

Узел кластера не должен сам себя обновлять. Я уже упоминал needrestart, который перезапускает службы после обновления библиотек. Запрещаем ему трогать то, на чём держится узел:

# /etc/needrestart/conf.d/50-kubernetes-needrestart.conf
$nrconf{override_rc}{qr(^systemd-networkd)} = 0;
$nrconf{override_rc}{qr(^kubelet)} = 0;
$nrconf{override_rc}{qr(^containerd)} = 0;

И оставляем apt только обновление списков, без установки обновлений:

# /etc/apt/apt.conf.d/20auto-upgrades
APT::Periodic::Update-Package-Lists "1";
APT::Periodic::Unattended-Upgrade "0";

Раз в месяц, в окно, обновляем узлы руками по одному: apt-get update && apt-get upgrade, needrestart -r l показывает, что ждёт перезапуска, затем перезагрузка с drain и uncordon.


Особенность Hyper-V: nr_cpus

Если ваши узлы — виртуальные машины Hyper-V, сразу добавьте в параметры ядра nr_cpus. Гостю объявляется 240 «возможных» процессоров (smpboot: Allowing 240 CPUs, 236 hotplug CPUs) при четырёх реальных, и ядро выделяет per-CPU память на все 240. Обычному узлу это безразлично, но у Calico в режиме eBPF карты с per-CPU значениями, и у меня 1,3–1,5 ГБ на каждом узле уходило впустую.

sudo sed -i 's/^GRUB_CMDLINE_LINUX_DEFAULT="/&nr_cpus=4 /' /etc/default/grub
sudo update-grub
sudo reboot

Проверка после перезагрузки:

grep Percpu /proc/meminfo     # десятки мегабайт, а не гигабайт

При добавлении виртуальных процессоров число в GRUB надо менять.


Проверка перед kubeadm

На каждом узле:

swapon --show                  # пусто
lsmod | grep -E 'overlay|br_netfilter'
sysctl net.ipv4.ip_forward     # = 1
systemctl is-active containerd # active
kubeadm version -o short       # v1.36.x
getent hosts control.k8s.example.com

Узлы готовы. Но прежде чем создавать кластер, нужен роутер, который умеет принимать маршруты: в части 3 настроим FRR.

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

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