Kubernetes на Raspberry Pi: кластер своими руками

Большинство людей взаимодействуют с Kubernetes через облако и физически находятся от кластера не ближе, чем от собственного ноутбука. Работать с облачным кластером через браузер или терминал вполне удобно, но в развёртывании Kubernetes на голом железе есть своя особая притягательность. А уж наблюдать вживую, как Kubernetes перераспределяет нагрузку после того, как вы физически вытащили узел из розетки — это ни с чем не сравнимое удовольствие.

Что понадобится

Суммарные расходы составят около €300 — в зависимости от того, что у вас уже есть, сколько узлов вы планируете использовать и захотите ли вы приобрести корпус для аккуратной сборки.

4x Raspberry Pi 4 (или Pi 5) с 4 ГБ ОЗУ и более. При 4 ГБ на плату достаточно запаса для запуска рабочих нагрузок. Модель на 8 ГБ — приятное дополнение, но не обязательна. 4x карты microSD (32 ГБ). Операционная система, образы контейнеров и бинарные файлы Kubernetes будут храниться на этих картах. 32 ГБ — достаточно комфортный объём. Многопортовое зарядное устройство USB-C (не менее 15 Вт на порт). Каждый Pi 4 потребляет до 15 Вт под нагрузкой. Четырёхпортовое зарядное устройство USB-C PD GaN питает весь кластер от одной розетки. 4x кабеля USB-C. Сетевой коммутатор (ethernet switch). 4x кабеля ethernet. Картридер для microSD, монитор (у Pi 4 два порта micro-HDMI) и USB-клавиатура. Монитор и клавиатура понадобятся лишь на этапе начальной настройки. Как только SSH настроен, всё управление ведётся удалённо с ноутбука. Корпус для кластера. Например, корпус UCTRONICS для кластера Raspberry Pi позволяет аккуратно разместить четыре платы стопкой с нормальной вентиляцией. Необязательно, но рекомендуется — и на столе смотрится отлично.

Архитектура, которую мы собираем

Прежде чем начать что-то подключать, разберёмся, что именно мы строим и почему именно так.

Pi 1 выполняет двойную роль. Его Wi-Fi-интерфейс подключается к домашнему роутеру и интернету. Его ethernet-интерфейс подключается к приватному коммутатору, к которому подключены три рабочих узла. Pi 1 работает как шлюз: выполняет NAT (трансляцию сетевых адресов, Network Address Translation), благодаря чему рабочие узлы получают доступ в интернет через него — точно так же, как домашний роутер открывает вашим устройствам интернет через один публичный IP.

Зачем такая топология? Рабочие узлы находятся в изолированной подсети 10.0.0.0/24. Это сделано намеренно: такая схема повторяет устройство production-кластеров, где узлы размещены в приватной сети и общаются по выделенной инфраструктуре, а не через тот же Wi-Fi, которым пользуется ваш телефон. Кроме того, кластер никак не будет мешать другим устройствам домашней сети.

Фаза 1: Запись операционной системы

На всех четырёх Pi мы будем использовать Ubuntu Server (64-битная редакция для aarch64).

Запись SD-карт

Для записи образов используйте Raspberry Pi Imager. Перед записью нажмите на иконку шестерёнки, чтобы заранее настроить каждый узел.

Для всех узлов:

  • Включите SSH с аутентификацией по публичному ключу. Если у вас нет ключевой пары ed25519, сгенерируйте её на ноутбуке (ssh-keygen -t ed25519) и вставьте публичный ключ в соответствующее поле.

  • Задайте имя пользователя.

Задайте уникальные имена хостов для каждого узла:

  • Pi 1: k8s-control (будет управляющим узлом и шлюзом)

  • Pi 2: worker-1

  • Pi 3: worker-2

  • Pi 4: worker-3

Запишите каждую карту и вставьте их в соответствующие Pi.

Фаза 2: Настройка сети

Эта фаза — фундамент, на котором держится всё остальное. Если узлы не могут стабильно общаться между собой, ничего дальнейшего не заработает.

Шаг 1. Настройка узла-шлюза (Pi 1)

Загрузите Pi 1 с подключёнными монитором и клавиатурой. Сначала настройте Wi-Fi-интерфейс для подключения к домашнему роутеру, а ethernet-интерфейсу назначьте статический IP — он станет шлюзом для сети кластера.

Создайте конфигурационный файл netplan по пути /etc/netplan/01-cluster.yaml:

network:
  version: 2
  wifis:
    wlan0:
      dhcp4: true
      access-points:
        "YOUR_WIFI_SSID":
          password: "your-wifi-password"
  ethernets:
    eth0:
      addresses:
        - 10.0.0.1/24

Примените настройки:

sudo netplan apply

Теперь у Pi 1 два интерфейса: wlan0 получает IP от домашнего роутера (доступ в интернет), а eth0 статически настроен на 10.0.0.1 (адрес шлюза кластера).

Почему 10.0.0.0/24? Убедимся, что диапазон не пересекается с домашней сетью (которая, скорее всего, использует 192.168.x.x).

Шаг 2. Включение NAT (трансляции сетевых адресов)

Рабочие узлы будут находиться в сети 10.0.0.0/24, у которой нет прямого маршрута в интернет. Pi 1 должен взять на себя роль маршрутизатора.

# Включаем IP-форвардинг
# Это указывает ядру Linux пересылать пакеты
# между интерфейсами, а не отбрасывать их
echo 'net.ipv4.ip_forward = 1' | sudo tee /etc/sysctl.d/ip-forward.conf
sudo sysctl --system

# Настраиваем NAT-маскарадинг
# Подменяем исходящие IP с 10.0.0.x на Wi-Fi-адрес Pi 1,
# чтобы интернет видел трафик как исходящий от Pi 1
sudo apt-get install -y iptables-persistent
sudo iptables -t nat -A POSTROUTING -o wlan0 -j MASQUERADE
sudo netfilter-persistent save

Без этого шага рабочие узлы смогут общаться друг с другом и с Pi 1, но любая попытка выйти в интернет (а он понадобится для загрузки образов контейнеров) будет молча завершаться неудачей.

Шаг 3. Запуск DHCP-сервера на Pi 1

Вместо ручной настройки статических IP на каждом рабочем узле запустим на Pi 1 DHCP-сервер, который будет раздавать адреса автоматически. Важнее другое: мы привяжем конкретные IP к конкретным MAC-адресам, чтобы каждый рабочий узел всегда получал один и тот же IP. Узлы Kubernetes регистрируются по своему IP, поэтому менять его нежелательно.

sudo apt-get install -y isc-dhcp-server

Отредактируйте /etc/dhcp/dhcpd.conf:

# Подсеть, которой управляет DHCP-сервер
subnet 10.0.0.0 netmask 255.255.255.0 {
  range 10.0.0.10 10.0.0.50; # динамический диапазон для новых устройств
  option routers 10.0.0.1; # указываем Pi 1 как шлюз для клиентов
  option domain-name-servers 8.8.8.8, 1.1.1.1; # публичные DNS-серверы
}

# Статические аренды. Каждый рабочий узел всегда получает один IP.
# MAC-адрес своего Pi можно узнать командой: ip link show eth0
host worker-1 {
  hardware ethernet XX:XX:XX:XX:XX:XX; # замените на MAC-адрес eth0 Pi 2
  fixed-address 10.0.0.11;
}
host worker-2 {
  hardware ethernet XX:XX:XX:XX:XX:XX; # замените на MAC-адрес eth0 Pi 3
  fixed-address 10.0.0.12;
}
host worker-3 {
  hardware ethernet XX:XX:XX:XX:XX:XX; # замените на MAC-адрес eth0 Pi 4
  fixed-address 10.0.0.13;
}

Укажите DHCP-серверу слушать только интерфейс eth0 (чтобы не мешать домашней сети через Wi-Fi). Отредактируйте /etc/default/isc-dhcp-server:

INTERFACESv4="eth0"

Здесь есть типичная проблема с порядком загрузки: DHCP-сервер может стартовать раньше, чем ethernet-интерфейс получит статический IP. В таком случае сервер не видит корректного адреса на eth0 и аварийно завершается. Чтобы избежать этого, создайте drop-in файл для systemd, который заставит DHCP-сервис дождаться готовности eth0:

sudo mkdir -p /etc/systemd/system/isc-dhcp-server.service.d
cat <<'EOF' | sudo tee /etc/systemd/system/isc-dhcp-server.service.d/wait-for-eth0.conf
[Unit]
After=sys-subsystem-net-devices-eth0.device
Wants=sys-subsystem-net-devices-eth0.device

[Service]
ExecStartPre=/bin/sh -c 'i=0; while [ $i -lt 30 ]; do ip -4 addr show eth0 | grep -q inet && exit 0; sleep 1; i=$((i+1)); done; exit 1'
Restart=on-failure
RestartSec=5
EOF
sudo systemctl daemon-reload
sudo systemctl enable --now isc-dhcp-server

Скрипт ExecStartPre ожидает до 30 секунд, пока eth0 не получит IPv4-адрес. Restart=on-failure — это страховка: если что-то пойдёт не так по другой причине, systemd перезапустит сервис, а не оставит вас без DHCP.

Шаг 4. Загрузка рабочих узлов и проверка

Подключите все три рабочих узла к коммутатору, включите их и дайте им минуту на загрузку и получение DHCP-аренд. Проверьте с Pi 1:

# Проверяем, что DHCP-аренды выданы
cat /var/lib/dhcp/dhcpd.leases

# Пингуем каждый рабочий узел
ping -c 2 10.0.0.11
ping -c 2 10.0.0.12
ping -c 2 10.0.0.13

# Проверяем, что рабочие узлы выходят в интернет через NAT Pi 1
ssh ubuntu@10.0.0.11 'ping -c 2 8.8.8.8'

Шаг 5. SSH-конфигурация на ноутбуке

Управлять кластером вы будете с ноутбука, а не с монитора, подключённого к Pi 1. Рабочие узлы недоступны напрямую с ноутбука (они находятся в подсети 10.0.0.0/24, о которой ноутбук ничего не знает), поэтому используем Pi 1 как промежуточный хост (jump host).

Добавьте следующее в ~/.ssh/config на вашем ноутбуке:

Host k8s-control
  HostName <Wi-Fi IP Pi 1>
  User ubuntu
  IdentityFile ~/.ssh/id_ed25519

Host worker-1
  HostName 10.0.0.11
  User ubuntu
  IdentityFile ~/.ssh/id_ed25519
  ProxyJump k8s-control

Host worker-2
  HostName 10.0.0.12
  User ubuntu
  IdentityFile ~/.ssh/id_ed25519
  ProxyJump k8s-control

Host worker-3
  HostName 10.0.0.13
  User ubuntu
  IdentityFile ~/.ssh/id_ed25519
  ProxyJump k8s-control

Теперь команда ssh worker-2 с ноутбука автоматически пройдёт через Pi 1. Директива ProxyJump указывает SSH сначала подключиться к k8s-control, а затем перепрыгнуть на нужный рабочий узел.

Фаза 3: Предварительные требования Kubernetes

Перед установкой Kubernetes на рабочих узлах нужно сделать несколько изменений.

Шаг 1. Отключение подкачки (swap)

sudo swapoff -a
sudo sed -i '/swap/d' /etc/fstab

Почему: Подкачка позволяет ОС сбрасывать содержимое оперативной памяти на диск, когда RAM заканчивается. В общем случае это полезно, но подрывает управление ресурсами Kubernetes. Без подкачки контейнер, превысивший лимит памяти, немедленно получает OOMKill — это проще отлаживать и честнее по отношению к планировщику. В новых версиях Kubernetes (1.28+) поддержка подкачки есть, но правильная её настройка выходит за рамки руководства для начинающих.

Шаг 2. Загрузка необходимых модулей ядра

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

Зачем overlay? Containerd (среда выполнения контейнеров) использует драйвер файловой системы OverlayFS для эффективного наслоения образов контейнеров. Каждый образ состоит из набора слоёв только для чтения, поверх которых расположен тонкий слой с возможностью записи. Без модуля overlay containerd не может создавать контейнеры.

Зачем br_netfilter? Сетевая модель Kubernetes использует Linux-бриджи для соединения контейнеров. По умолчанию трафик, проходящий через бридж, обходит правила iptables. Модуль br_netfilter делает такой трафик видимым для iptables — это критически важно, поскольку сервисы Kubernetes (ClusterIP, NodePort) реализованы исключительно через правила iptables в kube-proxy.

Шаг 3. Настройка параметров sysctl

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

Это параметры времени выполнения, дополняющие загруженные выше модули ядра. bridge-nf-call-iptables активирует фильтрацию трафика, которую мы открыли модулем br_netfilter. ip_forward разрешает маршрутизацию пакетов между сетевыми интерфейсами. Без него под на одном узле не сможет общаться с подом на другом — ядро просто откажется пересылать такой трафик.

Фаза 4: Среда выполнения контейнеров

Kubernetes не запускает контейнеры напрямую — он делегирует это среде выполнения контейнеров (container runtime). Мы будем использовать containerd — отраслевой стандарт.

Выполните это на каждом узле:

sudo apt-get update && sudo apt-get install -y containerd

# Генерируем файл конфигурации по умолчанию
sudo mkdir -p /etc/containerd
containerd config default | sudo tee /etc/containerd/config.toml

# Включаем драйвер cgroup для systemd
sudo sed -i 's/SystemdCgroup = false/SystemdCgroup = true/' /etc/containerd/config.toml
sudo systemctl restart containerd
sudo systemctl enable containerd

Почему SystemdCgroup = true? Linux cgroups — это механизм применения ограничений ресурсов для контейнеров. Управлять cgroup-ами можно двумя способами: через устаревший драйвер cgroupfs или через драйвер systemd. Kubelet по умолчанию использует драйвер systemd, поэтому containerd должен использовать тот же самый.

Фаза 5: Установка Kubernetes

Шаг 1. Добавление репозитория Kubernetes (на всех узлах)

Пакеты Kubernetes не входят в стандартные репозитории Ubuntu. Нужно добавить официальный репозиторий pkgs.k8s.io.

# Импортируем ключ подписи — он позволяет apt убедиться,
# что пакеты действительно пришли от проекта Kubernetes и не были подменены
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

# Добавляем репозиторий
# Директива signed-by ограничивает область действия ключа только этим репозиторием,
# чтобы его нельзя было использовать для проверки пакетов из других источников
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

Шаг 2. Установка пакетов и закрепление версий (на всех узлах)

sudo apt-get update
sudo apt-get install -y kubelet kubeadm kubectl
sudo apt-mark hold kubelet kubeadm kubectl

Что делает каждый пакет:

kubelet — агент узла. Запускается на каждом узле как системный сервис, следит за API-сервером на предмет назначенных подов и даёт containerd команды запускать или останавливать контейнеры. Это мост между Kubernetes (уровнем абстрактной оркестрации) и containerd (тем, что непосредственно запускает контейнеры).

kubeadm — считайте его инсталлятором Kubernetes.

kubectl — CLI для взаимодействия с кластером.

Зачем apt-mark hold? Компоненты Kubernetes нужно обновлять в строго определённом порядке. Если apt-get upgrade молча обновит kubectl до v1.33, пока kubelet остаётся на v1.32, возникнет расхождение версий, способное привести к труднодиагностируемым проблемам.

Фаза 6: Инициализация кластера

На этом этапе четыре независимые Linux-машины становятся кластером Kubernetes.

Шаг 1. Инициализация управляющего узла (только Pi 1)

sudo kubeadm init \
  --apiserver-advertise-address=10.0.0.1 \
  --pod-network-cidr=10.244.0.0/16

--apiserver-advertise-address=10.0.0.1 указывает API-серверу привязаться к ethernet-интерфейсу Pi 1. Без этого флага kubeadm может автоматически выбрать Wi-Fi-интерфейс, и рабочие узлы (находящиеся в ethernet-подсети) не смогут достучаться до API-сервера.

--pod-network-cidr=10.244.0.0/16 резервирует этот диапазон IP для сети подов. Каждый под в кластере получит IP из этого диапазона. Важно, чтобы он не пересекался с подсетью узлов (10.0.0.0/24) и стандартным служебным CIDR Kubernetes (10.96.0.0/12). Диапазон 10.244.0.0/16 — это общепринятый стандарт для Flannel, CNI-плагина, который мы установим следующим шагом.

По завершении этой команды kubeadm выполнит несколько действий: сгенерирует все TLS-сертификаты в /etc/kubernetes/pki/, запишет статичные манифесты подов для etcd, API-сервера, controller manager и планировщика в /etc/kubernetes/manifests/, а затем запустит kubelet, который поднимет их все как контейнеры.

Сохраните команду kubeadm join из вывода — она понадобится на шаге 4 для добавления рабочих узлов.

Шаг 2. Настройка kubectl (Pi 1)

mkdir -p $HOME/.kube
sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
sudo chown $(id -u):$(id -g) $HOME/.kube/config

kubeadm init создал файл kubeconfig для администратора, содержащий клиентский сертификат и центр сертификации (CA) кластера. Эти команды копируют его в домашний каталог, чтобы можно было запускать kubectl без прав суперпользователя. Без этого каждая команда kubectl потребовала бы sudo.

Проверьте работу:

kubectl get nodes

Шаг 3. Установка CNI-плагина (Flannel)

CNI-плагин (Container Network Interface, интерфейс контейнерных сетей) отвечает за назначение IP-адресов подам и обеспечение связи между подами на разных узлах. Без него kubelet сообщает о состоянии узла как NotReady, поскольку требование к сети не выполнено.

kubectl apply -f https://github.com/flannel-io/flannel/releases/latest/download/kube-flannel.yml

Эта команда создаёт DaemonSet, который автоматически запускает по одному поду на каждом узле. По мере добавления рабочих узлов каждый из них получит под Flannel, настраивающий локальную сеть.

Flannel — один из самых простых CNI-плагинов. Он создаёт VXLAN-оверлейную сеть, которая туннелирует трафик подов между узлами поверх существующей ethernet-сети. Каждый узел получает подсеть /24 из диапазона 10.244.0.0/16, а Flannel берёт на себя маршрутизацию между ними.

Шаг 4. Подключение рабочих узлов (Pi 2, Pi 3, Pi 4)

Зайдите по SSH на каждый рабочий узел и выполните команду join из шага 1:

sudo kubeadm join 10.0.0.1:6443 \
  --token <your-token> \
  --discovery-token-ca-cert-hash sha256:<your-hash>

Что происходит при подключении: рабочий узел обращается к API-серверу по адресу 10.0.0.1:6443, проверяет TLS-сертификат сервера по указанному хешу, отправляет запрос на подпись сертификата (CSR), который управляющий узел автоматически одобряет, после чего запускает kubelet с подписанным сертификатом. Kubelet регистрирует узел в API-сервере, и Kubernetes начинает планировать на нём под Flannel DaemonSet.

Шаг 5. Проверка кластера

Вернитесь на Pi 1:

kubectl get nodes -o wide

Примерно через минуту (пока Flannel загружает образы контейнеров) все четыре узла должны отобразить статус Ready.

Убедитесь, что все системные поды работают:

kubectl get pods -A

Примечание: На управляющем узле есть пометка (taint) node-role.kubernetes.io/control-plane:NoSchedule, которая запрещает планировщику размещать на нём обычные рабочие нагрузки.

Что у вас теперь есть

Поздравляем! У вас работающий кластер Kubernetes. А именно:

Управляющий узел (Pi 1) с запущенными etcd, API-сервером, controller manager и планировщиком.

Три рабочих узла (Pi 2–4) с работающими kubelet и kube-proxy, готовые принимать нагрузки.

Оверлейная сеть (Flannel), дающая каждому поду уникальный IP и обеспечивающая маршрутизацию трафика между ними, в том числе через узловые границы.

DNS кластера (CoreDNS), благодаря которому поды могут находить друг друга по имени, а не по IP.

Что делать дальше

Этот кластер — отправная точка. Вот несколько идей для дальнейшего развития:

Добавьте metrics server. После этого станут доступны метрики узлов и подов через kubectl top nodes.

Разверните реальное приложение. Хорошим стартом будут такие проекты, как демо интернет-магазина от Amazon или демо микросервисов от Google.

Попробуйте что-нибудь сломать. Отключите узел командой kubectl drain, масштабируйте деплоймент до числа реплик, превышающего возможности кластера, установите ограничения ресурсов и понаблюдайте за OOMKill, удалите под и посмотрите, как его контроллер пересоздаёт.

Настройте Ingress-контроллер. Установите Traefik или ingress-nginx, чтобы направлять внешний HTTP-трафик в сервисы внутри кластера. Именно так production-кластеры открывают доступ к веб-приложениям снаружи.

Установите GitOps-инструмент. Такие инструменты, как ArgoCD, позволяют описывать желаемое состояние кластера в Git-репозитории и автоматически применять изменения.

Добавьте постоянное хранилище. Изучите local-path-provisioner или NFS, чтобы предоставить подам хранилище, которое переживает их перезапуск. Для stateful-нагрузок (баз данных, очередей сообщений) это необходимо.

Эпилог

Всё это может выглядеть пугающе, но на деле — обычный проект на выходные. Это самый близкий к настоящему практический опыт работы с Kubernetes, какой только можно получить. Каламбур намеренный. Удачи!

Логотип сообщества DEV
© 2026 meganuke