
Введение
Если вы когда-либо разворачивали Kubernetes-кластер на виртуальных машинах (ВМ), то знаете, насколько это может быть ресурсоёмко. Особенно это ощущается на одноплатных компьютерах вроде Orange Pi 5 Plus, даже несмотря на его 16 ГБ оперативной памяти. Для домашнего лабораторного стенда или лёгкой продакшн-инфраструктуры хочется чего-то более лёгкого и быстрого.
Здесь на помощь приходят контейнеры LXC. Они позволяют запускать полноценные окружения, почти как ВМ, но с гораздо меньшими накладными расходами. В этой статье я покажу, как развернуть кластер Kubernetes не на ВМ, а внутри LXC-контейнеров под управлением Proxmox.
На практике это означает, что в контейнеры LXC можно «упаковать» больше узлов Kubernetes, чем в ВМ, особенно на ограниченном по ресурсам железе.
Исходные условия
В распоряжении — компактный, но мощный одноплатный сервер Orange Pi 5 Plus с 16 ГБ оперативной памяти. На нём уже установлен гипервизор Proxmox VE, который обеспечивает удобную платформу для управления виртуализацией и развёртывания ВМ и контейнеров LXC.
В рамках этой статьи мы не будем углубляться в установку самого Proxmox — подобных инструкций в интернете предостаточно. Вместо этого сосредоточимся на следующем:
как создать базовый шаблон LXC-контейнера;
как подготовить его для работы с Kubernetes;
как развернуть кластер с двумя управляющими и тремя рабочими узлами;
как всё это реализовать в рамках домашней лаборатории — для обучения, тестирования или разработки.
Важно понимать, что полноценный отказоустойчивый кластер Kubernetes разворачивается минимум на трёх управляющих узлах. Это позволяет системе продолжать функционировать даже при выходе одного из них. По мере роста кластера можно добавлять больше управляющих узлов — например, пять или семь — чтобы обеспечить стабильность, масштабируемость и отказоустойчивость при увеличении нагрузки.
Тем не менее, для целей экспериментов и освоения технологии мы ограничимся двумя управляющими узлами, понимая, что такой кластер будет иметь ограниченную отказоустойчивость, но всё же будет приближен к боевому сценарию.
Шаг 1. Подготовка шаблона LXC
Перед созданием контейнеров убедимся, что на сервере Proxmox есть нужные шаблоны LXC-образов. В Proxmox это делается очень просто:
pveam update # обновляем репозиторий
pveam available # можем посмотреть список всех доступных образов

У меня архитектура arm64 (Orange Pi 5 Plus), поэтому я использовал подходящий шаблон Ubuntu:
pveam download local ubuntu-jammy-20231124_arm64.tar.xz
Скачать образ можно и в веб-интерфейсе proxmox.
Шаг 2. Создание LXC-контейнера
Контейнер можно создать и через GUI, но я предпочитаю CLI — так проще повторять и автоматизировать. А потом если что можно подкорректировать через GUI, кому как удобнее. Вот пример через CLI
pct create 2000 \
/var/lib/vz/template/cache/ubuntu-jammy-20231124_arm64.tar.xz \
--unprivileged 0 \
--features nesting=1 \
--hostname k8s-node \
--cores 2 \
--memory 4096 \
--swap 0 \
--arch arm64 \
--net0 name=eth0,bridge=vmbr0,ip=192.168.1.20/24,gw=192.168.1.1 \
--storage local \
--rootfs local:20 \
--nameserver 192.168.1.1 \
--password your_root_password_here \
--ssh-public-keys /path/to/your/public_key.pub
Детализация параметров:
ID контейнера (2000)
Уникальный числовой идентификатор контейнера в кластере Proxmox.
Шаблон ОС
Путь к предварительно загруженному образу Ubuntu (Jammy) для ARM64.
--unprivileged 0
Контейнер работает с правами root (не изолирован). Необходимо для некоторых системных операций.
--features nesting=1
Активирует поддержку вложенной виртуализации (например, для запуска Docker/Kubernetes).
--hostname
Сетевое имя ноды в кластере. Должно быть уникальным в пределах сети.
--cores, --memory, --swap
Ресурсы 2 ядра CPU 4 ГБ RAM Swap отключен (рекомендуется для K8s)
--arch arm64
Явное указание архитектуры контейнера (должно совпадать с шаблоном и хостовой системой).
--net0
Сеть интерфейс eth0, подключение к мосту vmbr0, статический IPv4: 192.168.1.20/24 Шлюз: 192.168.1.1
--storage
Используется стандартное хранилище Proxmox
--rootfs
20 ГБ дискового пространства
--nameserver
Указание DNS-сервера (часто совпадает с шлюзом в домашних сетях).
--password
Пароль root (не рекомендуется для production — лучше использовать ключи SSH)
--ssh-public-keys
Публичный SSH-ключ для безопасного доступа.
Шаг 3. Настройка конфигурации контейнера
Перед первым запуском контейнера нужно отредактировать его конфигурационный файл, чтобы обеспечить совместимость с kubelet. Конфиги LXC лежат по пути:
vim /etc/pve/lxc/2000.conf
Добавляем в конец файла:
lxc.apparmor.profile: unconfined
lxc.cap.drop:
lxc.cgroup.devices.allow: a
lxc.mount.auto: proc:rw sys:rw
Эти параметры снимают часть ограничений безопасности, которые мешают kubelet работать корректно.
Еще нужно проверить установку модулей на хосте proxmox
lsmod | grep overlay
lsmod | grep br_netfilter
Если их нет в ядре, то нужно установить командами
modprobe overlay
modprobe br_netfilter
Шаг 4. Установка SSH-сервера
Контейнер создан, теперь его можно запустить:
pct start 2000
Переходим в его консоль и обновляем пакеты:
pct enter 2000
далее обновляем пакеты:
apt update
apt install -y openssh-server
Теперь можно подключаться к контейнеру через SSH, что особенно удобно при настройке кластера.
Шаг 5. Подготовка контейнера к работе с Kubernetes
Ubuntu в контейнере достаточно «чистая». Чтобы kubelet корректно запускался, необходимо добавить поддержку /dev/kmsg:
ln -s /dev/console /dev/kmsg
echo 'L /dev/kmsg - - - - /dev/console' > /etc/tmpfiles.d/kmsg.conf
Это обходной путь, потому что в LXC нет настоящего /dev/kmsg, но Kubernetes его ожидает.
Еще нужно разрешить маршрутизацию ip-трафика в ноде, выполнив команды:
echo -e "net.bridge.bridge-nf-call-ip6tables = 1\nnet.bridge.bridge-nf-call-iptables = 1\nnet.ipv4.ip_forward = 1" > /etc/sysctl.d/10-k8s.conf
sysctl -f /etc/sysctl.d/10-k8s.conf
Отключить swap можно командами, это делать если не сделали при создании контейнера
swapoff -a
sed -i '/ swap / s/^/#/' /etc/fstab
Для проверки отключения файла подкачки выполним команду:
swapon -s
Для проверки успешности изменения настроек в параметрах сетевого стека выполним команду:
sysctl net.bridge.bridge-nf-call-iptables net.bridge.bridge-nf-call-ip6tables net.ipv4.ip_forward
Шаг 6. Контейнерный runtime.
На момент настройки я не стал использовать docker, так как Kubernetes официально отказался от его поддержки, начиная с версии 1.24. Теоретически, можно было бы использовать cri-dockerd, но это лишняя прослойка, и я решил пойти более прямым путём — попробовать containerd и cri-o. Испытал оба рантайма на контейнерах LXC, оба работают нормально.
Здесь приведу пример как я настраивал containerd, а далее как настраивал cri-o. Нужно установить один из них на ваш выбор.
6.1 Устанавливаем containerd.
Все можно сделать по инструкции здесь
Если лень выполнять каждый пункт вот написал скрипт нужно только выбрать архитектуру, скрипт установит последние стабильные версии
Первым делом скачиваем бинари бинари выберите для своей архитектуры.
wget https://github.com/containerd/containerd/releases/download/v2.0.4/containerd-2.0.4-linux-arm64.tar.gz
Распаковываем
tar Cxzvf /usr/local containerd-1.6.2-linux-amd64.tar.gz
Скачаем файл для юнит
wget https://raw.githubusercontent.com/containerd/containerd/main/containerd.service
Перед использованием юнит-файла, нужно закомментировать одну строку. Зайдите редактором и закомментируйте строку ExecStartPre=-/sbin/modprobe overlay или удалите ее
mv containerd.service /usr/lib/systemd/system/containerd.service
systemctl daemon-reload
systemctl enable --now containerd
Скачаем runc для своей архитектуры здесь
wget https://github.com/opencontainers/runc/releases/download/v1.2.6/runc.arm64
Инсталлируем, выполнив команду
install -m 755 runc.amd64 /usr/local/sbin/runc
Установим сетевые плагины CNI, выбираем для своей архитектуры здесь
wget https://github.com/containernetworking/plugins/releases/download/v1.6.2/cni-plugins-linux-arm64-v1.6.2.tgz
Создадим директорию для бинарей
mkdir -p /opt/cni/bin
И туда распакуем архив
tar Cxzvf /opt/cni/bin cni-plugins-linux-amd64-v1.1.1.tgz
Теперь можно сделать так
systemctl start containerd.service
6.2 Установка cri-o
Установить можно по инструкции из официальной документации здесь
Здесь опишу как устанавливал я. Установим переменные:
export KUBERNETES_VERSION=v1.32
export CRIO_VERSION=v1.32
Если нет в системе, установим это
apt-get install -y software-properties-common
Пакет software-properties-common в системах на базе Debian/Ubuntu предоставляет набор инструментов для удобного управления репозиториями и источниками пакетов (PPA). Основная причина установки этого пакета — доступ к команде add-apt-repository, которая позволяет Добавлять PPA-репозитории (Personal Package Archives) для установки актуальных версий ПО. А так же управление ключами GPG
Дальше скачиваем ключ
curl -fsSL https://download.opensuse.org/repositories/isv:/cri-o:/stable:/$CRIO_VERSION/deb/Release.key | gpg --dearmor -o /etc/apt/keyrings/cri-o-apt-keyring.gpg
Добавляем репозиторий
echo "deb [signed-by=/etc/apt/keyrings/cri-o-apt-keyring.gpg] https://download.opensuse.org/repositories/isv:/cri-o:/stable:/$CRIO_VERSION/deb/ /" | tee /etc/apt/sources.list.d/cri-o.list
Обновляем пакетный менеджер и устанавливаем cri-o
apt-get update
apt-get install -y cri-o
Перегружаем демоны systemd и включаем cri-o в автозагрузку
systemctl daemon-reload
systemctl enable --now crio.service
Шаг 7. Установка kubeadm, kubectl, kubelet
Для развертывания кластера Kubernetes вручную на базе LXC-контейнеров (или физических/виртуальных машин) нам необходимо установить три ключевых компонента:
kubeadm — это официальная утилита от Kubernetes, предназначенная для быстрого и безопасного развертывания и настройки кластеров.
Она автоматизирует множество рутинных операций:
генерацию необходимых ключей и сертификатов;
установку и настройку control-plane компонентов (kube-apiserver, kube-controller-manager, kube-scheduler);
создание необходимых конфигураций;
регистрацию worker-нод в кластер.
При этом kubeadm не управляет кластером после развёртывания — он лишь выполняет его инициализацию. В дальнейшем для управления используется kubectl.
kubectl — основная командная утилита (CLI) для взаимодействия с кластером Kubernetes.
С её помощью можно:
просматривать и управлять подами, сервисами, деплойментами, namespace’ами и другими объектами;
выполнять команды внутри подов (kubectl exec);
следить за логами (kubectl logs);
применять манифесты (kubectl apply -f ...);
масштабировать приложения;
обновлять ресурсы и многое другое.
Эта утилита устанавливается на любой машине, с которой предполагается управление кластером. Обычно это master-нода или отдельная рабочая станция.
kubelet — это агент, который должен работать на каждой ноде (и master, и worker).
Он:
следит за запуском, работой и завершением подов;
взаимодействует с контейнерным рантаймом (в нашем случае это CRI-O);
опрашивает kube-apiserver, чтобы получить инструкции, какие поды должны быть запущены на узле;
сообщает состояние подов и ноды обратно в кластер;
следит за healthy-пробами, перезапускает контейнеры при падении и т.д.
Kubelet не управляет сам по себе контейнерами напрямую — он взаимодействует с ними через CRI (Container Runtime Interface).
Выполняем установку компонентов согласно официальной документации Kubernetes.
Установим дополнения, если нет в системе
apt-get install -y apt-transport-https ca-certificates curl gpg
Скачаем и установим ключ репозитория
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
Обновим менеджер пакетов и установим kubelet kubeadm kubectl
apt-get update
apt-get install -y kubelet kubeadm kubectl
Чтобы избежать непредвиденного обновления этих пакетов при apt upgrade, закрепим их версии:
apt-mark hold kubelet kubeadm kubectl
sudo systemctl enable --now kubelet
На этом подготовка контейнера завершена. Терь из него можно сделать шаблон, и потом на его основе создавать контейнер уже готовый для развертывания в нем кластера kubernetes
На хосте proxmox выполните команды
pct stop 2000
pct template 2000
Мы получили шаблон контейнера с установленным ПО. На его основе можно собрать кластер kubernetes. Можно автоматизировать все в terraform или ansible.
Для примера приведу код terraform. Провайдер использовал telmate/proxmox
Как настраивать провайдер и переменные для него можно посмотреть в документации
Показать/скрыть конфигурацию Terraform ▼
variable "kube_count" {
description = "Number of LXC to create"
default = 5
}
resource "proxmox_lxc" "kubernetes" {
count = var.kube_count
target_node = "pimox2"
hostname = format("k8s-node-%02d", 1 + count.index)
arch = "arm64"
clone = "2000"
full = true
password = ""
cores = 4
memory = 4096
start = true
vmid = format("44%02d", count.index+21)
ssh_public_keys = <<-EOT
ssh-ed25519 AA*****@Macmini.local
ssh-ed25519 AAA*************@MacBookAir.local
EOT
rootfs {
storage = "local"
size = count.index < 2 ? "20G" : "50G"
}
network {
name = "eth0"
bridge = "vmbr0"
ip = format("192.168.1.%d/24", count.index + 21)
gw = "192.168.1.1"
type = "veth"
}
}
Для своей домашней лаборатории соберу кластер из 5 нод, две мастер ноды и три воркер ноды. Знаю что для высоко доступного кластера нужно минимум три мастер ноды, но для экспериментов хватит и двух.
Шаг 8. Развёртывание отказоустойчивого кластера Kubernetes
На этом этапе мы перейдём к развертыванию полноценного кластера Kubernetes с высокой доступностью. В моём случае в качестве управляющих (мастер) узлов участвуют две виртуальные машины: kube-master1 и kube-master2.
Отказоустойчивость в Kubernetes: в чём суть
Отказоустойчивость — ключевая особенность Kubernetes, и она реализуется на двух уровнях:
Рабочие нагрузки (Pods)
Управление кластером (Control Plane)
1. Отказоустойчивость рабочих нагрузок
Kubernetes не просто запускает контейнеры — он управляет их жизненным циклом. Вместо ручного запуска контейнеров, как в Docker, администратор описывает желаемое состояние: сколько экземпляров приложения должно быть, какие ресурсы требуются, какие ограничения, и так далее.
Контроллеры Kubernetes (например, Deployment) следят за тем, чтобы желаемое состояние совпадало с фактическим. Если по какой-то причине приложение (Pod) упало или узел, на котором оно работало, вышел из строя — Kubernetes обнаружит это и перезапустит приложение на другом доступном узле. Всё это происходит автоматически и практически без участия пользователя.
2. Отказоустойчивость управляющих узлов
Управляющий узел — это мозг кластера. Он принимает решения, где и что запускать, и поддерживает общее состояние кластера. Его высокая доступность критична. Она обеспечивается за счёт:
Распределённого хранилища etcd: Конфигурация кластера хранится в нескольких копиях на разных узлах. Если один узел с etcd выйдет из строя, данные не будут потеряны.
Балансировщика нагрузки (load balancer): Он обеспечивает доступ к API Kubernetes даже в случае отказа одного из управляющих узлов. Все управляющие узлы — равноправны, и через балансировщик можно обратиться к любому доступному.
Балансировщик нагрузки
Для отказоустойчивого доступа к API Kubernetes нам потребуется внешний компонент, который будет балансировать трафик между kube-master1 и kube-master2. Я использую классическую связку:
Keepalived — обеспечивает работу виртуального IP-адреса, который будет автоматически перекидываться между kube-master1 и kube-master2, если один из них выйдет из строя.
HAProxy — реверс-прокси, который принимает запросы на виртуальный IP и равномерно распределяет их между работающими API-серверами Kubernetes.
В результате:
Клиент или компонент кластера всегда обращается к одному IP-адресу — виртуальному.
Keepalived следит за здоровьем узлов и перемещает IP на живой мастер при отказе.
HAProxy на каждом мастере перенаправляет запросы на локальный API-сервер или — при необходимости — на другой мастер.
На мастер нодах выполним установку и настройку:
apt install -y keepalived haproxy
И так, создаем файл настроек /etc/keepalived/keepalived.conf
mkdir -p /etc/keepalived && sudo tee /etc/keepalived/keepalived.conf << 'EOF' >/dev/null
global_defs {
enable_script_security
script_user nobody
}
vrrp_script check_apiserver {
script "/etc/keepalived/check_apiserver.sh"
interval 3
}
vrrp_instance VI_1 {
state BACKUP
interface ens33
virtual_router_id 5
priority 100
advert_int 1
nopreempt
authentication {
auth_type PASS
auth_pass qwerty
}
virtual_ipaddress {
192.168.1.20
}
track_script {
check_apiserver
}
}
EOF
Вместо ens33 нужно прописать имя адаптера LXC контейнера, а в virtual_ipaddress прописать свободный ip адрес из своей подсети. Далее создадим скрипт /etc/keepalived/check_apiserver.sh Скрипт предназначен для проверки доступности серверов
#!/bin/sh
APISERVER_VIP=192.168.1.20
APISERVER_DEST_PORT=3636
PROTO=http
errorExit() {
echo "*** $*" 1>&2
exit 1
}
curl --silent --max-time 2 --insecure ${PROTO}://localhost:${APISERVER_DEST_PORT}/ -o /dev/null || errorExit "Error GET ${PROTO}://localhost:${APISERVER_DEST_PORT}/"
if ip addr | grep -q ${APISERVER_VIP}; then
curl --silent --max-time 2 --insecure ${PROTO}://${APISERVER_VIP}:${APISERVER_DEST_PORT}/ -o /dev/null || errorExit "Error GET ${PROTO}://${APISERVER_VIP}:${APISERVER_DEST_PORT}/"
fi
Поменяем атрибуты скрипта, сделаем его запускаемым, включаем в автозагрузку
chmod +x /etc/keepalived/check_apiserver.sh
systemctl enable keepalived
systemctl start keepalived
Файл настроек haproxy /etc/haproxy/haproxy.cfg откроем удалим все и вставим следующее содержимое
Показать/скрыть файл ▼
# Global settings
#---------------------------------------------------------------------
global
log /dev/log local0 info alert
log /dev/log local1 notice alert
daemon
#---------------------------------------------------------------------
common defaults that all the 'listen' and 'backend' sections will
use if not designated in their block
#---------------------------------------------------------------------
defaults
mode http
log global
option httplog
option dontlognull
option http-server-close
option forwardfor except 127.0.0.0/8
option redispatch
retries 1
timeout http-request 10s
timeout queue 20s
timeout connect 5s
timeout client 20s
timeout server 20s
timeout http-keep-alive 10s
timeout check 10s
#---------------------------------------------------------------------
apiserver frontend which proxys to the control plane nodes
#---------------------------------------------------------------------
frontend apiserver
bind *:3636
mode tcp
option tcplog
default_backend apiserver
#---------------------------------------------------------------------
round robin balancing for apiserver
#---------------------------------------------------------------------
backend apiserver
option httpchk GET /healthz
http-check expect status 200
mode tcp
option ssl-hello-chk
balance roundrobin
server node1 192.168.1.21:6443 check
server node2 192.168.1.22:6443 check
Файл нужно подкорректировать в соответствии с вашими настройками, в поле от server node1 до server nodeN нужно прописать ip адреса мастер нод.
Запустим демона haproxy
systemctl enable haproxy
systemctl restart haproxy
Следующим шагом запустим первый управляющий узел
kubeadm init --pod-network-cidr=10.244.0.0/16 --control-plane-endpoint "192.168.1.20:3636" --upload-certs
После успешного выполнения команды kubeadm init, в консоли появится сообщение:

Это означает, что кластер успешно инициализирован, и мастер-узел готов к работе. Но чтобы начать управлять кластером через команду kubectl, нужно указать, где взять конфигурационный файл — admin.conf.
Что такое admin.conf и зачем он нужен?
Файл admin.conf содержит:
Информацию о кластере (куда подключаться).
Данные для аутентификации (сертификаты и ключи).
Контекст пользователя (от имени кого выполняются команды).
По сути, это паспорт администратора кластера, с помощью которого kubectl знает, куда обращаться и с какими правами.
Нужно создать папку .kube в домашнем каталоге и скопировать туда файл admin.conf под именем config. Команды:
mkdir -p $HOME/.kube
sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
sudo chown $(id -u):$(id -g) $HOME/.kube/config
После этого можно использовать kubectl без дополнительных параметров, например:
kubectl get nodes
Если работать на удаленной машине по ssh то нужно создать папку .kube в домашнем каталоге на своем компьютере и скопировать туда файл admin.conf под именем config.
Далее в выводе после успешной установки должна появится строка для добавления управляющих узлов. Здесь приведу для примера свою строку, у вас она будет другая
kubeadm join 192.168.1.20:3636 --token oyo60o.5eny6qtz3p1h522w \
--discovery-token-ca-cert-hash sha256:6776fc689078fad601927df097b20b0f316f22d42187e8466a63a304f70d8f93 \
--control-plane --certificate-key 791aef224ba82012fc2240232d83061fbd9ba5815d7f6ec8795c85233d450f02
Ее нужно скопировать и запустить на второй управляющей ноде, после этого вторая нода должна появится в кластере.
Для добавления рабочих узлов будет другая строка, примерное содержание будет таким
kubeadm join 192.168.1.30:3636 --token oyo60o.5eny6qtz3p1h522w \
--discovery-token-ca-cert-hash sha256:6776fc689078fad601927df097b20b0f316f22d42187e8466a63a304f70d8f93
Ее нужно запустить на всех рабочих нодах.
И в конце нужно установить сетевой плагин. На управляющей ноде запустить команду
kubectl apply -f https://github.com/flannel-io/flannel/releases/latest/download/kube-flannel.yml
Кластер готов к употреблению

Заключение
Развёртывание Kubernetes-кластера в LXC-контейнерах на базе гипервизора Proxmox — это отличный способ создать компактную, гибкую и производительную лабораторию для тестирования, обучения или разработки. Использование лёгких контейнеров LXC позволяет экономно расходовать ресурсы, а Proxmox обеспечивает удобный интерфейс и продвинутые возможности управления виртуализацией.
Мы прошли путь от подготовки шаблона контейнера до развёртывания полноценного кластера с несколькими управляющими и рабочими узлами. Такой подход легко масштабируется: при необходимости можно добавлять новые узлы, расширять конфигурацию, подключать хранилища и инструменты мониторинга.
Благодаря использованию Terraform и автоматизации развёртывания вы получаете воспроизводимую инфраструктуру, которую можно быстро развернуть заново или адаптировать под новые задачи. Это особенно ценно в условиях быстроменяющейся среды разработки и постоянного эксперимента.
Всё это делает Proxmox + LXC идеальной платформой для построения домашних и экспериментальных Kubernetes-кластеров.
P.S.
Это моя первая публикация — буду рад конструктивной критике, замечаниям или предложениям по улучшению. Пусть мой опыт станет небольшой опорой в вашему пути по миру Kubernetes 🙌