Есть момент в жизни каждого администратора, когда готовые коробочные решения вроде K3s или MicroK8s перестают устраивать. Хочется понять, из чего Kubernetes собран на самом деле, какие компоненты общаются между собой, где крутится etcd, как CNI договаривается с kubelet. Kubeadm даёт ровно этот опыт - официальный инструмент от самой команды Kubernetes, который собирает ванильный кластер без магии и чужих мнений.
Три ноды на Ubuntu Server - классическая конфигурация для учебного или небольшого продакшен-кластера. Одна машина под control-plane, две под воркеры. Такой расклад позволяет прочувствовать распределённую природу Kubernetes, обкатать отказоустойчивость на уровне приложений и при этом не утонуть в сложности многомастерных инсталляций. Дорога займёт пару часов, если идти размеренно и не пропускать шаги.
Что должно быть готово до запуска первой команды
Первое - три машины с Ubuntu Server 24.04 LTS, у каждой минимум 2 ГБ RAM и 2 vCPU. Для master-ноды лучше взять с запасом, 4 ГБ памяти избавят от неприятных сюрпризов, когда на кластер накатят первые реальные нагрузки. Диск 20 ГБ и выше, сеть - гигабит между нодами, интернет для скачивания образов.
Пример схемы адресации, по которой поедет вся дальнейшая настройка:
- k8s-master с адресом 192.168.1.10, роль control-plane
- k8s-worker01 с адресом 192.168.1.11, роль worker
- k8s-worker02 с адресом 192.168.1.12, роль worker
На каждой машине сразу стоит задать осмысленный hostname, чтобы потом не путаться в выводе kubectl:
sudo hostnamectl set-hostname k8s-master
sudo hostnamectl set-hostname k8s-worker01
sudo hostnamectl set-hostname k8s-worker02
Все три ноды должны видеть друг друга по именам. Проще всего добавить соответствия в /etc/hosts на каждой машине:
cat <<EOF | sudo tee -a /etc/hosts
192.168.1.10 k8s-master
192.168.1.11 k8s-worker01
192.168.1.12 k8s-worker02
EOF
Важная деталь, про которую забывают примерно все, кто делает это впервые. У виртуалок, склонированных из одного шаблона, часто совпадают MAC-адреса и product_uuid. Kubernetes опирается на их уникальность, и совпадение приводит к странным ошибкам при join. Проверить и поправить:
sudo cat /sys/class/dmi/id/product_uuid
ip link show
Если значения совпали, виртуалки нужно пересоздать с уникальными идентификаторами.
Подготовка системы к роли узла кластера, где мелочи решают всё
Kubernetes не дружит со swap. Когда планировщик рассчитывает, сколько памяти отдать поду, страничный своп путает ему все карты. На каждой из трёх нод swap отключается навсегда:
sudo swapoff -a
sudo sed -i '/ swap / s/^\(.*\)$/#\1/g' /etc/fstab
Дальше - модули ядра. Сетевой стек Kubernetes требует overlay для наложенных сетей и br_netfilter, чтобы трафик через мост виделся iptables. Загрузка при каждом старте системы обеспечивается через /etc/modules-load.d:
cat <<EOF | sudo tee /etc/modules-load.d/k8s.conf
overlay
br_netfilter
EOF
sudo modprobe overlay
sudo modprobe br_netfilter
Параметры sysctl нужно выставить так, чтобы включить IP-форвардинг и правильно обрабатывать bridge-трафик:
cat <<EOF | sudo tee /etc/sysctl.d/k8s.conf
net.bridge.bridge-nf-call-iptables = 1
net.bridge.bridge-nf-call-ip6tables = 1
net.ipv4.ip_forward = 1
EOF
sudo sysctl --system
Файрвол на нодах кластера обычно отключают или открывают нужные порты. UFW не препятствует работе Kubernetes при правильной настройке, но на начальном этапе проще всего сделать так:
sudo ufw allow 6443/tcp
sudo ufw allow 2379:2380/tcp
sudo ufw allow 10250/tcp
sudo ufw allow 10257/tcp
sudo ufw allow 10259/tcp
sudo ufw allow 179/tcp
sudo ufw allow 4789/udp
Порт 6443 - API-сервер, 2379-2380 - etcd, 10250 - kubelet, 10257 и 10259 - controller-manager и scheduler, 179 и 4789 - Calico BGP и VXLAN. На воркер-нодах достаточно 10250 и портов NodePort (30000-32767), если планируется использовать этот тип сервисов.
Containerd как контейнерный рантайм и почему именно он
Docker давно ушёл из картины Kubernetes в качестве рантайма. Сегодня стандарт де-факто - containerd, тот самый, что когда-то был частью Docker и стал самостоятельным проектом под крылом CNCF. Ставится он из репозиториев Docker, там лежит свежая версия:
sudo apt update
sudo apt install -y ca-certificates curl gnupg
sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list
sudo apt update
sudo apt install -y containerd.io
Дефолтный конфиг containerd почти годится, но одну деталь нужно поправить обязательно. Kubelet в Ubuntu использует systemd как cgroup-драйвер, и containerd должен играть по тем же правилам. Иначе кластер поднимется, но будет вести себя непредсказуемо при нагрузке:
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
sudo systemctl enable containerd
Эта правка в /etc/containerd/config.toml критична. Если забыть про неё, kubelet будет жаловаться на несовместимость cgroup-драйверов, а поды - перезапускаться без видимой причины.
Установка kubeadm, kubelet и kubectl из официального репозитория
Kubernetes давно не лежит в дефолтных репозиториях Ubuntu, для него существует отдельный apt-источник. Каждая минорная версия имеет собственный URL, и подключать нужно именно тот, что соответствует целевой версии кластера. Актуальная стабильная ветка на момент написания - 1.32:
sudo mkdir -p /etc/apt/keyrings
curl -fsSL https://pkgs.k8s.io/core:/stable:/v1.32/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.32/deb/ /' | sudo tee /etc/apt/sources.list.d/kubernetes.list
sudo apt update
sudo apt install -y kubelet kubeadm kubectl
sudo apt-mark hold kubelet kubeadm kubectl
Последняя команда фиксирует версии пакетов. Это страховка от ситуации, когда apt upgrade случайно обновит kubelet до несовместимой версии и разнесёт работающий кластер. Когда придёт время обновляться, hold снимается, выполняется контролируемый апгрейд, hold ставится обратно.
На этом этапе kubelet будет падать и перезапускаться каждые несколько секунд. Это нормально. Он ждёт конфигурацию, которую получит при инициализации кластера или при присоединении к нему.
Инициализация control-plane на мастер-ноде
Момент, ради которого делалась вся предыдущая подготовка. Команда kubeadm init запускает цепочку действий: предварительные проверки, генерация сертификатов, поднятие статических подов API-сервера, controller-manager, scheduler и etcd, настройка kubelet. Параметр pod-network-cidr обязателен, потому что CNI-плагин (в данном случае Calico) ожидает конкретный диапазон:
sudo kubeadm init \
--pod-network-cidr=192.168.0.0/16 \
--apiserver-advertise-address=192.168.1.10 \
--kubernetes-version=stable-1.32
Если подсеть 192.168.0.0/16 пересекается с локальной сетью, стоит выбрать другой диапазон, например 10.244.0.0/16, и потом скорректировать настройки Calico. Apiserver-advertise-address важен, когда на ноде несколько сетевых интерфейсов и нужно явно указать, какой адрес API-сервер будет показывать наружу.
Процесс занимает несколько минут. В конце появляется блок с командой kubeadm join и токеном - её нужно сохранить, воркеры будут присоединяться именно этой строкой. Если команда потерялась, её легко восстановить позже через kubeadm token create --print-join-command.
Дальше настраивается kubectl для обычного пользователя, чтобы не работать из-под root:
mkdir -p $HOME/.kube
sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
sudo chown $(id -u):$(id -g) $HOME/.kube/config
Проверка:
kubectl get nodes
Master показывается со статусом NotReady. Это ожидаемо, кластер ещё не имеет CNI, сеть между подами не работает. CoreDNS тоже висит в состоянии Pending и ждёт, пока появится сеть.
Развёртывание Calico и почему сеть решает всё
Kubernetes намеренно не включает сетевой плагин в поставку. Выбор CNI - ответственность администратора. Calico - самый популярный вариант, зрелый, с поддержкой сетевых политик, BGP-роутинга и гибких режимов работы. Установка в версии 3.27 и выше использует оператор:
kubectl create -f https://raw.githubusercontent.com/projectcalico/calico/v3.27.0/manifests/tigera-operator.yaml
kubectl create -f https://raw.githubusercontent.com/projectcalico/calico/v3.27.0/manifests/custom-resources.yaml
Через минуту-другую поды calico-node и calico-kube-controllers появляются в состоянии Running в namespace calico-system. Master переходит в Ready, CoreDNS запускается. Прогресс легко наблюдать командой kubectl get pods -A -w, которая показывает изменения статусов в реальном времени.
Если под-CIDR был выбран отличным от 192.168.0.0/16, в файле custom-resources.yaml нужно поправить диапазон до применения. Это делается через wget, редактирование и последующий kubectl apply.
Присоединение воркеров и первая проверка работоспособности
На обеих воркер-нодах выполняется та самая команда kubeadm join, которую напечатал init. Выглядит она примерно так:
sudo kubeadm join 192.168.1.10:6443 \
--token 72n5yk.7khhkaav311irike \
--discovery-token-ca-cert-hash sha256:4c3a99418e3cd40cafe8858306657205f97b1d56ed1e57d729be10a0d2d0d431
Процесс короче инициализации, через 30-60 секунд нода появляется в списке. Проверка с мастера:
kubectl get nodes -o wide
Вывод должен показать все три ноды в статусе Ready, с указанием их внутренних адресов, версии Kubernetes, ОС и рантайма. Если воркер висит в NotReady, стоит посмотреть journalctl -u kubelet -f на проблемной ноде, почти всегда причина описана в логах явно.
Самый простой тест работоспособности - развернуть nginx и проверить доступность:
kubectl create deployment nginx --image=nginx:alpine --replicas=3
kubectl expose deployment nginx --port=80 --type=NodePort
kubectl get pods -o wide
kubectl get svc nginx
Поды должны распределиться между воркерами, сервис получить NodePort в диапазоне 30000-32767. Запрос curl на любую ноду кластера по этому порту вернёт приветственную страницу nginx.
Что делать дальше и какие привычки сберегут нервы
Кластер работает, но это только фундамент. Для реального использования понадобятся несколько компонентов, которых kubeadm принципиально не ставит. MetalLB или kube-vip для LoadBalancer-сервисов, иначе внешние адреса доступны только через NodePort. Ingress-контроллер - nginx-ingress, Traefik, HAProxy Ingress - для HTTP-маршрутизации. Storage-провайдер - local-path-provisioner для простых случаев, Longhorn или Rook-Ceph для распределённого хранилища. Cert-manager для автоматического получения TLS-сертификатов от Let's Encrypt. Metrics-server, чтобы kubectl top работал и HPA мог масштабировать поды.
Несколько привычек, которые окупаются многократно:
- Регулярные снапшоты etcd (etcdctl snapshot save) - без них любая порча control-plane означает потерю всего состояния кластера
- Мониторинг ротации сертификатов (kubeadm certs check-expiration), потому что сертификаты kubeadm живут один год
- Осознанный апгрейд по официальному пути (kubeadm upgrade plan, затем apply, нодами по очереди), без перепрыгивания через минорные версии
- Документирование каждого отклонения от стандарта - через полгода никто не вспомнит, зачем была поправлена именно эта строчка
- Отдельная сеть для ноd и для подов - пересечения подсетей приводят к тонким багам, которые ищутся часами
Kubeadm честно показывает, из чего состоит Kubernetes. Нет скрытых слоёв, нет волшебных ярлыков, нет встроенных контроллеров, которые делают что-то сами по себе. Каждый компонент установлен администратором, каждая настройка выбрана осознанно. Именно этот опыт превращает новичка в человека, который понимает, почему кластер ведёт себя так, а не иначе, и умеет чинить его в три часа ночи, когда что-то идёт не по плану. А идти не по плану оно будет, рано или поздно, и лучше встретить этот момент с пониманием устройства, а не с инструкцией из чужого блога.