Proxmox -> LXC (OpenVZ) -> Docker -> Kubernetes-кластер

Автор: | 04.01.2022

Прочитав заголовок, сразу же вспоминается персонаж сказок Кащей бессмертный, смерть которого на конце иглы, игла в яйце, яйцо в утке, утка в зайце, заяц в ларце, ларец в цепях на дубу, дуб на черной горе. Короче есть, но достать не легко. Так и тут: чтобы обеспечить отказоустойчивость сервиса реализуется целая гора сервисов, которые обеспечивают работу одного или группы сервисов какого-то приложения. Вот и я решил организовать нечто подобное.

Что нужно сделать можно понять из загаловка, но я все таки оглашу план:

  • Установка менеджера ВМ Proxmox VE
  • Запуск в контейнерах LXC кластера kubernetes:
    • мастер-нода: kube-master
    • воркер-ноды: kube-worker1 и kube-worker2
  • Базовая настройка инфраструктуры кластера
    • установка балансировщика MetaLB
    • установка ingress-контроллера haproxy
    • установка storage-контроллера OpenEBS
    • установка менеджера сертификатов cert-manager

Пунктов не много, выглядит не сложно. Можно приступать! )

В установке самого poxmox ничего хитрого нет — качаем образ, записываем на флэшку, устанавливаем и готово, можно запускать виртуальные машины. Из нюансов связанных с установкой proxmox, следует обратить внимание на IP адрес. Не смотря на то, что изначально он получается через DHCP, в результирующей системе он таки будет настроен как статический на том интерфейсе для которого он был настроен.

После установки нам понадобятся образы ОС, т.н. "golden image" из которых будут запускаться наши контейнеры. Proxmox имеет собственный репозиторий таких контейнеров, список которых можно получить с помощью следующих команд:

# обновляем репозиторий
pveam update

# можем посмотреть список всех доступных образов
pveam available

# но нас интересует секция system
pveam available --section system

На момент установки мне выводился такой список:

system          almalinux-8-default_20210928_amd64.tar.xz
system          alpine-3.12-default_20200823_amd64.tar.xz
system          alpine-3.13-default_20210419_amd64.tar.xz
system          alpine-3.14-default_20210623_amd64.tar.xz
system          alpine-3.15-default_20211202_amd64.tar.xz
system          archlinux-base_20210420-1_amd64.tar.gz
system          archlinux-base_20211202-1_amd64.tar.zst
system          centos-7-default_20190926_amd64.tar.xz
system          centos-8-default_20201210_amd64.tar.xz
system          debian-10-standard_10.7-1_amd64.tar.gz
system          debian-11-standard_11.0-1_amd64.tar.gz
system          devuan-3.0-standard_3.0_amd64.tar.gz
system          fedora-34-default_20210427_amd64.tar.xz
system          fedora-35-default_20211111_amd64.tar.xz
system          gentoo-current-default_20200310_amd64.tar.xz
system          opensuse-15.3-default_20210925_amd64.tar.xz
system          rockylinux-8-default_20210929_amd64.tar.xz
system          ubuntu-16.04-standard_16.04.5-1_amd64.tar.gz
system          ubuntu-18.04-standard_18.04.1-1_amd64.tar.gz
system          ubuntu-20.04-standard_20.04-1_amd64.tar.gz
system          ubuntu-21.04-standard_21.04-1_amd64.tar.gz
system          ubuntu-21.10-standard_21.10-1_amd64.tar.zst

Из которого, на всякий случай, было скачано сразу несколько образов:

pveam download local ubuntu-18.04-standard_18.04.1-1_amd64.tar.gz
pveam download local ubuntu-20.04-standard_20.04-1_amd64.tar.gz
pveam download local alpine-3.15-default_20211202_amd64.tar.xz

Далее, приступаем к созданию контейнеров LXC перейдя в веб панель, которая должна быть доступна по адресу https://<ip_address>:8006 с логином root и паролем, который был задан при установке. Рассказывать о том что где находится в этой панели думаю нет смысла, т.к. все достаточно просто и интуитивно понятно. Встаем на наш хост и нажимаем кнопку Create CT, чтобы создать контейнер. Для начала создадим контейнер c именем kube-master, на котором соответственно у нас будет развернута master-нода kubernetes. Т.к. внутри у нас должен работать docker, то убираем галочку напротив Unprivileged container, задаем пароль или публичный ключ ssh, выбираем образ ubuntu-20.04-standard_20.04-1_amd64.tar.gz, устанавливаем колличество ресурсов процессора, диска и памяти. В моем случае это: 15Гб диск, 4Гб ОЗУ и 4 потока. Для начала мне кажется этого достаточно, а в случе необходимости, всегда можно добавить. Так же сразу следует подумать о том, чтобы контейнер автоматически запускался при перезагрузке хоста виртуализации. Для этого нужно в его настройках переключить параметр Sart at boot в Yes. Теперь в консоли ‘Shell’ сервера ВМ нужно добавить в получившийся файл конфигурации, находящийся в /etc/pve/lxc/<ID контейнера>.conf, следующие строки приведенные ниже. Это нужно для того чтобы можно было запустить docker с драйвером overlay2, с которым дружит kebernetes и чтобы kubelet имел необходимые права.

lxc.apparmor.profile: unconfined
lxc.cap.drop:
lxc.cgroup.devices.allow: a
lxc.mount.auto: proc:rw sys:rw

Теперь переходим в консоль самого контейнера и устанавливаем docker

sudo apt-get remove docker docker-engine docker.io containerd runc
sudo apt-get update
sudo apt-get upgrade
sudo apt-get install ca-certificates curl gnupg lsb-release
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt-get update
sudo apt-get install docker-ce docker-ce-cli containerd.io

Теперь нужно проверить какой драйвер хранилища (Storage Driver) использует docker:

docker info | grep 'Storage Driver'

Если это что-то отличное от overlay2 — значит на хостовой машине, на самом proxmox, не подключен модуль ядра overlay. Чтобы это исправить делаем следующее:

echo "overlay" >> /etc/modules-load.d/modules.conf

перезагружаем proxmox, и проверяем что модуль подключился:

lsmod | grep 'overlay'

Если предыдущая операция прошла успешно, но драйвер все равно не overlay2, то можно попробовать указать его через конфигурационный файл /etc/docker/daemon.json

sudo cat <<EOF >/etc/docker/daemon.json
{
  "storage-driver": "overlay2"
}
EOF
systemctl restart docker

Так же для успешного запуска kubelet необходимо сделать следующее:

sudo ln -s /dev/console /dev/kmsg
sudo echo 'L /dev/kmsg - - - - /dev/console' > /etc/tmpfiles.d/kmsg.conf

Все готово для создания кластера kubernetes. Все остальные ноды будут разворачиваться аналогичным образом.

Разворачиваем кластер Kubernates

Шаг 1. Установка основных компонентов

Изначально я руководствовался официальной документацией и начал с этой страницы.
Ниже идет текст взятый отсюда.

Устанавливаем (если не устанавливали ранее) стандартный набор для добавления внешних репозиториев + open-iscsi

sudo apt-get update
sudo apt-get install -y apt-transport-https ca-certificates curl open-iscsi

Скачиванм gpg ключ

sudo curl -fsSLo /usr/share/keyrings/kubernetes-archive-keyring.gpg https://packages.cloud.google.com/apt/doc/apt-key.gpg

Добавляем сам репозиторий в apt

echo "deb [signed-by=/usr/share/keyrings/kubernetes-archive-keyring.gpg] https://apt.kubernetes.io/ kubernetes-xenial main" | sudo tee /etc/apt/sources.list.d/kubernetes.list

И устанавливаем пакеты, которые будем использовать

sudo apt-get update
sudo apt-get install kubeadm=1.23.0-00 kubelet=1.23.0-00 kubectl=1.23.0-00 kubernetes-cni=0.8.7-00 
sudo apt-mark hold kubeadm kubectl kubelet kubernetes-cni

На этой стадии можно создать резервную копию нашего контейнера или выполнить операцию ‘Convert to template’. Я выбрал второй вариант, чтобы при разворачивании последующих контейнеров не устанавливать docker и kubernetes. Единственное что нужно помнить при таком подходе — это что после создания клона из шаблона необходимо менять IP-адрес, ну и возможно параметры выделяемых ресурсов, таких как диск, память, процессор.

Создаем файл конфигурации для оркестратора kubeadm, где вместо <server_ip_address> вписываем IP адрес контейнера:

sudo cat <<EOF >kubeadm-config-master.yaml
# kubeadm-config-master.yaml
apiVersion: kubelet.config.k8s.io/v1beta1
kind: KubeletConfiguration
cgroupDriver: systemd
featureGates:
  NodeSwap: true
failSwapOn: false
memorySwap:
  SwapBehavior: LimitedSwap
---
apiVersion: kubeadm.k8s.io/v1beta2
kind: ClusterConfiguration
api:
  advertiseAddress: <server_ip_address>
networking:
  podSubnet: 10.244.0.0/16
EOF

Разворачиваем мастер-ноду, с отключенной проверкой наличия swap и системы.

sudo kubeadm init --config kubeadm-config-master.yaml --ignore-preflight-errors=swap,SystemVerification

проверка системы отключается чтобы не ловить ошибку подобного вида:

[WARNING SystemVerification]: failed to parse kernel config: unable to load kernel module: "configs", output: "modprobe: FATAL: Module configs not found in directory /lib/modules/5.13.19-2-pve\n", err: exit status 1

Создаем пользователя, который будет админом кластера adduser admin0 и выполняем следующие действия:

mkdir /home/admin0/.kube
sudo cp -i /etc/kubernetes/admin.conf /home/admin0/.kube/config
sudo chown admin0:admin0 /home/admin0/.kube/config

Дальнейшие действия будем выполнять от имени пользователя admin0

Далее нам необходимо установить аддон, который будет реализовать CNI (Container Network Interface). Собственно тот интерфейс, через который контейнеры будут общаться между собой.

Обратившись к README.md официального репозитория на github’e, устанавливаем этот аддон:

kubectl apply -f https://raw.githubusercontent.com/coreos/flannel/master/Documentation/kube-flannel.yml

Чтобы при нажатии кнопки Tab происходило автодополнение команд kubectl, выполним такую команду:

kubectl completion bash >/etc/bash_completion.d/kubectl

Так же для удобства деплоя стандартных сервисов и не только установим helm, выполнив следующий набор действий:

curl https://baltocdn.com/helm/signing.asc | sudo apt-key add -
sudo apt-get install apt-transport-https --yes
echo "deb https://baltocdn.com/helm/stable/debian/ all main" | sudo tee /etc/apt/sources.list.d/helm-stable-debian.list
sudo apt-get update
sudo apt-get install -y helm
Шаг 2. Подключение worker-нод

Воркер-ноды системно ничем не отличаются от мастер-ноды, и по этой причине я просто клонировал два контейнера из созданного мной ранее шаблона контейнера, поменял IP-адреса, добавил им дискового пространства — до 50Гб и урезал колличество потоков CPU до двух. Думаю, пока хватит.

После успешного запуска master-ноды в конце вывода должна быть строка для подключения worker-нод

kubeadm join <IP_мастер-ноды>:6443 --token g7mku9.52m1do9u3t58mfeb \
    --discovery-token-ca-cert-hash sha256:1be8897143e616cb2ce270865a12eb9e09242eea8da461a250f481554cc9fe1b 

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

kubeadm token create --print-join-command

Единственное, чтобы kubeadm не ругался при проверке системы, к этой строке нужно будет добавить ключ --ignore-preflight-errors=SystemVerification, т.е. результирующая строка подключения worker-нод будет выглядеть примерно так:

kubeadm join <IP_мастер-ноды>:6443 --token g7mku9.52m1do9u3t58mfeb \
    --discovery-token-ca-cert-hash sha256:1be8897143e616cb2ce270865a12eb9e09242eea8da461a250f481554cc9fe1b \
  --ignore-preflight-errors=SystemVerification

В итоге на местер-ноде, выполнив команду kubectl get nodes, мы должны увидеть следующую картину:

NAME           STATUS   ROLES                  AGE     VERSION
kube-master    Ready    control-plane,master   56m     v1.23.0
kube-worker1   Ready    <none>                 9m49s   v1.23.0
kube-worker2   Ready    <none>                 52s     v1.23.0
Шаг 3. Установка балансировщика MetalLB и ingress-контроллера

MetalLB нужен чтобы ingress-контроллер знал в каком диапазоне IP-адресов он может работать. В моем случае это будет лишь один адрес, но тем ни мение — на всякий случай, лучше чтобы он был.
Для его установки, сначала, нам понадобится создать файл конфигурации, где вместо <IP_ingress-node> указать IP мастер-ноды:

cat <<EOF > metallb-values.yaml
# metallb-values.yaml
configInline:
  address-pools:
   - name: default
     protocol: layer2
     addresses:
     - <IP_ingress-node>/32
EOF

затем немного изменить конфигурацию сервиса kube-proxy

kubectl get configmap kube-proxy -n kube-system -o yaml | \
sed -e "s/strictARP: false/strictARP: true/" | \
kubectl apply -f - -n kube-system

после чего можно установить сам MetalLB по средствам helm. Для этого делаем слудующее:

helm repo add metallb https://metallb.github.io/metallb
helm install metallb metallb/metallb -f metallb-values.yaml --namespace metallb-system --create-namespace

Теперь убеждаемся, что все запустилось с помощью команды kubectl get all -n metallb-system. Вывод должен быть примерно таким:

NAME                                   READY   STATUS    RESTARTS   AGE
pod/metallb-controller-c55c89d-bz2kq   1/1     Running   0          13m
pod/metallb-speaker-7hld5              1/1     Running   0          13m
pod/metallb-speaker-lxzxl              1/1     Running   0          13m
pod/metallb-speaker-wjzp4              1/1     Running   0          13m

NAME                             DESIRED   CURRENT   READY   UP-TO-DATE   AVAILABLE   NODE SELECTOR            AGE
daemonset.apps/metallb-speaker   3         3         3       3            3           kubernetes.io/os=linux   13m

NAME                                 READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/metallb-controller   1/1     1            1           13m

NAME                                         DESIRED   CURRENT   READY   AGE
replicaset.apps/metallb-controller-c55c89d   1         1         1       13m

Если все нормально, то приступаем к установке ingress-контроллера.

Добавим репозиторий haproxy в helm:

helm repo add haproxytech https://haproxytech.github.io/helm-charts

И установим сам haproxy-ingress:

helm install haproxy-ingress haproxytech/kubernetes-ingress\
  --namespace ingress-controller\
  --create-namespace\
  --version 1.17.11\
  --set controller.service.type=LoadBalancer

Проверяем работу сервисов ingress-контроллера командой kubectl -n ingress-controller get services. В результате мы должны увидеть нечто подобное:

NAME                                                 TYPE           CLUSTER-IP     EXTERNAL-IP   PORT(S)                                     AGE
haproxy-ingress-kubernetes-ingress                   LoadBalancer   10.103.170.8   192.168.0.3   80:31186/TCP,443:30931/TCP,1024:31746/TCP   2m19s
haproxy-ingress-kubernetes-ingress-default-backend   ClusterIP      None           <none>        8080/TCP                                    2m19s

Где ключевым моментом будет являться наличие значения в колонке "EXTERNAL-IP". Так же можно попробовать открыть этот адрес в браузере и получить 404 ошибку от бэкенда по умолчанию.

Шаг 4. Установка storage-контроллера OpenEBS

Это необходимо для того чтобы было проще управлять persistent-хранилищами наших деплойментов и не думать каждый раз где и что нам создать. Данный контроллер позволяет автоматизировать сей процесс. Конечно, в случае организации более масштабного клатсера следовало бы использовать Ceph, GlusterFS или хотя бы NFS, но для небольшого кластера, я думаю, хватит и базовой установки.
Чтобы установить его в наш кластер необходимо выполнить следующие действия:

helm repo add openebs https://openebs.github.io/charts
helm repo update
helm install openebs --namespace openebs openebs/openebs --create-namespace --set ndm.enabled=false --set ndmOperator.enabled=false

После чего в нашем кластере должно появиться два класса хранилищ. Проверяем, выполнив команду kubectl get sc. В результате мы должны увидеть примерно такой вывод:

NAME               PROVISIONER        RECLAIMPOLICY   VOLUMEBINDINGMODE      ALLOWVOLUMEEXPANSION   AGE
openebs-hostpath   openebs.io/local   Delete          WaitForFirstConsumer   false                  19m

Собственно openebs-hostpath я и собираюсь задействовать в перспективе. Этот класс обеспечивает автоматическое создание Persistent Volume на ноде, где будет распологаться POD. Создаваемые им хранилища находятся (в случае использования настроек по умолчанию) в папке /var/openebs/local/ на нодах где будет запрашиваться пространство для постоянного хранилища.

Шаг 5. Установка cert-manager

Данный менеджер упрощает, как следует из названия, процесс создания TLS-сертификатов. И на мой взгляд, просто необходим, если вы планируете публикацию сервисов в интернете с использованием центров сертификации, например — LetsEncrypt.

helm repo add jetstack https://charts.jetstack.io
helm repo update
kubectl apply -f https://github.com/jetstack/cert-manager/releases/download/v1.6.1/cert-manager.crds.yaml
helm install \
  cert-manager jetstack/cert-manager \
  --namespace cert-manager \
  --create-namespace \
  --version v1.6.1

Так же можно сразу создать эмитент для кластера, который будет подписывать наши сертификаты, на примере Let’s Encrypt. Для этого создаем манифест примерно такого содержания:

apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: letsencrypt
spec:
  acme:
    # URL ACME сервера
    server: https://acme-v02.api.letsencrypt.org/directory
    # Email адрес используемый для ACME регистрации
    email: your@mail.com
    # Имя секрета используемого для хранения приватного ключа ACME-аккаунта
    privateKeySecretRef:
      name: letsencrypt
    # Добавление HTTP-01 challenge provider
    solvers:
    - http01:
        ingress:
          class: haproxy

Проверить список эмитентов можно выполнив kubectl get clusterissuers. Результат вывода примерно такой:

NAME          READY   AGE
letsencrypt   True    1h
Опциональные действия

Если есть желание управлять кластером удаленно, на удаленной машине в рамках локальной сети, то нужно будет обеспечить на ней наличие kubectl и конфига подключения, который, на мой взгляд, легче всего получить так:

curl -LO https://storage.googleapis.com/kubernetes-release/release/v1.23.0/bin/linux/amd64/kubectl
chmod +x ./kubectl
sudo chown root:root ./kubectl
sudo mv ./kubectl /usr/local/bin/kubectl
mkdir ./.kube
scp root@kube-master:/etc/kubernetes/admin.conf ~/.kube/config
kubectl completion bash | sudo tee /etc/bash_completion.d/kubectl > /dev/null
echo -e "\nexport KUBE_EDITOR=nano" >> .bashrc

Так же не помешает helm, который ставится по стандартной схеме, так же как на мастер-ноде kubernetes.

На этом можно считать что задача выполнена. Следующим шагом будет деплой WordPress + MySQL, но это будет уже следующей статьей.