Возможно, у вас была мысль запустить собственную услугу 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 logglobal 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.
Читайте также в нашем блоге:
ссылка на оригинал статьи https://habr.com/ru/articles/778394/
Добавить комментарий