Дано: домашний сервер под управлением Debian 11 с установленным гипервизором Xen.

Требуется: развернуть Kubernetes-кластер для получения опыта, связанного с настройкой и управлением Kubernetes-кластера, и дальнейшего его использования для разработки и хостинга персональных проектов.

Развёртывание Debian и Xen, а также миграция с Hyper-V на Xen нескольких Windows-виртуальных машин описана в моей статье Миграция домашнего сервера с Hyper-V на Xen Project на Debian. В ней также описана конфигурация моей сети.

В данной статье я исхожу из того, что читатель имеет представление о том, что такое Kubernetes, как он работает, из каких компонентов состоит и знаком с необходимой терминологией. Многие идеи и код я брал из статьи Разворачиваем среду для работы с микросервисами. Часть 1 установка Kubernetes HA на bare metal (Debian), но местами адаптировал под свои нужды и окружение. Задача данной статьи дать читателям готовое решение, требующее минимальных усилий для повторения и, вместе с тем, не требующее дополнительных инструментов (вроде Ansible или Terraform), а также показать новичкам некоторые моменты работы с Linux, Kubernetes и используемыми пакетами. Повествование разбито на несколько шагов, каждый из которых заключается в запуске скрипта и нескольких ручных командах.

Конфигурация кластера

Минимальная конфигурация кластера может состоять из одного-двух узлов: либо один master, которому разрешено хостить пользовательские нагрузки, либо один master и один worker. Однако, подобный кластер не будет, даже близко, похож на типичный промышленный high available (HA) кластер, а значит, полученный опыт будет сильно ограничен как в процессе развёртывания, так и в процессе эксплуатации. Конечно, ни о каком HA-кластере на одной физической машине (как в моём случае) речи быть не может, но, напомню, главная цель — получение опыта и площадки для экспериментов. К слову, если для разработки нужен кластер на рабочей станции, скажем для разработки cloud native приложений, или для изучения простейших команд kubectl, отличным выбором будет minikube. Я уже его использую для TDD через end-to-end и интеграционные тесты с использованием skaffold, о чём, возможно, расскажу в одной из следующих статей.

Как выглядит минимально-достаточный промышленный HA Kubernetes-кластер?

  • от двух master-узлов,

  • от трёх etcd-узлов и load balancer для них,

  • от двух worker-узлов.

Если мы будем хостить какие-либо web-приложения в кластере, доступ к которым необходимо обеспечить извне, может потребоваться либо внешний load balancer, либо ingress. Однако, это отдельная большая тема, достойная отдельной статьи.

Нам потребуется 5 виртуальных машин. На первых трёх будут развёрнуты master-узлы Kubernetes и etcd-серверы. На двух оставшихся worker-узлы Kubernetes и узлы haproxy-кластера.

Важный момент, почему мастеров 3? Дело в том, что etcd-узлов в кластере должно быть не менее трёх. Если их будет 2, в случае если один из них выйдет из строя или будет отключён, голосование по выбору лидера etcd-кластера не сможет завершиться, и единственный оставшийся etcd-узел будет недоступен, а вместе с ним и функциональность Kubernetes-кластера. Таким образом, узлов в etcd-кластере должно быть не менее 3. etcd-узлы я планирую размещать вместе с master-узлами Kubernetes, чтобы не плодить виртуалки. Поэтому master-узлов должно быть 3.

Worker-узлов может быть любое количество. Я выбрал 2, чтобы можно было симулировать выход одного из них из строя и наблюдать за миграцией подов на оставшийся. Добавить новые worker-узлы в кластер можно в любой момент.

В состав кластера будут входить следующие виртуалки:

Имя узла

IP-адрес узла

Роли узла

Процессор

Объём ОЗУ

master01

10.44.44.11

etcd, k8s-master

2vcpu

4GB

master02

10.44.44.12

etcd, k8s-master

2vcpu

4GB

master03

10.44.44.13

etcd, k8s-master

2vcpu

4GB

worker01

10.44.44.14

haproxy, k8s-worker

2vcpu

4GB

worker02

10.44.44.15

haproxy, k8s-worker

2vcpu

4GB

Создание PV-доменов с помощью xen-tools

Для начала необходимо создать виртуалки для нашего кластера. Xen поддерживает несколько техник виртуализации. Одна из них называется paravirtualization. Если кратко, эта техника не требует от процессора встроенной поддержки виртуализации. На первый взгляд это может показаться незначительным, ведь большинство современных процессоров поддерживают специальные расширения для виртуализации. На практике же это означает ускорение работы виртуалок, так как не требует перехода между пользовательским контекстом и контекстом ядра ОС. Для Linux-виртуалок нужно пользоваться именно этим видом виртуализации. Каких-либо специальных драйверов, в отличии от Windows HVM виртуалок, ставить не нужно, всё уже есть в ядре Linux.

В Xen PV-домены можно создать вручную или с помощью набора инструментов xen-tools. Это набор скриптов, которые позволяют создать из шаблона виртуалку одного из дистрибутивов Linux (и не только), настроив её с помощью параметров командной строки и «скелета» файловой системы (набор папок и файлов, которые должны быть скопированы в файловую систему виртуалки). Я буду использовать LVM для хранения дисков и Debian 11 в качестве гостевой ОС. Поскольку мне нужно будет создать 5 подобных друг другу виртуалок, я решил немного автоматизировать этот процесс и создал следующий простой скрипт (я назвал его create-debian-pv.sh):

#!/bin/bash

hostname=$1
ip=10.44.44.$2
mac=00:16:3e:44:44:$2

xen-create-image \
--hostname=$hostname --memory=2gb --vcpus=2 \
--lvm=vg0 --ip=$ip --mac=$mac \
--pygrub --dist=bullseye --noswap --noaccounts \
--noboot --nocopyhosts --extension=.pv \
--fs=ext4 --genpass=0 --passwd --nohosts \
--bridge=xenlan44 --gateway=10.44.44.1 --netmask=255.255.255.0

Скрипт необходимо сделать запускаемым:

chmod 777 create-debian-pv.sh

В первом параметре указывается имя хоста, во втором последняя цифра IP-адреса и MAC-адреса. Для простоты я принял следующее соглашение к формированию MAC-адреса: «00:16:3e» — стандартный префикс MAC-адреса Xen-виртуалок, «44:44» — взято из подсети, последний сегмент одинаков у MAC-адреса и IP-адреса. Мне так проще ориентироваться, но вы можете придумать что-то своё и скорректировать скрипт.

«vg0» — название моей LVM volume group, «bullseye» — название одного из поддерживаемых данной командой дистрибутивов (это Debian 11, полный список можно посмотреть в папке /usr/share/xen-tools/). Создание раздела swap запрещено, так как этого требует Kubernetes, и, чтобы не расходовать место на диске, я отключил создание этого раздела полностью. Более подробно о доступных параметрах xen-create-image можно ознакомиться на его man-странице.

Но перед тем, как использовать данный скрипт, на хосте Xen нужно установить необходимый пакет:

apt install -y xen-tools

Настройка SSH в создаваемых виртуалках

Я предпочитаю сразу настроить SSH в новой виртуалке аналогично тому, как это сделано в ОС гипервизора (dom0). Для этого необходимо создать в папке (папка «скелета» файловой системы xen-tools, о которой я говорил ранее) /etc/xen-tools/skel/ необходимую структуру папок и скопировать файлы (также на хосте Xen). Я использую авторизацию с помощью пользовательского сертификата с установленным на нём паролем для доступа.

mkdir -p /etc/xen-tools/skel/root/.ssh
cp /root/.ssh/authorized_keys /etc/xen-tools/skel
mkdir -p /etc/xen-tools/skel/etc/ssh
cp /etc/ssh/sshd_config /etc/xen-tools/skel/etc/ssh

Создание виртуалок

Последовательно запускаем создание необходимых виртуалок:

./create-debian-pv.sh master01 11
./create-debian-pv.sh master02 12
./create-debian-pv.sh master03 13
./create-debian-pv.sh worker01 14
./create-debian-pv.sh worker02 15

В процессе своей работы скрипт будет запрашивать пароль для учётной записи root. Вводим его дважды и вскоре виртуалка создана. Для каждой из них будет создан соответствующий файл конфигурации в папке /etc/xen. Например для master01 будет создан скрипт /etc/xen/master01.pv. Позднее, по мере необходимости, можно настроить количество памяти, количество виртуальных ядер процессора и другие параметры виртуальных машин.

В каждом из созданных файлов конфигурации домена необходимо добавить следующую строку: localtime = 1. Это скажет Xen, что в виртуалке нужно использовать ваш часовой пояс, а не время по Гринвичу.

Файл настроек

Создайте новую папку на машине, имеющей сетевой доступ к виртуалкам, и настроенный SSH-клиент для доступа к хосту Xen. В данной папке будут размещаться скрипты, которые будут запускаться на виртуалках. Далее по тексту я буду называть эту папку рабочей.

Все скрипты будут использовать общие настройки, которые я разместил в отдельном файле variables.sh. Создайте его в рабочей папке.

#!/bin/bash
# IP-адреса и имена хостов мастер-узлов Kubernetes. Замените на свои.
master01_IP=10.44.44.11
master02_IP=10.44.44.12
master03_IP=10.44.44.13

master01_Hostname=master01
master02_Hostname=master02
master03_Hostname=master03
# IP-адрес и имя хоста, на котором выполняется скрипт.
thisHostname=$(hostname)
thisIP=$(hostname -i)
# etcd-token. Замените на свой. Может быть любой строкой.
etcdToken=my-etcd-cluster-token
# CRI-socket.
containerdEndpoint=unix:///run/containerd/containerd.sock
# Версии пакетов. Замените на текущие актуальные.
etcdVersion=3.5.3
containerdVersion=1.6.2
runcVersion=1.1.1
cniPluginsVersion=1.1.1
kubernetesVersion=1.23.6
calicoVersion=3.22
# Пространство адресов подов. Зависит от CNI-плагина. В данном случае используется Calico.
podSubnet=192.168.0.0/16
# Пространство адресов сервисов. Замените на своё. Может быть любое, но должно быть достаточно большим.
serviceSubnet=10.46.0.0/16

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

Развёртывание etcd

etcd используется в качестве основного хранилища информации о состоянии Kubernetes-кластера. Считайте, что это его база данных. Для устойчивости к отказам этого хранилища, как было сказано ранее, необходимо создать etcd-кластер. etcd можно развернуть различными способами, но я выбрал самый простой: развернуть на том же «виртуальном железе», что и master-узлы.

Запускаем виртуалки master-узлов из консоли dom0 Xen:

xl create /etc/xen/master01.pv
xl create /etc/xen/master02.pv
xl create /etc/xen/master03.pv

Создаём скрипт развёртывания etcd-узла install-etcd.sh в рабочей папке:

#!/bin/bash

source variables.sh

etcdEnvironmentFilePath=/etc/etcd.env
serviceFilePath=/etc/systemd/system/etcd.service

function installAndConfigurePrerequisites {
  apt install curl -y
}

function downloadAndInstallEtcd {
  curl -L https://github.com/etcd-io/etcd/releases/download/v${etcdVersion}/etcd-v${etcdVersion}-linux-amd64.tar.gz --output etcd-v${etcdVersion}-linux-amd64.tar.gz
  tar -xvf etcd-v${etcdVersion}-linux-amd64.tar.gz -C /usr/local/bin/ --strip-components=1
}

function createEnvironmentFile {
cat > ${etcdEnvironmentFilePath} <<EOF
${thisHostname} > ${etcdEnvironmentFilePath}
${thisIP} >> ${etcdEnvironmentFilePath}
EOF
}

function createServiceFile {
cat > $serviceFilePath <<EOF
[Unit]
Description=etcd
Documentation=https://github.com/coreos/etcd
Conflicts=etcd.service
Conflicts=etcd2.service

[Service]
EnvironmentFile=/etc/etcd.env
Type=notify
Restart=always
RestartSec=5s
LimitNOFILE=40000
TimeoutStartSec=0

ExecStart=/usr/local/bin/etcd \\
  --name ${thisHostname} \\
  --data-dir /var/lib/etcd \\
  --listen-peer-urls http://${thisIP}:2380 \\
  --listen-client-urls http://0.0.0.0:2379 \\
  --advertise-client-urls http://${thisIP}:2379 \\
  --initial-cluster-token ${etcdToken} \\
  --initial-advertise-peer-urls http://${thisIP}:2380 \\
  --initial-cluster ${master01_Hostname}=http://${master01_IP}:2380,${master02_Hostname}=http://${master02_IP}:2380,${master03_Hostname}=http://${master03_IP}:2380 \\
  --initial-cluster-state new

[Install]
WantedBy=multi-user.target
EOF
}

function removeDownloads {
  rm -f etcd-v${etcdVersion}-linux-amd64.tar.gz
}

installAndConfigurePrerequisites
downloadAndInstallEtcd
createEnvironmentFile
createServiceFile
removeDownloads

systemctl enable --now etcd

И копируем все скрипты из рабочей папки в папку /root каждой виртуалки master-узлов Kubernetes (здесь и далее показано только для одной виртуалки):

scp *.sh root@10.44.44.11:/root

SSH-клиент запросит пароль (если вы его установили) доступа к приватному SSH-ключу и скопирует скрипты.

Последовательно подключаемся по SSH к каждому master-узлу и запускаем скрипт из папки /root:

./install-etcd.sh

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

После того как скрипт на всех трёх master-узлах будет запущен, статус etcd-узлов должен стать active (running):

root@master01:~# systemctl status etcd
* etcd.service - etcd                                                              
     Loaded: loaded (/etc/systemd/system/etcd.service; disabled; vendor preset:    
enabled)                                                                           
     Active: active (running) since Sat 2022-04-23 03:51:23 UTC; 14s ago           
       Docs: https://github.com/coreos/etcd                                        
   Main PID: 3129 (etcd)                                                           
      Tasks: 7 (limit: 2250)                                                       
     Memory: 11.2M                                                                 
        CPU: 250ms                                                                 
     CGroup: /system.slice/etcd.service                                            
             `-3129 /usr/local/bin/etcd --name master01 --data-dir /var/lib/etcd

…а статус всего etcd-кластера должен выглядеть примерно так:

root@master01:~# etcdctl --write-out=table --endpoints=10.44.44.11:2379,10.44.44.12:2379,10.44.44.13:2379 endpoint status

+------------------+------------------+---------+---------+-----------+------------+-----------+------------+--------------------+--------+
|     ENDPOINT     |        ID        | VERSION | DB SIZE | IS LEADER | IS LEARNER | RAFT TERM | RAFT INDEX | RAFT APPLIED INDEX | ERRORS |
+------------------+------------------+---------+---------+-----------+------------+-----------+------------+--------------------+--------+
| 10.44.44.11:2379 | 5f81cb6fe79d652f |   3.5.3 |   20 kB |      true |      false |         2 |          9 |                  9 |        |
| 10.44.44.12:2379 | 836005ecbe98da1a |   3.5.3 |   20 kB |     false |      false |         2 |          9 |                  9 |        |
| 10.44.44.13:2379 | ae73e256fd5aa97c |   3.5.3 |   20 kB |     false |      false |         2 |          9 |                  9 |        |
+------------------+------------------+---------+---------+-----------+------------+-----------+------------+--------------------+--------+

Для более подробного ознакомления можно почитать в документации kubernetes.

Установка containerd и runc

После того, как Kubernetes перестал поддерживать Dockershim (да и не нужен Docker, по большому счёту, в Kubernetes-кластере), осталось два довольно равнозначных варианта среды исполнения: containerd и CRI-O. Я выбрал первый. Поставить его можно двумя способами (компиляция из исходников не в счёт): воспользоваться репозиторием пакетов ОС или скачать релиз с GitHub. На момент написания статьи версия в репозитории Debian — 1.4.13, версия релиза в GitHub — 1.6.2. Разница существенная, поэтому я выбрал вариант с GitHub.

Для того, чтобы понять, что такое среда выполнения, для чего она нужна, а также осознать, что Docker — не наше всё, рекомендую почитать статью Различия между Docker, containerd, CRI-O и runc.

containerd нужно установить на всех узлах кластера, поэтому запускаем и виртуалки worker-узлов:

xl create /etc/xen/worker01.pv
xl create /etc/xen/worker02.pv

Для установки containerd в рабочей папке создаём скрипт install-containerd.sh следующего содержания:

#!/bin/bash

source variables.sh

function installAndConfigurePrerequisites {
  apt install curl -y

cat <<EOF | tee /etc/modules-load.d/containerd.conf
overlay
br_netfilter
EOF

  modprobe overlay
  modprobe br_netfilter
# Настройка обязательных параметров sysctl.
cat <<EOF | tee /etc/sysctl.d/99-kubernetes-cri.conf
net.bridge.bridge-nf-call-iptables=1
net.ipv4.ip_forward=1
net.bridge.bridge-nf-call-ip6tables=1
EOF
# Применяем изменения без перезагрузки.
  sysctl --system
}

function downloadAndInstallContainerd {
  # Загружаем релиз containerd.
  curl -L https://github.com/containerd/containerd/releases/download/v${containerdVersion}/containerd-${containerdVersion}-linux-amd64.tar.gz --output containerd-${containerdVersion}-linux-amd64.tar.gz
  tar -xvf containerd-${containerdVersion}-linux-amd64.tar.gz -C /usr/local
  # Создаём файл конфигурации containerd.
  mkdir /etc/containerd/
  containerd config default > /etc/containerd/config.toml
  # Разрешаем использование systemd cgroup.
  sed -i "s/SystemdCgroup = false/SystemdCgroup = true/g" /etc/containerd/config.toml
# Создаём файл сервиса containerd.
cat > /etc/systemd/system/containerd.service <<EOF
[Unit]
Description=containerd container runtime
Documentation=https://containerd.io
After=network.target local-fs.target

[Service]
ExecStartPre=-/sbin/modprobe overlay
ExecStart=/usr/local/bin/containerd

Type=notify
Delegate=yes
KillMode=process
Restart=always
RestartSec=5
LimitNPROC=infinity
LimitCORE=infinity
LimitNOFILE=infinity
TasksMax=infinity
OOMScoreAdjust=-999

[Install]
WantedBy=multi-user.target
EOF
  # Разрешаем и запускаем сервис containerd.
  systemctl daemon-reload
  systemctl enable --now containerd
}

function downloadAndInstallRunc {
  # Загружаем и устанавливаем низкоуровневую службу запуска контейнеров.
  curl -L https://github.com/opencontainers/runc/releases/download/v${runcVersion}/runc.amd64 --output runc.amd64
  install -m 755 runc.amd64 /usr/local/sbin/runc
}

function downloadAndInstallCniPlugins {
  curl -L https://github.com/containernetworking/plugins/releases/download/v${cniPluginsVersion}/cni-plugins-linux-amd64-v${cniPluginsVersion}.tgz --output cni-plugins-linux-amd64-v${cniPluginsVersion}.tgz
  mkdir -p /opt/cni/bin
  tar Cxzvf /opt/cni/bin cni-plugins-linux-amd64-v${cniPluginsVersion}.tgz
}

function removeDownloads {
  rm -f containerd-${containerdVersion}-linux-amd64.tar.gz
  rm -f runc.amd64
  rm -f cni-plugins-linux-amd64-v${cniPluginsVersion}.tgz
}

installAndConfigurePrerequisites
downloadAndInstallContainerd
downloadAndInstallRunc
downloadAndInstallCniPlugins
removeDownloads

systemctl restart containerd

Снова копируем скрипты на все master- и worker-узлы:

scp *.sh root@10.44.44.11:/root

…и запускаем его на каждом узле:

./install-containerd.sh

После его установки проверяем, что сервис работает (должно быть Active: active (running)):

root@master01:~# systemctl status conatinerd

* containerd.service - containerd container runtime
     Loaded: loaded (/etc/systemd/system/containerd.service; enabled; vendor pre
set: enabled)
     Active: active (running) since Sat 2022-04-23 20:59:47 UTC; 20s ago
       Docs: https://containerd.io
    Process: 3526 ExecStartPre=/sbin/modprobe overlay (code=exited, status=0/SUC
CESS)
   Main PID: 3528 (containerd)
      Tasks: 9
     Memory: 20.1M
        CPU: 121ms
     CGroup: /system.slice/containerd.service
             `-3528 /usr/local/bin/containerd

Развёртывание первого master-узла Kubernetes

В рабочей папке создаём скрипт install-master-node.sh:

#!/bin/bash

source variables.sh

function installAndConfigurePrerequisites {
  apt install -y apt-transport-https curl gnupg2 apparmor apparmor-utils
  curl -s https://packages.cloud.google.com/apt/doc/apt-key.gpg | apt-key add -
cat <<EOF >/etc/apt/sources.list.d/kubernetes.list
deb https://apt.kubernetes.io/ kubernetes-xenial main
EOF
# Установить необходимые пакеты и зафиксировать их версию.
  apt update
  apt install -y kubelet=$kubernetesVersion-00
  apt install -y kubeadm=$kubernetesVersion-00
  apt install -y kubectl=$kubernetesVersion-00
  apt-mark hold kubelet kubeadm kubectl
}

function createKubeadmConfig {
cat > kubeadm-init.yaml <<EOF
---
apiVersion: kubeadm.k8s.io/v1beta3
kind: InitConfiguration
localAPIEndpoint:
  advertiseAddress: "${thisIP}"
nodeRegistration:
  criSocket: "${containerdEndpoint}"
---
apiVersion: kubeadm.k8s.io/v1beta3
kind: ClusterConfiguration
kubernetesVersion: v${kubernetesVersion}
apiServer:
  certSANs:
  - ${master01_IP}
  - ${master02_IP}
  - ${master03_IP}
  - 127.0.0.1
controlPlaneEndpoint: ${thisIP}
etcd:
  external:
    endpoints:
    - http://${master01_IP}:2379
    - http://${master02_IP}:2379
    - http://${master03_IP}:2379
networking:
  podSubnet: "${podSubnet}"
  serviceSubnet: "${serviceSubnet}"
  dnsDomain: "cluster.local"
EOF
}

function initializeMasterNode {
  kubeadm init --config=kubeadm-init.yaml
}

function installCalicoCNI {
  export KUBECONFIG=/etc/kubernetes/admin.conf
  kubectl apply -f https://docs.projectcalico.org/v${calicoVersion}/manifests/calico.yaml
}

function archiveCertificates {
  tar -zcvf certificates.tar.gz -C /etc/kubernetes/pki .
}

function extractCertificates {
  mkdir -p /etc/kubernetes/pki
  tar -xvf certificates.tar.gz -C /etc/kubernetes/pki
}

installAndConfigurePrerequisites
createKubeadmConfig

if [[ $thisIP == $master01_IP ]]; then
  # На первом мастер-узле устанавливаем CNI-плагин и архивируем сертификаты
  # для последующего использования на других мастер-узлах.
  initializeMasterNode
  installCalicoCNI
  archiveCertificates
fi

if [[ $thisIP != $master01_IP ]]; then
  # На не первом мастер-узле используем сертификаты, полученные с первого мастер-узла.
  extractCertificates
  initializeMasterNode
fi

Этот скрипт будет использоваться также и для остальных master-узлов, поэтому копируем его также и на них.

Запускаем скрипт на первом master-узле master01:

./install-master-node.sh

После успешного запуска в папке /root появится архив с сертификатами кластера certificates.tar.gz, который необходимо скопировать в папки /root остальных master-узлов.

В результате успешного выполнения вы должны получить примерно такой вывод:

Then you can join any number of worker nodes by running the following on each as root:

kubeadm join 10.44.44.11:6443 --token 635j2y.7ar0dbhq7l77jne0 \
        --discovery-token-ca-cert-hash sha256:8fab070cc51d4449d8fa0fc86348aeccc0554ca5e473da3a5138c6fc5a3f5e77

Необходимо сохранить данный текст, так как далее нам понадобятся два параметра из него.

После установки проверяем, что master-узел работает:

root@master01:~# kubectl get nodes
NAME       STATUS   ROLES                  AGE     VERSION
master01   Ready    control-plane,master   32s     v1.23.6

Обеспечение доступа kubectl к кластеру

kubectl используется в качестве основного инструмента администрирования Kubernetes-кластера. Мы его установим на всех узлах кластера, однако, если попробовать его использовать, мы получим ошибку с достаточно непонятным текстом сообщения.

The connection to the server localhost:8080 was refused - did you specify the right host or port?

Для того, чтобы kubectl работал необходимо сказать ему, где искать конфигурацию для доступа к кластеру. Проще всего это сделать с помощью переменной окружения KUBECONFIG, указывающей на файл конфигурации. В зависимости от разных условий, файл этот может располагаться в разных местах.

  • На master-узлах при работе из-под учётки root: /etc/kubernetes/admin.conf.

  • На worker-узлах при работе из-под учётки root: /etc/kubernetes/kubelet.conf.

  • На любых узлах при работе из-под другой учётки: $HOME/.kube/config.

В последнем случае этот файл необходимо создать или скопировать с master-узлов. Устройство этого файла — тема достаточно обширная, поэтому детально описывать её здесь я не буду. Ознакомиться можно здесь: Organizing Cluster Access Using kubeconfig Files.

После того, как файл найден/скопирован/создан в файле .bashrc (или аналогичном, вашей командной оболочки) добавляем следующую строку (показано для случая master-узел из-под root):

export KUBECONFIG=/etc/kubernetes/admin.conf

И перезагружаем оболочку (показано для bash):

exec bash

Рекомендую установить kubectl на вашей рабочей машине. Дистрибутивы есть для всех основных ОС, включая Windows. Хотя на Windows я всё же предпочитаю работать не из PowerShell, а из bash в WSL-виртуалке.

Развёртывание остальных master-узлов Kubernetes

Для развёртывания оставшихся master-узлов используется тот же скрипт install-master-node.sh. Помимо скриптов, как было сказано ранее, необходимо скопировать в папку скриптов на целевые машины архив с сертификатами. Без этого архива скрипт не отработает. Проще всего распространить данный архив на все master-узлы можно, скопировав его вначале в рабочую папку с master01, а затем из рабочей папки на master02 и master03.

После установки посредством скрипта install-master-node.sh проверяем, что все master-узлы работают:

~ kubectl get nodes
NAME       STATUS   ROLES                  AGE     VERSION
master01   Ready    control-plane,master  1h5s     v1.23.6
master02   Ready    control-plane,master   13m     v1.23.6
master03   Ready    control-plane,master   14s     v1.23.6

Развёртывание worker-узлов Kubernetes-кластера

Worker-узлы развёртываем с помощью следующего скрипта install-worker-node.sh, который необходимо создать в рабочей папке:

#!/bin/bash

source variables.sh

function installAndConfigurePrerequisites {
  apt install -y apt-transport-https curl gnupg2 apparmor apparmor-utils
  curl -s https://packages.cloud.google.com/apt/doc/apt-key.gpg | apt-key add -
cat <<EOF >/etc/apt/sources.list.d/kubernetes.list
deb https://apt.kubernetes.io/ kubernetes-xenial main
EOF

  apt update
  apt install -y kubelet=$kubernetesVersion-00
  apt install -y kubeadm=$kubernetesVersion-00
  apt install -y kubectl=$kubernetesVersion-00
  apt-mark hold kubelet kubeadm kubectl
}

function joinMasterToCluster {
  read -p "Enter token: " token
  read -p "Enter SHA256 without 'sha256:' prefix: " sha
  kubeadm join 10.44.44.11:6443 --token $token --discovery-token-ca-cert-hash sha256:$sha
}

installAndConfigurePrerequisites
joinMasterToCluster

Как и в предыдущих шагах копируем все скрипты на worker-узлы:

scp *.sh root@10.44.44.14:/root

Подключаемся по SSH к узлу и запускаем скрипт:

./install-worker-node.sh

В ходе его выполнения будут заданы два вопроса про токен и SHA его сертификата. Их значения нужно взять из вывода, полученного при развёртывании первого master-узла. В моём случае это были: 635j2y.7ar0dbhq7l77jne0 и 8fab070cc51d4449d8fa0fc86348aeccc0554ca5e473da3a5138c6fc5a3f5e77 соответственно.

Время жизни токена ограничено 24 часами. Поэтому если между развёртыванием первого master-узла и worker-узла пройдёт более 24 часов, вам необходимо повторно сгенерировать этот токен. Для этого, подключившись к кластеру, необходимо выполнить следующую команду и использовать полученный токен и его хэш-сумму для скрипта install-worker-node.sh.

kubeadm token create --print-join-command

Развёртывание узлов haproxy-кластера

Последним штрихом будет создание haproxy-кластера для доступа worker-узлов к master-узлам. Зачем это нужно? Дело в том, что в конфигурации worker-узла жёстко прописывается адрес одного из master-узлов. В этом можно убедиться, найдя в файле /etc/kubernetes/kubelet.conf на worker-узле параметр server:. Если этот master-узел «приляжет», worker-узел «отвалится» от кластера. Чтобы этого не происходило, и кластер не терял свои worker-узлы, необходимо обеспечить защиту от подобных проблем. Одно из возможных решений реализовать доступ к control-plane через HA-proxy.

Создаём в рабочей папке скрипт install-haproxy.sh со следующим содержимым:

#!/bin/bash

source variables.sh

function installAndConfigurePrerequisites {
  apt install -y apt-transport-https curl gnupg2 apparmor apparmor-utils
}

function downloadAndInstallHaproxy {
  curl https://haproxy.debian.net/bernat.debian.org.gpg \
    | gpg --dearmor > /usr/share/keyrings/haproxy.debian.net.gpg
  echo deb "[signed-by=/usr/share/keyrings/haproxy.debian.net.gpg]" \
    http://haproxy.debian.net bullseye-backports-2.5 main \
    > /etc/apt/sources.list.d/haproxy.list

  apt update
  apt install -y haproxy=2.5.\*
}

function configureHaproxy {
cat > /etc/haproxy/haproxy.cfg <<EOF
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

	# Default SSL material locations
	ca-base /etc/ssl/certs
	crt-base /etc/ssl/private

	# Default ciphers to use on SSL-enabled listening sockets.
	# For more information, see ciphers(1SSL). This list is from:
	#  https://hynek.me/articles/hardening-your-web-servers-ssl-ciphers/
	# An alternative list with additional directives can be obtained from
	#  https://mozilla.github.io/server-side-tls/ssl-config-generator/?server=haproxy
	ssl-default-bind-ciphers ECDH+AESGCM:DH+AESGCM:ECDH+AES256:DH+AES256:ECDH+AES128:DH+AES:RSA+AESGCM:RSA+AES:!aNULL:!MD5:!DSS
	ssl-default-bind-options no-sslv3

defaults
	log	global
	mode	http
	option	httplog
	option	dontlognull
  timeout connect 5000
  timeout client  50000
  timeout server  50000
	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 k8s-api
	bind ${thisIP}:6443
	bind 127.0.0.1:6443
	mode tcp
	option tcplog
	default_backend k8s-api

backend k8s-api
	mode tcp
	option tcplog
	option tcp-check
	balance roundrobin
	default-server port 6443 inter 10s downinter 5s rise 2 fall 2 slowstart 60s maxconn 250 maxqueue 256 weight 100
        server apiserver1 ${master01_IP}:6443 check
        server apiserver2 ${master02_IP}:6443 check
        server apiserver3 ${master03_IP}:6443 check
EOF
systemctl restart haproxy
}

function connectNodeToLocalHaproxy {
  # Заменить адрес мастер узла на локальный адрес узла Haproxy.
  sed -i --regexp-extended "s/(server: https:\/\/)[[:digit:]]{1,3}\.[[:digit:]]{1,3}\.[[:digit:]]{1,3}\.[[:digit:]]{1,3}/\1127.0.0.1/g" /etc/kubernetes/kubelet.conf
  systemctl restart kubelet
}

installAndConfigurePrerequisites
downloadAndInstallHaproxy
configureHaproxy
connectNodeToLocalHaproxy

И, как обычно, копируем все скрипты из рабочей папки на worker-узлы:

scp *.sh root@10.44.44.14:/root

Запускаем скрипт на worker-узлах:

./install-haproxy.sh

После этого проверяем, что кластер работает и доступен:

~ kubectl get nodes
NAME       STATUS   ROLES                  AGE     VERSION
master01   Ready    control-plane,master   1d      v1.23.6
master02   Ready    control-plane,master   1d      v1.23.6
master03   Ready    control-plane,master   1d      v1.23.6
worker01   Ready    <none>                 1d      v1.23.6
worker02   Ready    <none>                 1d      v1.23.6

Что ещё осталось сделать

Кластер работает, но, в моём случае, пока не готов к использованию. Необходимо реализовать ещё, как минимум, две задачи:

  • обеспечить доступ извне к web-приложениям с помощью ingress и настройки роутера,

  • обеспечить хранение данных на дисках.

Однако, это выходит за рамки данной статьи. Я планирую осветить реализацию этих задач, а также развёртывание одного web-приложения в кластере в будущих статьях.

Благодарю за внимание. Надеюсь моя статья поможет вам разобраться с развёртыванием собственного кластера.

Комментарии (4)


  1. carebellum
    05.05.2022 15:24
    +1

    для быстрой установки кластера на сингл-хост использовал MicroK8s, опыт использования - достаточно приятный


  1. stralex7
    05.05.2022 16:22

    Если цель попробовать Kubernetes, то возможно https://microk8s.io/ будет хорошим решением, даже в случае HA https://microk8s.io/docs/high-availability


  1. DrrRos
    05.05.2022 17:01

    Сколько ванильный k8s кушает цпу в таком конфиге? Разворачивал у себя 3 ноды openshift - потребление от 12 до 15 ГГц. Сейчас SingleNode OpenShift + уже задеплоено десяток очень мелких проектов - 6-9ГГц.


  1. vainkop
    05.05.2022 19:27
    +2

    Столько много букв вместо

    curl -sfL https://get.k3s.io | sh -

    https://k3s.io

    Или мультикластер K3s в докере:

    curl -s https://raw.githubusercontent.com/k3d-io/k3d/main/install.sh | bash

    https://k3d.io

    Если очень хочется разобраться, то https://github.com/kelseyhightower/kubernetes-the-hard-way

    Для AWS (то, что реально используется в работе в проде): https://registry.terraform.io/modules/terraform-aws-modules/eks/aws/latest