Возможно, у вас была мысль запустить собственную услугу Infrastructure as a Service (IaaS) либо разместить свои проекты на Kubernetes, но не платить за managed-услуги, или ваш проект — это и есть self-managed Kubernetes. У full-stack-разработчика и предпринимателя Нейта Бакареффа (Nate Buckareff) эти желания сошлись воедино. Он специализируется на консультации по веб-разработке — помогает людям воплощать потребности бизнеса в технические решения, сохраняя при этом внимание к клиенту.
Нейт решил научиться развертывать собственный кластер на виртуальных выделенных серверах, о чем и рассказал в своем блоге. Эта статья является переводом его материала. Далее идет текст от автора.
Сначала мне казалось, что установка Kubernetes будет самым сложным этапом. Но благодаря проектам вроде k0s это оказалось довольно просто. Настоящая же проблема была в другом: для работы с cloud-init, QEMU, виртуальными сетевыми интерфейсами Linux и HAProxy мне пришлось изучить немало источников.
Пока мы не приступили к настройке кластера, хочу предупредить, что его не стоит использовать для production. Установить Kubernetes легко, но обслуживать кластер и поддерживать его в актуальном состоянии труднее. Мой совет всем, кто рассматривает Kubernetes в качестве production-системы: вам придется либо стать экспертом по Kubernetes, либо нанять эксперта, либо выбрать какой-то managed-сервис. Также рекомендую использовать Docker Compose до тех пор, пока рабочая нагрузка помещается на одном сервере.
В этой статье мы рассмотрим, как развернуть локальный ВМ-кластер с помощью QEMU, настроить виртуальную сеть с помощью команды ip
, установить Kubernetes (k0s), настроить Ingress-контроллер, а также HAProxy в качестве балансировщика нагрузки. Предполагается, что вы используете Linux.
Локальное тестовое окружение
Мы будем создавать локальные виртуальные машины (ВМ) с помощью QEMU. Изначально я хотел использовать VPS (виртуальный частный сервер), но локальный вариант больше подходит для экспериментов. В зависимости от целей такая установка — хорошая отправная точка для развертывания CI/CD-системы или локальной среды разработки.
Так выглядит высокоуровневая архитектурная схема того, что мы будем строить:
Всего у нас будет четыре ВМ на базе QEMU в отдельной приватной сети /24
. Три ВМ мы используем в качестве узлов Kubernetes, а на четвертой запустим балансировщик нагрузки HAProxy. На каждом узле будет виртуальное TAP-устройство, подключенное к виртуальному мосту br0
. Демон dnsmasq, запущенный на хосте, будет назначать IP-адреса узлам на основе их MAC-адресов.
Я использую QEMU, но подойдет и другое ПО для виртуализации в Linux. Например, VirtualBox или VMWare. Еще есть проект libvirt, который абстрагируется от различных бэкендов виртуализации (включая QEMU). Можно использовать его.
Приватная сеть выступает в роли эмулированного VPC. Правила iptables позволяют узлам выходить во внешнюю сеть.
Каждый узел получает предопределенный MAC-адрес. Далее dnsmasq будет назначать соответствующий IP-адрес по MAC-адресу клиента.
Также можно использовать cloud-init для статического назначения IP-адресов, но тогда потребуется создавать свой seed ISO для каждого узла.
Узел HAProxy отвечает за обратное проксирование внешнего трафика на узлы Kubernetes.
Debian на QEMU с cloud-init
Для ВМ мы будем использовать Debian 12 в виде типового облачного образа. Образ оптимизирован для виртуальных машин — из него исключены драйверы для реального аппаратного обеспечения. Это не обязательно, но в дальнейшем может пригодиться, если развертывать собственные ВМ на bare-metal-серверах.
Загружаем последнюю версию типового облачного образа Debian 12:
wget -O debian12.qcow2 https://cloud.debian.org/images/cloud/bookworm/latest/debian-12-genericcloud-amd64.qcow2
Минус этого образа — в нем нет учетной записи пользователя по умолчанию или пароля root
. Чтобы войти в систему, настроим ее с помощью сценария первоначальной настройки cloud-init. Для этого используем seed ISO. По сути, именно это облачный провайдер делает каждый раз при запуске нового VPS.
Получается, чтобы создать новую ВМ, мы клонируем базовый образ диска. Затем загружаем ВМ с помощью нового образа диска и seed ISO. Если настройки ВМ отличаются, мы сразу генерируем новый seed ISO. В нашем случае во все ВМ сразу добавим открытый ключ SSH, чтобы на них можно было удаленно заходить с терминала по протоколу SSH.
Для создания seed ISO-файла воспользуемся инструментом cloud-localds. Пользователям Ubuntu необходимо установить пакет cloud-image-utils:
sudo apt install cloud-image-utils
Теперь создадим cloud-конфиг cloud_init.yaml:
#cloud-config
users:
- name: root
ssh-authorized-keys:
- <ВАШ_ПУБЛИЧНЫЙ_КЛЮЧ_SSH>
Подставим в него свой публичный ключ и запустим cloud-localds:
cloud-localds -v seed.iso cloud_init.yaml
Должен появиться ISO-файл с именем seed.iso:
fileseed.iso
seed.iso: ISO 9660 CD-ROM filesystem data 'cidata'
Далее скопируем загруженный базовый образ диска:
cp ./debian12.qcow2 ./test.qcow2
Теперь можно загрузить ВМ с помощью образа диска и seed ISO:
sudo qemu-system-x86_64 \
-m 2G \
-smp 2 \
-device virtio-net-pci,netdev=net0 \
-netdev user,id=net0,hostfwd=tcp::2222-:22 \
-drive file=./test.qcow2,if=virtio,cache=writeback,discard=ignore,format=qcow2 \
-drive file=./seed.iso,media=cdrom \
-boot d \
-serial stdio \
-machine type=pc,accel=kvm
У QEMU много флагов, поэтому изучим эту команду подробнее:
qemu-system-x86_64
— QEMU использует разные исполняемые файлы для архитектур гостевых систем. Например, здесь мы указываем тип гостевой системы x86_64.-m 2G
— объем оперативной памяти.-smp 2
— число ядер CPU (vCPU).-device virtio-net-pci,netdev=net0
— создание виртуальной карты сетевого интерфейса с net0 в качестве сетевого бэкенда. В QEMU сетевые устройства и сетевые бэкенды — разные вещи. Устройство — это то, что видит гостевая система, а бэкенд — это то, что подключается к окружению хоста.-netdev user,id=net0,hostfwd=tcp::2222-:22
— создание сетевого бэкенда с именем net0. Мы используем тип бэкенда user networking (или SLIRP) и перенаправляем трафик с порта2222
на хосте на порт22
на госте.-drive file=./test.qcow2,...
— создание виртуального диска на базе клонированного образа Debian.-drive file=./seed.iso,...
— создание виртуального CD-ROM со вставленным seed ISO.-boot d
— загрузка ВМ с CD-ROM. В нашем случае загружается seed ISO, который настраивает систему.-serial stdio
— перенаправление виртуальной консоли в stdout нашего терминала для облегчения отладки. Это делается, чтобы можно было прокручивать вывод назад.-machine type=pc,accel=kvm
— предписание QEMU использовать ускорение технологии KVM. Это позволяет задействовать встроенную поддержку виртуализации на хосте с помощью KVM-модуля ядра.
После выполнения этой команды откроется графический интерфейс QEMU и начнется загрузка виртуальной машины. Мы должны увидеть вывод виртуальной консоли в графическом интерфейсе и терминале. Здесь нужно обратить внимание на логи cloud-init и ci-info — в одном из сообщений будет упомянут заданный в конфигурации публичный ключ.
После завершения загрузки ВМ можно войти в систему по SSH на порту 2222
:
ssh root@localhost -p 2222
В итоге мы успешно создали пробную виртуальную машину. Теперь пора настроить виртуальную сеть и создать ВМ для каждого из четырех узлов.
Виртуальная сеть
Напомним, что пробная ВМ, которую мы только что создали, использует тип бэкенда user networking. Этот бэкенд отлично подходит для тестирования, но отличается низкой производительностью и изолирует каждую ВМ в отдельной сети. Мы же хотим создать кластер виртуальных машин, которые работают в общей виртуальной сети, поэтому этот сетевой бэкенд нам не подходит.
Еще раз посмотрим на схему:
В ней четыре основных сетевых компонента:
Мост
br0
выступает в роли виртуального коммутатора, к которому подключаются все наши виртуальные машины, образуя виртуальную локальную сеть.К каждой ВМ подключено свое TAP-устройство. Затем TAP-устройство «включается» в мост и пересылает пакеты через виртуальную карту сетевого интерфейса виртуальной машины.
Демон dnsmasq, который выполняет функции сервера DHCP, назначает узлам IP-адреса.
Правило iptables, которое превращает
br0
в NAT-маршрутизатор, позволяя узлам выходить в интернет.
Создадим устройство виртуального моста с помощью команды ip
, присвоим ему IP-адрес 10.0.0.0/24
и запустим:
# Создаем мост.
sudo ip link add br0 type bridge
# Назначаем адрес интерфейсу. Если этого не сделать, впоследствии dnsmasq будет жаловаться, что интерфейс, к которому он привязан, "не имеет адреса".
sudo ip addr add 10.0.0.0/24 dev br0
# Поднимаем мост.
sudo ip link set up dev br0
Убедимся, что устройство создано:
ip -d link show br0
Должно получиться нечто подобное:
159: br0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP mode DEFAULT group default qlen 1000
link/ether 6a:09:53:74:75:05 brd ff:ff:ff:ff:ff:ff promiscuity 0 allmulti 0 minmtu 68 maxmtu 65535
bridge [...]
Далее нужно запустить TAP-устройство для каждого из четырех узлов, которые мы будем создавать:
for NODE_NAME in node0 node1 node2 node3; do
echo "${NODE_NAME}tap"
# Создаем устройство TAP.
sudo ip tuntap add "${NODE_NAME}tap" mode tap
# Запускаем его.
sudo ip link set "${NODE_NAME}tap" up
# Подключаем его к мосту.
sudo ip link set "${NODE_NAME}tap" master br0
done
Имена интерфейсов, которые выводятся через echo
, понадобятся нам позже, когда мы будем настраивать сетевые бэкенды ВМ.
Снова убедимся, что устройства TAP созданы:
ip -d link show node0tap
Вывод должен выглядеть примерно так:
160: node0tap: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast master br0 state UP mode DEFAULT group default qlen 1000
link/ether e6:7d:17:30:0d:e5 brd ff:ff:ff:ff:ff:ff promiscuity 1 allmulti 1 minmtu 68 maxmtu 65521
tun type tap pi off vnet_hdr on persist on
bridge_slave [...]
Далее настроим dnsmasq на запуск демоном на хосте. Он будет выступать DHCP-сервером и сетевым шлюзом для наших узлов.
Вот файл dnsmasq.conf:
# Привязываем к мосту.
interface=br0
bind-interfaces
# Игнорируем /etc/resolv.conf и /etc/hosts хоста.
no-resolv
no-hosts
# Перенаправляем DNS-запросы на публичный DNS-резольвер.
domain-needed
bogus-priv
server=8.8.8.8
server=8.8.4.4
# Задаем диапазон адресов.
dhcp-range=10.0.0.0,10.0.0.255
# Предоставляем эти IP-адреса узлам с конкретными MAC-адресами.
dhcp-host=86:e2:e3:21:13:b4,10.0.0.10,node0
dhcp-host=7c:92:6e:84:1f:50,10.0.0.11,node1
dhcp-host=b0:aa:4c:4c:7b:1a,10.0.0.12,node2
dhcp-host=a6:fc:4a:66:8f:c9,10.0.0.13,node3
Здесь наиболее важны следующие три части:
Привязываем dnsmasq к мосту.
Перенаправляем DNS-запросы на
8.8.8.8
и8.8.4.4
.Присваиваем IP-адреса нашим ВМ на основе их MAC-адресов.
Теперь запускаем демон dnsmasq:
sudo dnsmasq \
--no-daemon --conf-file=./dnsmasq.conf \
> dnsmasq.log 2>&1 &
echo $! > dnsmasq.pid
В результате dnsmasq запустится в фоне, весь его вывод будет перенаправляться в dnsmasq.log, а PID запишется в dnsmasq.pid. Потом по этому PID’у мы сможем «убить» демона.
Последнее, что нужно сделать, — применить правило iptables, чтобы узлы могли выходить в интернет:
sudo iptables \
-t nat -A POSTROUTING ! -o br0 \
--source 10.0.0.0/24 -j MASQUERADE
Без него мы сможем связаться с узлами с хоста, например, зайти на них по SSH, но сами узлы не смогут выйти в локальную сеть или интернет.
После завершения работы правило можно удалить с помощью флага -D
:
sudo iptables \
-t nat -D POSTROUTING ! -o br0 \
--source 10.0.0.0/24 -j MASQUERADE
Теперь мы готовы создать виртуальные машины и запустить кластер Kubernetes.
Бутстрап узлов
Виртуальная сеть у нас есть. Пришло время создать сами узлы. Сначала клонируем базовый образ для каждого узла и увеличим его размер до 10 ГБ:
for NODE_NAME in node0 node1 node2 node3; do
# Клонируем образ.
cp ./debian12.qcow2 "./${NODE_NAME}.qcow2"
# Меняем его размер.
qemu-img resize "./${NODE_NAME}.qcow2" 10G
done
Образы дисков на самом деле не занимают 10 ГБ — файлы в формате qcow2 увеличивают свой размер по необходимости.
Далее с помощью уже знакомой команды qemu-system-x86_64
создаем первый узел node0
:
NODE_NAME="node0"
NODE_MAC="86:e2:e3:21:13:b4"
NODE_DISK="./${NODE_NAME}.qcow2"
sudo qemu-system-x86_64 \
-name "$NODE_NAME" \
-m 2G \
-smp 2 \
-device "virtio-net-pci,netdev=${NODE_NAME}tap,mac=${NODE_MAC}" \
-netdev "tap,id=${NODE_NAME}tap,ifname=${NODE_NAME}tap,script=no" \
-drive "file=./${NODE_DISK},if=virtio,cache=writeback,discard=ignore,format=qcow2" \
-drive "file=./seed.iso,media=cdrom" \
-boot d \
-machine type=pc,accel=kvm &
Я закинул некоторые значения в переменные окружения, чтобы было проще понять, что происходит. Это похоже на то, что мы раньше делали для пробной ВМ:
-device
— явно задает MAC-адрес сетевого интерфейса. Ориентируясь на него, демон dnsmasq навесит на этот узел IP-адрес10.0.0.10
.-netdev
— задает тип сетевого бэкенда (tap) и ссылается на имя TAP-устройства node0tap, созданного ранее.script=no
— запрещает QEMU автоматически настраивать дополнительные сетевые устройства на хосте. Опцияscript
по умолчанию имеет значениеscript=yes
, то есть QEMU будет пытаться автоматически настроить мост при использовании сетевого бэкенда tap. Поскольку мост уже создан, ее необходимо отключить.-drive
— создает два виртуальных диска, но на этот раз используется клонированный образ.
Процесс запускается в фоновом режиме (&) — можно продолжить создавать ВМ в том же окне терминала. Если что-то пойдет не так, можно добавить опцию -serial stdio
и запустить каждую ВМ в своем терминале, чтобы изучить вывод виртуальной консоли.
Виртуальная машина начинает загружаться. В логе dnsmasq.log должна появиться запись о навешивании IP-адреса на узел:
$ watch -n0.1 cat ./dnsmasq.log
...
dnsmasq-dhcp: DHCPDISCOVER(br0) 86:e2:e3:21:13:b4
dnsmasq-dhcp: DHCPOFFER(br0) 10.0.0.10 86:e2:e3:21:13:b4
dnsmasq-dhcp: DHCPREQUEST(br0) 10.0.0.10 86:e2:e3:21:13:b4
dnsmasq-dhcp: abandoning lease to 86:e2:e3:21:13:b4 of 10.0.0.10
dnsmasq-dhcp: DHCPACK(br0) 10.0.0.10 86:e2:e3:21:13:b4 node0
Как только узел получит IP-адрес, на него можно будет зайти по SSH:
ssh root@10.0.0.10
Создадим еще три узла, подставив соответствующее имя TAP-интерфейса и MAC-адрес. Когда все четыре узла будут запущены, можно переходить к установке Kubernetes.
Установка Kubernetes
Как и с Linux, есть множество дистрибутивов Kubernetes, которые отличаются способом установки. В этой статье мы воспользуемся k0s. Это одиночный статически связанный исполняемый двоичный файл, который работает как сервис systemd.
Существуют и другие однофайловые дистрибутивы Kubernetes, например K3s. Он ориентирован на простоту использования и работу в окружениях с малыми ресурсами. При этом k0s ставит во главу угла минимализм и более чистый upstream Kubernetes.
В состав K3s входят Ingress-контроллер и балансировщик нагрузки, а k0s не включает дополнительных инструментов. Мы остановимся на k0s, поскольку это прочный фундамент, на котором можно построить все что угодно. Тем же, кто работает в условиях ограниченных ресурсов, например на Raspberry PI, стоит обратить внимание на K3s.
Проект k0s включает CLI-инструмент k0sctl — рекомендуемый метод удаленной установки.
Установим последнюю версию k0sctl и создадим следующую конфигурацию кластера:
apiVersion: k0sctl.k0sproject.io/v1beta1
kind: Cluster
metadata:
name: local-cluster
spec:
hosts:
- role: controller
installFlags:
- --debug
ssh:
address: 10.0.0.10
user: root
port: 22
keyPath: ~/.ssh/id_ed25519
- role: worker
installFlags:
- --debug
ssh:
address: 10.0.0.11
user: root
port: 22
keyPath: ~/.ssh/id_ed25519
- role: worker
installFlags:
- --debug
ssh:
address: 10.0.0.12
user: root
port: 22
keyPath: ~/.ssh/id_ed25519
k0s:
version: 1.27.5+k0s.0
dynamicConfig: true
Здесь мы ссылаемся на каждый из узлов Kubernetes по его IP-адресу и удаленно логиним k0sctl с помощью файла идентификации SSH.
У каждого узла есть роль:
Controller-узлы образуют управляющий слой (control plane). На них работают компоненты управляющего слоя Kubernetes.
На worker-узлах запускаются рабочие нагрузки. Мы видим их в выводе kubectl get nodes.
В этом туториале используется один controller-узел. В production обычно запускают 2k + 1 controller-узлов, где k — число отказов узлов, которые должен выдержать кластер. В идеале каждый controller-узел должен работать в отдельной зоне отказа (или зоне доступности).
Теперь остается только попросить k0sctl применить кластерную конфигурацию к узлам.
Загрузка исполняемого файла k0s может занять много времени. В этом случае можно самостоятельно загрузить свежий исполняемый файл со страницы релизов проекта на GitHub и вручную установить его в
/usr/local/bin/k0s
как часть образа базового диска.
k0sctl --debug apply \
--config ./k0sctl.yaml \
--kubeconfig-out kubeconfig
После выполнения этой команды в текущей рабочей директории появится файл kubeconfig, который можно использовать для подключения к кластеру с помощью kubectl.
Если что-то пойдет не так, k0sctl выдаст путь к файлу лога ошибок в системе. В моем случае это был
~/.cache/k0sctl/k0sctl.log
.
Убедимся, что кластер запущен:
KUBECONFIG=./kubeconfig kubectl get pods -A
Должно получиться нечто подобное:
NAMESPACE NAME READY STATUS RESTARTS AGE
kube-system coredns-878bb57ff-l7c8p 1/1 Running 1 (3m52s ago) 59m
kube-system coredns-878bb57ff-tl4nx 1/1 Running 1 (3m53s ago) 59m
kube-system konnectivity-agent-6bdm5 1/1 Running 1 (3m53s ago) 59m
kube-system konnectivity-agent-fcssv 1/1 Running 1 (3m52s ago) 59m
kube-system kube-proxy-5lpg2 1/1 Running 1 (3m52s ago) 59m
kube-system kube-proxy-c8tbq 1/1 Running 1 (3m53s ago) 59m
kube-system kube-router-n4q7s 1/1 Running 1 (3m53s ago) 59m
kube-system kube-router-xtzkk 1/1 Running 1 (3m52s ago) 59m
kube-system metrics-server-7cdb99bf49-zwgvm 1/1 Running 2 (3m13s ago) 59m
Теперь у нас есть работающий кластер Kubernetes. Пора настроить Ingress-контроллер и внешний балансировщик нагрузки, чтобы трафик мог доходить до подов.
Ingress и балансировка нагрузки
В Kubernetes Ingress-контроллер управляет маршрутизацией HTTP-трафика к сервисам приложения. Но он не определяет, как именно внешний трафик попадает на Ingress-контроллер.
Те, кто пользуется управляемыми сервисами Kubernetes вроде GKE или EKS, могли заметить, что у сервиса Ingress-контроллера тип LoadBalancer. По нему облачная среда понимает, что необходимо выделить внешний балансировщик нагрузки для данного кластера. При этом Ingress-контроллер получит EXTERNAL IP — доступный снаружи IP-адрес, который привязан к балансировщику нагрузки. Балансировщик нагрузки будет проксировать весь трафик, который поступает на этот адрес, на Ingress-контроллер кластера.
Так как облачное окружение у нас отсутствует, настраивать балансировщик нагрузки придется самим. Здесь как раз и пригодится четвертый узел.
В Kubernetes у сервисов может быть тип ClusterIP, NodePort, LoadBalancer или ExternalName. Из документации Kubernetes следует, что LoadBalancer фактически ничего не делает. Часто это вызывает путаницу при попытке освоить Kubernetes в облачном окружении.
Будем работать следующим образом:
Установим Ingress-контроллер: воспользуемся ingress-nginx.
Установим HAProxy на
node3
и настроим его на обратное проксирование всего HTTP- и HTTPS-трафика на Ingress-контроллер. Точнее на Ingress-контроллере будет настроен сервис NodePort, на который HAProxy сможет ссылаться как на бэкенд обратного прокси.
Сетевой поток будет выглядеть примерно так:
Сервис NodePort Ingress-контроллера открывает порты на каждом рабочем узле. Затем HAProxy проксирует полученный HTTP(S)-трафик на эти порты.
Теперь нужно установить ingress-nginx. Применим манифест baremetal, как указано в документации по настройке сервиса NodePort:
kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/controller-v1.8.2/deploy/static/provider/baremetal/deploy.yaml
В production-кластере следует воспользоваться установкой с помощью Helm — это поможет автоматизировать процесс обновления Ingress-контроллера.
После завершения установки Ingress-контроллера мы должны увидеть нечто подобное:
$ kubectl get pods -n ingress-nginx
NAME READY STATUS RESTARTS AGE
ingress-nginx-admission-create-dv4p7 0/1 Completed 0 60s
ingress-nginx-admission-patch-7htxb 0/1 Completed 2 60s
ingress-nginx-controller-79bc9f5df8-vpmrw 0/1 Running 0 60s
Посмотрим на сервис Ingress-контроллера:
$ kubectl get -n ingress-nginx svc
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
ingress-nginx-controller NodePort 10.106.85.180 <none> 80:30299/TCP,443:30448/TCP 85s
ingress-nginx-controller-admission ClusterIP 10.105.197.7 <none> 443/TCP 85s
У сервиса ingress-nginx-controller тип NodePort, на порт 30299 принимается HTTP-трафик, на 30448 — HTTPS. Запомним их, так как они понадобятся нам для настройки HAProxy.
Стоит обратить внимание, что у каждого из нас порты могут быть разными. Kubernetes выделяет порты из предустановленного диапазона в зависимости от их доступности. Потом порты не меняются, если только не освободить их вручную (например, при удалении сервиса).
Теперь, когда Ingress-контроллер установлен и готов к работе, можно установить HAProxy на узле балансировщика нагрузки:
ssh "root@$10.0.0.13" "apt-get update -y && apt-get install -y haproxy=2.6.\*"
Убедимся, что он работает:
$ ssh root@10.0.0.13 systemctl status haproxy
● haproxy.service - HAProxy Load Balancer
Loaded: loaded (/lib/systemd/system/haproxy.service; enabled; preset: enabled)
Active: active (running) since Wed 2023-09-27 18:03:55 UTC; 21s ago
Docs: man:haproxy(1)
file:/usr/share/doc/haproxy/configuration.txt.gz
Main PID: 1186 (haproxy)
Tasks: 3 (limit: 2353)
Memory: 41.9M
CPU: 108ms
CGroup: /system.slice/haproxy.service
├─1186 /usr/sbin/haproxy -Ws -f /etc/haproxy/haproxy.cfg -p /run/haproxy.pid -S /run/haproxy-master.sock
└─1188 /usr/sbin/haproxy -Ws -f /etc/haproxy/haproxy.cfg -p /run/haproxy.pid -S /run/haproxy-master.sock
Sep 27 18:03:55 node3 systemd[1]: Starting haproxy.service - HAProxy Load Balancer...
Sep 27 18:03:55 node3 haproxy[1186]: [NOTICE] (1186) : New worker (1188) forked
Sep 27 18:03:55 node3 haproxy[1186]: [NOTICE] (1186) : Loading success.
Sep 27 18:03:55 node3 systemd[1]: Started haproxy.service - HAProxy Load Balancer.
Попытка зайти на http://10.0.0.13
в браузере приведет к ошибке подключения, поскольку мы еще не настроили бэкенды в HAProxy. Для этого воспользуемся следующим конфигом:
global
log /dev/log local0
log /dev/log local1 notice
chroot /var/lib/haproxy
stats socket /run/haproxy/admin.sock mode 660 level admin
stats timeout 30s
user haproxy
group haproxy
daemon
defaults
log global
mode http
option httplog
option dontlognull
timeout connect 5000
timeout client 50000
timeout server 50000
timeout http-request 10000
errorfile 400 /etc/haproxy/errors/400.http
errorfile 403 /etc/haproxy/errors/403.http
errorfile 408 /etc/haproxy/errors/408.http
errorfile 500 /etc/haproxy/errors/500.http
errorfile 502 /etc/haproxy/errors/502.http
errorfile 503 /etc/haproxy/errors/503.http
errorfile 504 /etc/haproxy/errors/504.http
frontend http-frontend
bind *:80
default_backend http-backend
backend http-backend
server node1 10.0.0.11:30299
server node2 10.0.0.12:30299
frontend https-frontend
mode tcp
option tcplog
bind *:443
default_backend https-backend
backend https-backend
mode tcp
server node1 10.0.0.11:30448
server node2 10.0.0.12:30448
В нем настраиваются два фронтенда и два бэкенда: один — для HTTP, другой — для HTTPS.
Фронтенд — это сокет, к которому HAProxy привязывается и через который принимает пакеты. Бэкенд проксирует пакеты от фронтенда к серверу. В данном случае мы проксируем HTTP(S)-трафик на открытые порты worker-узлов.
Для HTTPS-фронтенда и бэкенда установлен режим mode tcp. Использование фронтенда HTTPS с бэкендом в mode tcp называется TLS passthrough. В нем HAProxy просто передает зашифрованные TCP-пакеты на бэкенд-серверы, не пытаясь их расшифровать.
Плюс этого режима в том, что все управление сертификатами ведется через Kubernetes. Иначе пришлось бы устанавливать HTTPS-сертификаты и на Ingress-контроллер, и на балансировщик нагрузки.
Проверим, работает ли конфигурация:
$ curl -ik https://10.0.0.13
HTTP/2 404
date: Wed, 27 Sep 2023 18:24:38 GMT
content-type: text/html
content-length: 146
strict-transport-security: max-age=15724800; includeSubDomains
<html>
<head><title>404 Not Found</title></head>
<body>
<center><h1>404 Not Found</h1></center>
<hr><center>nginx</center>
</body>
</html>
Теперь мы получаем 404 вместо ошибки подключения. Это ошибка nginx, то есть мы проходим балансировщик нагрузки и добираемся до Ingress-контроллера.
Наконец-то можно развертывать приложение в кластере. Воспользуемся этим контейнером whoami и настроим deployment, сервис и Ingress-маршрут:
apiVersion: apps/v1
kind: Deployment
metadata:
name: whoami-test
labels:
app.kubernetes.io/name: whoami
spec:
replicas: 3
selector:
matchLabels:
app.kubernetes.io/name: whoami
template:
metadata:
labels:
app.kubernetes.io/name: whoami
spec:
containers:
- name: whoami
image: traefik/whoami:v1.10
ports:
- containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
name: nginx-test
spec:
selector:
app.kubernetes.io/name: nginx
ports:
- protocol: TCP
port: 80
targetPort: 80
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: nginx-test
spec:
ingressClassName: nginx
rules:
- host: example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: nginx-test
port:
number: 80
Обычный шаблон Kubernetes. В спецификации Ingress мы задаем ingressClassName: nginx
и ссылаемся на Ingress-контроллер, а в правилах в качестве хоста используем example.com
.
Проверим эндпоинт:
$ curl -ik -H 'Host: example.com' http://10.0.0.13
HTTP/1.1 200 OK
date: Wed, 27 Sep 2023 18:45:20 GMT
content-type: text/plain; charset=utf-8
content-length: 429
Hostname: whoami-test-6747545df6-m9mcg
IP: 127.0.0.1
IP: ::1
IP: 10.244.0.20
IP: fe80::90f6:43ff:fe75:d8f6
RemoteAddr: 10.244.1.8:51950
GET / HTTP/1.1
Host: example.com
User-Agent: curl/7.88.1
Accept: */*
X-Forwarded-For: 10.0.0.12
X-Forwarded-Host: example.com
X-Forwarded-Port: 80
X-Forwarded-Proto: http
X-Forwarded-Scheme: http
X-Real-Ip: 10.0.0.12
X-Request-Id: f942bf1e3c5a9e4af9a14b7c4903ad16
X-Scheme: http
Чтобы проверить с помощью браузера, сначала добавляем в файл /etc/hosts
строку 10.0.0.13 example.com
. Вывод в браузере должен быть похожим.
Итоги
Мы прошлись по основным шагам по созданию собственного кластера Kubernetes. Перенести их на кластер VPS довольно просто, так как не нужно связываться с кастомными образами дисков или сетевыми настройками — за нас это сделает облачный провайдер.
При установке Kubernetes с помощью k0sctl нужно обязательно ознакомиться с документацией по правильной настройке bastion-узла, чтобы не давать всем узлам публичные IP-адреса.
Возможные дальнейшие шаги по расширению кластера:
Установить Helm.
Установить Local Persistence Volume Static Provisioner, чтобы создавать постоянные тома на подключенных к узлу дисках.
Установить Harbor для управления образами контейнеров.
Установить cert-manager для автоматического управления сертификатами TLS.
Настроить высокодоступный API-сервер Kubernetes с помощью keepalived с виртуальным IP-адресом.
Весь код и конфигурационные файлы для этого туториала доступны в Git-репозитории.
P. S.
Читайте также в нашем блоге:
Комментарии (8)
denaspireone
06.12.2023 07:52+2Я бы советовал обратить внимание на firecracker, вот тут хорошо описано https://iximiuz.com/en/posts/iximiuz-labs-story/
Igorgro
06.12.2023 07:52+2Делал примерно то же самое на домашнем сервер но немного на других технологиях:
Вместо сырого QEMU - LXD, он позволяет управлять не только контейнерами, но и виртуальными машинами, поддерживает cloud-init. В принципе это отличная замена libvirt, с намного более user-friendly интерфейсом с несложными YAML вместо монструозных XML;
В качестве балансировщика нагрузки перед нодами - Nginx в режиме stream - в отличие от http это балансировка на уровне TCP, за счет этого должна работать быстрее;
Все это управляется Ansible, а внутри Kubernetes используется FluxCD - получается что вся система полностью работает за счет IaaC.
TimurTukaev
06.12.2023 07:52То есть прям гитерменизм и вот это всё? А для какой цели разворачивали?
Igorgro
06.12.2023 07:52+1Для двух целей: во-первых чтобы вынести сервисы, которые развернуты на нескольких машинах в докере в одно место, чтобы постоянно не вспоминать где что развернуто (ssh+docker compose). А во-вторых чтобы перенести те сервисы, которые развернуты не в докере и про которые достаточно быстро забывается, что и где ты настраивал. Вот эти бесконечные копания в /etc и т.п. После того как перешел на Ansible - небо и земля.
В некоторых случаях правда получается что для того чтобы развернуть что-то надо написать манифестов в 4-5 раз больше чем docker compose для той же цели.
Twissell
06.12.2023 07:52А почему никого не смущает, что для коннект машин автор использует tap-device вместо ethernet?
Может я чего-то не понял, но разве оно будет так работать?
dsoastro
Хороший текст. Хорошо описано создание виртуальных машин с qemu, заведение в них tap-интерфейсов и их объединения при помощи бриджа. Настройка сетевой части куба с haproxy тоже весьма интересна. Попробую на досуге
TimurTukaev
Будет, чем заняться на выходных:)