Обновление ОС рабочих узлов кластера k8s при помощи KubeOS

Введение

В сегодняшней статье мы затронем такую довольно важную тему, как автоматизация обновлений ОС рабочих узлов кластера k8s. А чтобы не демонстрировать кластер k8s без полезной нагрузки, мы установим на него KubeVirt и запустим внутри контейнера виртуальную машину на базе ОС OpenScaler 22.03 SP3.

При использовании обычных, классических ОС (где обновление ОС следует выполнять при помощи пакетного менеджера) следует использовать сторонние инструменты (к примеру, такие как ansible), которые позволят устанавливать обновления ОС целиком на весь кластер k8s или его выбранные узлы, делая ОС рабочих узлов одинаковыми по уровню обновлений. При этом всё равно остаётся проблема при введении новых узлов в кластер, потому что, как правило, эти узлы устанавливаются из какого-то шаблона ОС, а только потом (если не забыли) обновляются до уровня обновлений всего кластера. Проблемы можно было бы избежать, если все рабочие узлы использовали бы один и тот же базовый образ ОС и обновление ОС происходило простой сменой этого базового образа на обновлённый путём обычной перезагрузки. Таким образом, новые сервера сразу бы получали нужный (по уровню обновлений) образ ОС для работы и были бы готовы сразу добавляться в кластер k8s и вступать в работу. Вот для автоматизации подобного подхода и предназначен рассматриваемый в данной статье проект KubeOS.

Несколько слов о KubeOS

Проект KubeOS входит в портфолио технологий, которые разработаны внутри экосистемы дистрибутива OpenEuler. Ознакомится с подробной документацией по KubeOS можно пройдя по ссылке https://docs.openeuler.org/en/docs/22.03_LTS_SP3/docs/KubeOS/about-kubeos.html.

Если описать вкратце эту технологию, то можно упомянуть, что основана она на формировании загрузочных образов (как для виртуальной машины, так и для «железного» сервера), на базе которых разворачиваются как виртуальные, так и аппаратные рабочие узлы кластера k8s. Сразу следует сделать оговорку, что речь идёт именно о рабочих узлах, мастер узлы (control plane) работать под управлением KubeOS не могут.

Для лучшего понимания архитектуры KubeOS можно обратиться к следующей схеме:

Иллюстрация взята с сайта https://docs.openeuler.org/en/docs/22.03_LTS_SP3/docs

Сначала пробежимся по этой схеме и выделим на ней следующие компоненты KubeOS:

  • ContainerOS — это ОС рабочего узла, образ которой сформирован KubeOS.
  • os-agent — сервис, непосредственно запущенный на рабочем узле. Предназначен для загрузки новой версии образа ОС, записи её на диск и изменении настройки загрузчика ОС, чтобы при последующей перезагрузке рабочий узел загрузился с использованием уже новой версии образа ОС.
  • os-operator и os-proxy — это программные компоненты KubeOS, запущенные в виде контейнеров внутри кластера k8s. При этом os-operator запускается на master узлах, а os-proxy на рабочих узлах. Далее рассмотрим их по отдельности:
    • os-operator — это менеджер обновлений KubeOS, который проверяет соответствие версии ОС на рабочих узлах и заданной конфигурации. Если он видит, что в заданной конфигурации версия ОС изменилась, он помечает все узлы для которых требуется обновление и эта информация становится доступной os-proxy.
    • os-proxy — компонент KubeOS, который работает непосредственно на рабочих узлах и проверяет информацию от os-operator о наличии обновлений ОС для своего узла. Если он замечает, что такая информация есть, то информация о наличии образа ОС с новой версией передаётся os-agent, а с узла выселяются (evicted) контейнеры.

Теперь рассмотрим несколько подробнее устройство рабочего узла с точки зрения работы с переключением версий образов ОС. Для этого нам поможет следующая иллюстрация:

Иллюстрация взята с сайта https://docs.openeuler.org/en/docs/22.03_LTS_SP3/docs

На приведённой иллюстрации рабочий узел загружен с использованием раздела rootA, а в раздел rootB записывается новая версия, которая стала доступной на сервере образов.

Давайте рассмотрим, что это за разделы такие. Как мы видим, диск рабочего узла состоит из 4 разделов:

  • boot — тут находится стандартный раздел efi.
  • rootA и rootB — это два одинаковых раздела, которые предназначены для попеременной записи в них образов с обновлениями ОС. При этом, следует заметить, что внутри ОС файловая система образа смонтирована только на чтение.
  • Persist — раздел, с постоянными данными, которые сохраняются при перезагрузке. Именно в нём находятся разделы /etc, /opt, /var, которые монтируются в корневой раздел при помощи технологии overlayfs.

При появлении новой версии образа ОС рабочего узла, os-agent записывает его в раздел, который не является сейчас активным, настраивает загрузчик на его использование и после успешной записи узел автоматически перезагружается.

Используемый стенд

Для демонстрации возможностей KubeOS мы будем использовать следующий стенд (все сервера — это виртуальные машины развёрнутые в среде виртуализации oVirt):

  • 172.17.3.80 – сервер kubeos-oe2203sp3-prepare (под управлением ОС openEuler 22.03 SP3). На этой машине мы будем готовить образы KubeOS для рабочих узлов.
  • 172.17.3.71 – сервер kubeos-oe2203sp3-k8s-m1 (под управлением ОС openEuler 22.03 SP3). Это мастер узел кластера k8s. Мы подготовили его заранее. Для демонстрации и тестирования нам не нужно обеспечивать отказоустойчивость, поэтому одного мастер узла нам вполне хватит.
  • узлы 172.17.3.72 kubeos-oe2203sp3-k8s-w1 и 172.17.3.73 kubeos-oe2203sp3-k8s-w2 — это рабочие узлы кластера k8s, образ для которых мы готовим при помощи KubeOS.
  • registry контейнеров по адресу 172.17.1.91:5000

Несколько важных замечаний:

  1. Так как на момент написания статьи самая новая версия k8s для openEuler была v1.28.2, то мы будем использовать именно её. В качестве runtime для запуска контейнеров мы будем использовать containerd. Здесь следует заметить, что в репозитории openEuler находится версия containerd 1.2.0, тогда как актуальная версия 1.7.16. Поэтому мы в нашем кластере будет использовать версию 1.7.16, упаковав её в rpm пакет и добавив его в образ KubeOS рабочего узла.
  2. Так же, сразу стоит отметить, что в статье мы не будем приводить (чтобы излишне не перегружать её листингами файлов) конфигурацию для containerd для его работы с insecure registry 172.17.1.91:5000. Информацию по настройке containerd для подобных случаев можно подсмотреть в официальной документации по самой containerd.
  3. Для демонстрации смены версии KubeOS мы подготовим собственный rpm пакет, в котором будет повышать версию в строчке содержащейся в текстовом файле /usr/share/test-package
  4. В нашем кластере k8s мы используем в качестве CNI проект calico. И так как в KubeOS недоступен для записи раздел /usr, то нам придётся изменить пути размещения плагинов для kubelet в настройке следующим образом:

kubectl -n calico-system edit installations.operator.tigera.io default

Правим все пути вида flexVolumePath: /usr/libexec/kubernetes/kubelet-plugins/volume/exec/ на flexVolumePath: /var/lib/kubelet/plugins/volume/exec/

Итого, подготовим теперь краткий план, предстоящей демонстрации:

  • установка KubeOS и подготовка базового образа для развёртывания рабочих узлов k8s
  • Установка и настройка рабочих узлов k8s на базе KubeOS
  • Подготовка и запуск в кластере k8s os-operator и os-proxy
  • Выпуск новой версии базового образа KubeOS для рабочих узлов и его установка на рабочие узлы
  • Не забываем, что мы хотели продемонстрировать KubeVirt — установим его на наш k8s и настроим
  • Запуск при помощи KubeVirt виртуальных машин на базе OpenScaler 22.03 SP3 в кластере k8s.

Установка KubeOS и подготовка образа для рабочих узлов

Действия описанные в данном разделе мы будем производить на сервере kubeos-oe2203sp3-prepare.

На сервере kubeos-oe2203sp3-prepare отключим SELinux и установим на сервер следующие пакеты:

dnf install qemu-img bc parted tar dosfstools docker
dnf install KubeOS KubeOS-scripts

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

Создадим их:

mkdir -p /opt/kubeOS/scripts/www-repo/Packages /opt/kubeOS/scripts/new-version

Итого, у нас должна получиться следующая структура каталогов:

/opt/kubeOS/
├── bin
├── files
└── scripts
    ├── 00bootup
    ├── common
    ├── create
    ├── new-version
    └── www-repo
        └── Packages

 

Теперь, давайте установим nginx и настроим его в качестве веб сервера, который будет обеспечивать работу репозитория пакетов и публикацию новых версий образов KubeOS

dnf install nginx

в конфиге /etc/nginx/nginx.conf меняем root у дефолтного сервера:

    server {
        listen       80;
        listen       [::]:80;
        server_name  _;
        root         /opt/kubeOS/scripts;

        # Load configuration files for the default server block.
        include /etc/nginx/default.d/*.conf;

        error_page 404 /404.html;
        location = /404.html {
        }

        error_page 500 502 503 504 /50x.html;
        location = /50x.html {
        }
    }

 

И запускаем nginx:

systemctl enable --now nginx

Теперь давайте подготовим 2 пакета, которые мы будем добавлять в формируемый образ KubeOS. В первый пакет в включим новую версию containerd, а так же конфигурационные файлы которые обеспечивают настройки, необходимые для работы k8s. Во второй пакет мы будем добавлять тестовый файл /usr/share/test-package на котором мы будем тестировать обновление образов KubeOS

Ставим на сервер необходимые пакеты:

dnf install rpmdevtools rpmlint rpm-build automake autoconf pkgconfig git make cmake libtool libtool-ltdl gcc gcc-c++

Даём команду rpmdev-setuptree для формирования древа каталогов для сборки пакетов.

Подготовка пакета containerd-all

Качаем https://github.com/containerd/containerd/releases/tag/v1.7.16 архив с бинарными файлами containerd, распаковываем его и помещаем файлы в каталог SOURCES. Туда же добавляем файл для запуска containerd с использованием systemd: https://raw.githubusercontent.com/containerd/containerd/main/containerd.service. Не забываем исправить внутри этого файла путь к бинарнику с /usr/local/bin/containerd на /usr/bin/containerd.

Качаем последнюю стабильную версию runc https://github.com/opencontainers/runc/releases и помещаем бинарный файл в SOURCES.

Теперь качаем c https://github.com/kubernetes/kubernetes/blob/master/CHANGELOG/CHANGELOG-1.28.md#server-binaries бинарники для k8s и так же размещаем их в SOURCES.

Теперь подготовим файлы для systemd, которые позволят автоматически монтировать в /opt раздел из /persist. Это сделать необходимо, по причине того, что по умолчанию в образе KubeOS раздел /opt пустой и доступен только для чтения, а нам он понадобится для работы k8s.

Файл 98-opt.preset:

enable opt.mount

enable opt-create.service

 

 

Файл opt-create.service:

[Unit]

Description=create opt persist dirs

DefaultDependencies=no

After=etc.mount

Wants=etc.mount

Before=opt.mount

[Service]

Type=oneshot

ExecStart=/bin/bash -c 'mkdir -p /persist/opt /persist/optwork'

[Install]

WantedBy=graphical.target

 

 

Файл opt.mount:

[Unit]

Description=opt Dir

DefaultDependencies=no

Wants=opt-create.service

After=opt-create.service

[Mount]

What=overlay

Where=/opt

Type=overlay

Options=upperdir=/persist/opt,lowerdir=/opt,workdir=/persist/optwork

[Install]

WantedBy=graphical.target

 

 

Их так же помещаем в SOURCES.

Теперь давайте подготовим файлы, которые настроят ОС рабочего узла для работы с k8s.

 

 

Файл modules-k8s.conf:

br_netfilter

 

 

Файл sysctl-k8s.conf:

net.ipv4.ip_forward=1

 

 

Теперь подготовим файл с спецификацией пакета containerd-all.spec:

Name: containerd-all

Version: 0.1

Release: 17

Summary: Package with containerd for kubernetes v1.28.2

Source0: containerd

Source1: containerd-shim

Source2: containerd-shim-runc-v1

Source3: containerd-shim-runc-v2

Source4: containerd-stress

Source5: ctr

Source6: runc

Source7: config.toml

Source10: modules-k8s.conf

Source11: sysctl-k8s.conf

Source20: containerd.service

Source21: opt-create.service

Source22: opt.mount

Source23: 98-opt.preset

Requires: bash setup util-linux

Requires(post): systemd-units

Requires(postun): systemd-units

%description

Package with containerd for kubernetes v1.28.2

%build

%install

rm -rf $RPM_BUILD_ROOT

install -d -m0755 $RPM_BUILD_ROOT/%{_bindir}

install -m 755 %{SOURCE0} $RPM_BUILD_ROOT%{_bindir}

install -m 755 %{SOURCE1} $RPM_BUILD_ROOT%{_bindir}

install -m 755 %{SOURCE2} $RPM_BUILD_ROOT%{_bindir}

install -m 755 %{SOURCE3} $RPM_BUILD_ROOT%{_bindir}

install -m 755 %{SOURCE4} $RPM_BUILD_ROOT%{_bindir}

install -m 755 %{SOURCE5} $RPM_BUILD_ROOT%{_bindir}

install -m 755 %{SOURCE6} $RPM_BUILD_ROOT%{_bindir}

install -d -m0755 $RPM_BUILD_ROOT%{_sysconfdir}/containerd

install -m 644 %{SOURCE7} $RPM_BUILD_ROOT%{_sysconfdir}/containerd/config.toml

install -d -m0755 $RPM_BUILD_ROOT%{_sysconfdir}/modules-load.d

install -m 644 %{SOURCE10} $RPM_BUILD_ROOT%{_sysconfdir}/modules-load.d/k8s.conf

install -d -m0755 $RPM_BUILD_ROOT%{_sysconfdir}/sysctl.d

install -m 644 %{SOURCE11} $RPM_BUILD_ROOT%{_sysconfdir}/sysctl.d/100-k8s.conf

install -d -m0755 $RPM_BUILD_ROOT/%{_unitdir}

install -m 755 %{SOURCE20} $RPM_BUILD_ROOT/%{_unitdir}

install -m 755 %{SOURCE21} $RPM_BUILD_ROOT/%{_unitdir}

install -m 755 %{SOURCE22} $RPM_BUILD_ROOT/%{_unitdir}

install -d -m0755 $RPM_BUILD_ROOT/%{_presetdir}

install -m 755 %{SOURCE23} $RPM_BUILD_ROOT/%{_presetdir}

%clean

rm -rf $RPM_BUILD_ROOT

%post

systemctl enable containerd.service

systemctl enable opt-create.service

systemctl enable opt.mount

%files

%defattr(-,root,root)

%attr(0755,root,root) %{_bindir}/containerd

%attr(0755,root,root) %{_bindir}/containerd-shim

%attr(0755,root,root) %{_bindir}/containerd-shim-runc-v1

%attr(0755,root,root) %{_bindir}/containerd-shim-runc-v2

%attr(0755,root,root) %{_bindir}/containerd-stress

%attr(0755,root,root) %{_bindir}/ctr

%attr(0755,root,root) %{_bindir}/runc

%attr(0755,root,root) %dir %{_sysconfdir}/containerd

%attr(0644,root,root) %config %{_sysconfdir}/containerd/config.toml

%attr(0644,root,root) %config %{_sysconfdir}/modules-load.d/k8s.conf

%attr(0644,root,root) %config %{_sysconfdir}/sysctl.d/100-k8s.conf

%attr(0644,root,root) %{_unitdir}/containerd.service

%attr(0644,root,root) %{_unitdir}/opt-create.service

%attr(0644,root,root) %{_unitdir}/opt.mount

%attr(0644,root,root) %{_presetdir}/98-opt.preset

%changelog

* Mon May 13 2024 test <test@openscaler.ru> - 0.1-17

- unit

 

 

В результате мы должны иметь следующую структуру каталогов для сборки пакета:

├── BUILD

├── BUILDROOT

├── RPMS

│   └── x86_64

├── SOURCES

│   ├── 98-opt.preset

│   ├── config.toml

│   ├── containerd

│   ├── containerd.service

│   ├── containerd-shim

│   ├── containerd-shim-runc-v1

│   ├── containerd-shim-runc-v2

│   ├── containerd-stress

│   ├── ctr

│   ├── kubeadm

│   ├── kubectl

│   ├── kubelet

│   ├── modules-k8s.conf

│   ├── opt-create.service

│   ├── opt.mount

│   ├── runc

│   ├── sysctl-k8s.conf

├── SPECS

│   ├── containerd-all.spec

└── SRPMS

 

 

Собираем пакет и размещаем его каталоге нашего репозитория:

rpmbuild -bb SPECS/containerd-all.spec

mv RPMS/x86_64/containerd-all-* /opt/kubeOS/scripts/www-repo/Packages

cd /opt/kubeOS/scripts/www-repo/

createrepo .

 

Так как при сборке KubeOS образа надо указать файл с используемыми репозиториями пакетов (а у нас в образе KubeOS будет такая же версия ОС, как и на самом сервере kubeos-oe2203sp3-prepare), то мы или делаем отдельную копию файла /etc/yum.repos.d/openEuler.repo или просто добавляем в этот файл наш репозиторий:

[for-k8s]

name=for-k8s

baseurl=http://172.17.3.80/www-repo

enabled=1

gpgcheck=0

 

Подготовка тестового пакета

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

Для этого в существующее древо сборки пакетов добавляем 2 файла:

 

тестовый файл SOURCES/test-package:

version XX

 

и файл спецификации SPECS/test-package.spec:

Name: test-package

Version: 1.0

Release: XX

Summary: Test package

Source0: test-package

Requires: bash setup util-linux

%description

Test Package

%build

%install

rm -rf $RPM_BUILD_ROOT

install -d -m0755 $RPM_BUILD_ROOT/%{_datadir}

install -m 755 %{SOURCE0} $RPM_BUILD_ROOT%{_datadir}/test-package

%clean

rm -rf $RPM_BUILD_ROOT

%post

%files

%defattr(-,root,root)

%attr(0644,root,root) %{_datadir}/test-package

%changelog

* Thu May 16 2024 test <test@openscaler.ru> - 1.0-XX

- testing

Во время сборки мы будем заменять XX на нужную нам версию.

В самом начале мы выставим версию пакета в “1” и содержимое файла test-package тоже сделаем «version 1»

Соберём пакет:

rpmbuild -bb SPECS/test-package.spec

поместим получившийся RPM пакет в наш репозиторий /opt/kubeOS/scripts/www-repo/Packages и обновим его метаданные.

Теперь нам осталось только добавить наши пакеты в список пакетов, которые будут установлены в базовый образ KubeOS. Для этого мы добавляем их в файл /opt/kubeOS/scripts/rpmlist так, чтобы он имел следующий вид:

kernel

passwd

dhcp

NetworkManager

openssh-server

containerd-all

cri-tools

kubeadm

kubelet

kubectl

containernetworking-plugins

socat

conntrack-tools

ebtables

ethtool

rsyslog

vi

net-tools

hwinfo

dracut

coreutils

gawk

parted

dosfstools

test-package

 

Сборка базового образа KubeOS

После того, как мы подготовили все наши пакеты и разместили их в репозитории, мы можем приступать к сборке нашего базового образа KubeOS, с которого мы будем устанавливать наши рабочие узлы k8s.

Для этого переходим в каталог /opt/kubeOS/scripts/ и даём команду:

./kbimg.sh create vm-image -p /etc/yum.repos.d/openEuler.repo -v v1 -b ../bin/os-agent -e '''$1$SALT$Pf6lsQR45JfQZQgPfpkJ91'''

Здесь в этой команде мы создаём образ для виртуальной машины с использование файла репозиториев /etc/yum.repos.d/openEuler.repo, указываем, что версия у нас будет v1 и сразу задаём хэш пароля (который заблаговременно подготовлен при помощи утилиты openssl). В качестве примера пароль у нас будет «123456»

После того, как эта команда закончила свою работу, в каталоге /opt/kubeOS/scripts/ появятся файлы:

  • system.qcow2 (образ виртуальной машины в qcow2 формате)
  • system.img (образ виртуальной машины в raw формате)
  • update.img (образ для обновления)

Для развёртывания рабочих узлов нам необходимо использовать образ system.qcow2. Мы переписываем его в систему виртуализации, используем его как базовый образ и копируем с него образа дисков для наших двух рабочих узлов.

Настройка рабочих узлов кластера k8s на базе образа KubeOS

Запускаем наши виртуальные машины рабочих узлов, дожидаемся их загрузки и логинимся в систему. Далее я перечислю те базовые настройки которые мы должны сделать на каждом рабочем узле (останавливаться на них подробно не вижу смысла, ограничимся их простым перечислением):

  • настроить сетевую конфигурацию на использование статического ip
  • настроить hostname рабочего узла
  • добавить все узлы k8s кластера в файл /etc/hosts
  • активируем службу kubelet (systemctl enable kubelet.service)

Проделав эти базовые операции мы добавляем наши рабочие узлы в кластер k8s используя команду kubeadm:

kubeadm join 172.17.3.71:6443  --cri-socket=unix:///var/run/containerd/containerd.sock \
--token 50bvl3.v0d1pu12w4rxhhhy \
--discovery-token-ca-cert-hash sha256:06b663d3e112a19012e2d2db86adcba369038532ad666d82ad8e41b6b506b87a

Проверяем вывод kubectl и убеждаемся, что рабочие узлы стартовали и успешно подключились к кластеру k8s:

kubectl get no -o wide

NAME STATUS ROLES AGE VERSION INTERNAL-IP EXTERNAL-IP OS-IMAGE KERNEL-VERSION CONTAINER-RUNTIME

kubeos-oe2203sp3-k8s-m1 Ready control-plane 14d v1.28.2 172.17.3.71 <none> openEuler 22.03 (LTS-SP3) 5.10.0-197.0.0.110.oe2203sp3.x86_64 containerd://1.7.16

kubeos-oe2203sp3-k8s-w1 Ready <none> 1h v1.28.2 172.17.3.72 <none> KubeOS v1 5.10.0-199.0.0.112.oe2203sp3.x86_64 containerd://1.7.16

kubeos-oe2203sp3-k8s-w2 Ready <none> 1h v1.28.2 172.17.3.73 <none> KubeOS v1 5.10.0-199.0.0.112.oe2203sp3.x86_64 containerd://1.7.16

Убеждаемся в том, что у нас используется первая версия нашего тестового пакета test-package в образе KubeOS для рабочих узлов. Для этого мы можем зайти на любой рабочий узел и проверяем содержимое файла /usr/share/test-package:

cat /usr/share/test-package

version 1

Подготовка и запуск в кластере k8s os-operator и os-proxy

Теперь нам надо подготовить образы для os-operator и os-proxy, а также подготовить для них yaml манифесты для развёртывания на k8s.

При написании yaml файлов мы будем использовать примеры из репозитория https://gitee.com/openeuler/KubeOS/tree/master/docs/example/config

На сервере kubeos-oe2203sp3-prepare переходим в каталог /opt/kubeOS и подготавливаем файлы:

Dockerfile-operator:
FROM openeuler/openeuler:22.03-lts-sp3
COPY ./bin/operator /operator
RUN chown root:root /operator && chmod 500 /operator
ENTRYPOINT ["/operator"] 


Dockerfile-proxy:
FROM openeuler/openeuler:22.03-lts-sp3 COPY ./bin/proxy /proxy RUN chown root:root /proxy && chmod 500 /proxy ENTRYPOINT ["/proxy"]

В качестве базового образа мы берём openeuler:22.03-lts-sp3.

Собираем образы и помещаем их в наш registry:

docker build -t 172.17.1.91:5000/os-proxy:v1 -f Dockerfile-proxy .
docker build -t 172.17.1.91:5000/os-operator:v1 -f Dockerfile-operator .
 
docker push 172.17.1.91:5000/os-proxy:v1
docker push 172.17.1.91:5000/os-operator:v1

 

Теперь на станции, откуда мы управляем кластером k8s готовим и применяем следующие yaml файлы:

kubectl apply -f https://gitee.com/openeuler/KubeOS/raw/master/docs/example/config/crd/upgrade.openeuler.org_os.yaml -f https://gitee.com/openeuler/KubeOS/raw/master/docs/example/config/crd/upgrade.openeuler.org_osinstances.yaml

Предоставляем доступы RBAC:

kubectl apply -f https://gitee.com/openeuler/KubeOS/raw/master/docs/example/config/rbac/role.yaml -f https://gitee.com/openeuler/KubeOS/raw/master/docs/example/config/rbac/role_binding.yaml

И собственно запуск приложений os-operator и os-proxy описываем в manager.yaml:

apiVersion: v1
kind: Namespace
metadata:
   name: upgrade-system

apiVersion: apps/v1
kind: DaemonSet
metadata:
   name: upgrade-proxy
   namespace: upgrade-system
   labels:
      control-plane: upgrade-proxy
spec:
   selector:
      matchLabels:
         control-plane: upgrade-proxy
   template:
      metadata:
         labels:
            control-plane: upgrade-proxy
      spec:
         containers:
         – name: proxy
           command:
           – /proxy
           image: 172.17.1.91:5000/os-proxy:v1
           imagePullPolicy: Always
           volumeMounts:
           – name: upgrade-agent
             mountPath: /var/run/os-agent
           env:
           – name: NODE_NAME
              valueFrom:
                 fieldRef:
                    apiVersion: v1
                    fieldPath: spec.nodeName
         volumes:
         – name: upgrade-agent
            hostPath:
            path: /var/run/os-agent

apiVersion: apps/v1
kind: Deployment
metadata:
   name: upgrade-operator
   namespace: upgrade-system
   labels:
      control-plane: upgrade-operator
spec:
   selector:
      matchLabels:
         control-plane: upgrade-operator
   replicas: 1
   template:
      metadata:
         labels:
            control-plane: upgrade-operator
      spec:
         containers:
         – command:
            – /operator
            image: 172.17.1.91:5000/os-operator:v1
            imagePullPolicy: Always
            name: operator
            securityContext:
               allowPrivilegeEscalation: false
               runAsUser: 6552
               runAsGroup: 6552
           resources:
             limits:
               cpu: 100m
               memory: 30Mi
            requests:
               cpu: 100m
               memory: 20Mi
         terminationGracePeriodSeconds: 10
         nodeSelector:
            node-role.kubernetes.io/control-plane: “”
         tolerations:
            – key: “node-role.kubernetes.io/control-plane”
               operator: “Exists”

применяем его:

kubectl apply -f manager.yaml

Убеждаемся, что всё запустилось:

Отлично, теперь у нас всё готово для проверки обновления образа KubeOS.

Давайте подготовим новую версию KubeOS образа для рабочих узлов и применим его.

Выпуск новой версии базового образа KubeOS для рабочих узлов и его установка на рабочие узлы

Для начала мы меняем в нашем test-package версию с 1 на 2 и собираем новую версию нашего тестового пакета, размещаем её в каталоге /opt/kubeOS/scripts/www-repo/Packages/ (старую версию можно удалить, чтобы не замусоривать репозиторий) и обновляем метаданные репозитория.

Теперь переходим в каталог /opt/kubeOS/scripts/ и даём команду (такую же как мы использовали при формировании базового образа KubeOS, но изменив версию на «2»):

./kbimg.sh create vm-image -p /etc/yum.repos.d/openEuler.repo \

-v v2 -b ../bin/os-agent -e '''$1$SALT$Pf6lsQR45JfQZQgPfpkJ91'''

Полученный файл update.img мы помещаем в каталог /opt/kubeOS/scripts/new-version/ под именем update-2.img (это сделано для удобства, чтобы мы потом не путались в версиях, если у нас возникнет надобность откатится на нужную нам версию).

От этого файла update-2.img необходимо рассчитать sha256 контрольную сумму, которую мы впишем в yaml манифест файла обновления:

Подготовим файл обновления upgrade_os.yaml:

apiVersion: upgrade.openeuler.org/v1alpha1

kind: OS

metadata:

   name: os-update

spec:

   imagetype: disk

   opstype: upgrade

   osversion: KubeOS v2

   imageurl: http://172.17.3.80/new-version/update-2.img

   maxunavailable: 1

   flagSafe: true

   checksum: dced65742a70079bd19c039f73edbe1d66033d370602258e019f0870aec4d800

   containerimage: ""

   evictpodforce: true

   mtls: false

И применим его:

kubectl apply -f upgrade_os.yaml

Давайте теперь подключимся к виртуальным консолям наших рабочих узлов k8s, чтобы мы смогли убедиться, что при перезагрузке произойдёт переключение загрузки корневого раздела. Пусть эти окна весят в фоне, применение обновлений обычно занимает 2-3 минуты (в случае нашего почти пустого кластера k8s).

Мы всегда можем проверить текущую версию ОС, на которую в данный момент должен быть обновлёна ОС рабочих узлов кластера k8s. Сделать это мы можем командой:

Давайте посмотрим текущую версию образа KubeOS, который в данный момент работает на рабочих узлах:

Из вывода команды мы видим, что пока узлы работают на первой версии «KubeOS v1».

В консоли нашего рабочего узла (он ещё не перезагрузился) мы тоже видим в приветствии версию:

Теперь наш первый рабочий узел автоматически пошёл на перезагрузку и мы видим в меню GRUB как без всякого нашего вмешательства выбрана позиция «B»:

После загрузки узла мы видим, что версия в приветствии сменилась на v2:

Так же, если опять посмотреть сведения о node используя инструмент kubectl мы увидим, что узел kubeos-oe2203sp3-k8s-w1 уже обновил версию:

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

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

Заключение касательно KubeOS

Собственно таким образом мы продемонстрировали процесс обновления образов рабочих узлов кластера k8s с применением kubeOS. Благодаря этому инструменту, мы можем обновлять пакеты (мы это сделали на примере нашего тестового пакета), добавлять или изменять конфиги и распространять наши изменения. И всё это делать используя целостные образы, которые однообразны и одинаково применяются на всех рабочих узлах кластера. Если вдруг, по какой-то причине нам надо будет откатиться на другой образ — мы просто прописываем необходимую версию образа в upgrade_os.yaml и применяем его.

Здесь необходимо сделать несколько важных замечаний. Объект типа OS с одним и те же именем должен быть в кластере только одной версии. В противном случае рабочие узлы начнут время от времени перезагружаться с одной версии на другую. Так же стоит заметить, что, если мы по какой-то причине перезагрузили рабочий узел и вручную выбрали в GRUB предыдущую версию образа (к примеру мы выбрали «А», при условии, что текущая версия в «B»), то через какое-то непродолжительное время наш узел автоматически будет перезапущен os-proxy, чтобы его версия соответствовала требуемой в upgrade_os.yaml.

Установка и настройка KubeVirt

Теперь давайте добавим нашему кластеру k8s полезной нагрузки, а именно запустим на нём пару виртуальных машин под управлением openScaler 22.03 SP3.

И делать это мы будем при помощи проекта KubeVirt. Если кратко, то данный проект позволяет запускать виртуальную машину внутри контейнера как процесс qemu. Для этого узлы должны поддерживать аппаратную виртуализацию (в случае наших виртуальных машин мы задействуем nested режим).

Установка KubeVirt на k8s подробно расписана https://kubevirt.io/quickstart_cloud/, поэтому просто выполняем:

export VERSION=$(curl -s https://storage.googleapis.com/kubevirt-prow/release/kubevirt/kubevirt/stable.txt)
echo $VERSION
kubectl create -f https://github.com/kubevirt/kubevirt/releases/download/${VERSION}/kubevirt-operator.yaml
kubectl create -f https://github.com/kubevirt/kubevirt/releases/download/${VERSION}/kubevirt-cr.yaml
kubectl get all -n kubevirt

Также, мы сразу установим Containerized Data Importer (https://kubevirt.io/user-guide/operations/containerized_data_importer/). Этот оператор позволяет создавать ресурсы типа DataVolume, которые мы будем использовать для импорта образов виртуальных машин и при создании виртуальных машин, которые будут использовать отдельные PV тома для хранения своих данных.

Произведём установку:

export TAG=$(curl -s -w %{redirect_url} https://github.com/kubevirt/containerized-data-importer/releases/latest)
export VERSION=$(echo ${TAG##*/})
kubectl create -f https://github.com/kubevirt/containerized-data-importer/releases/download/$VERSION/cdi-operator.yaml
kubectl create -f https://github.com/kubevirt/containerized-data-importer/releases/download/$VERSION/cdi-cr.yaml

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

kubectl create namespace vm-images

После установки мы должны наблюдать следующие namespace:

Теперь у нас всё готово к тому, чтобы импортировать базовый образ openScaler 22.03 SP3 и создать на его основе виртуальные машины.

Запуск при помощи KubeVirt виртуальных машин на базе OpenScaler 22.03 SP3 в кластере k8s.

Для того, чтобы мы могли начать импортировать образ openScaler 22.03 SP3 мы должны создать тома (PV) для размещения данных этого образа, а так же данных виртуальных машин.

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

Для начала, мы создадим том, на котором у нас будут хранится образ openscaler 22.03 sp3.

Для этого нам создаём pv-for-image.yaml:

apiVersion: v1
kind: PersistentVolume
metadata:
   name: openscaler-2203sp3-image
   labels:
      system: openscaler-2203sp3
      node: w1
spec:
   capacity:
      storage: 50Gi
   accessModes:
   - ReadWriteOnce
   persistentVolumeReclaimPolicy: Retain
   local:
      path: /persist/pv/vm-images/os2203sp3
   nodeAffinity:
      required:
         nodeSelectorTerms:
         - matchExpressions:
            - key: kubernetes.io/hostname
              operator: In
              values:
              - kubeos-oe2203sp3-k8s-w1

---
apiVersion: v1
kind: PersistentVolume
metadata:
   name: openscaler-2203sp3-image-scratch
   labels:
      system: openscaler-2203sp3-scratch
      node: w1
spec:
   capacity:
      storage: 50Gi
   accessModes:
   - ReadWriteOnce
   persistentVolumeReclaimPolicy: Retain
   local:
      path: /persist/pv/vm-images/os2203sp3-scratch
   nodeAffinity:
      required:
         nodeSelectorTerms:
         - matchExpressions:
           - key: kubernetes.io/hostname
             operator: In
             values:
             - kubeos-oe2203sp3-k8s-w1

и применяем его:

kubectl apply -f pv-for-image.yaml

Образ openScaler 22.03 sp3 (с поддержкой настройки при помощи cloud-init) мы разместим на сервере kubeos-oe2203sp3-prepare по пути /opt/kubeOS/scripts/openscaler-2203sp3.qcow2, при этом сразу заметим, что этот файл доступен через веб.

Создаём описание datavolume dv-os.yaml:

apiVersion: cdi.kubevirt.io/v1beta1
kind: DataVolume
metadata:
   name: openscaler2203sp3-cloud-base
   namespace: vm-images
spec:
   source:
      http:
         url: http://172.17.3.80/openscaler-2203sp3.qcow2
   pvc:
      accessModes:
      - ReadWriteOnce
      resources:
         requests:
            storage: 50Gi

И применяем его:

kubectl apply -f dv-os.yaml

После этого мы можем следить за процессом импорта образа.

В начале мы видим, что импорт только запланирован:

Теперь импорт начался и уже выполнен на 37.98%:

Импорт образа завершён на 100%:

Мы видим, что у нас создался PVC, который захватил созданный нами том PV на рабочем узле:

При этом второй том (openscaler-2203sp3-image-scratch) уже выполнил свою работу, был освобождён и по идее необходимости в нём уже нет.

Мы будем размещать наши виртуальные машины в namespace default, а для этого нам придётся создать правила RBAC, чтобы обеспечить доступ к образам в namespace vm-images.

Создаём файл rbac-kv.yaml:

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
   name: cdi-cloner
rules:
- apiGroups: ["cdi.kubevirt.io"]
  resources: ["datavolumes/source"]
  verbs: ["create"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
   name: default-cdi-cloner
   namespace: vm-images
subjects:
- kind: ServiceAccount
  name: default
  namespace: default
roleRef:
  kind: ClusterRole
  name: cdi-cloner
  apiGroup: rbac.authorization.k8s.io

И применим его:

kubectl apply -f rbac-kv.yaml

Так как наша виртуальная машина настраивается при помощи cloud-init, то мы можем просто сформировать скрипт startup.sh для создания пользователя и скормить его виртуалке на её старте:

#!/bin/bash
export NEW_USER="test1"
export SSH_PUB_KEY="ssh-rsa XXXXXXXXX laba@ubuntu-test"

adduser -U -m $NEW_USER
echo "$NEW_USER:12345" | chpasswd
usermod -aG wheel $NEW_USER
mkdir /home/$NEW_USER/.ssh
echo "$SSH_PUB_KEY" > /home/$NEW_USER/.ssh/authorized_keys
chown -R ${NEW_USER}: /home/$NEW_USER/.ssh

В этот скрипт мы помещаем публичный ssh-rsa ключ с машины управления администратора, по которому будем предоставлен доступ пользователю test1 через протокол SSH. Так же пользователь test1 будет иметь возможность использовать sudo (мы сразу помещаем его в группу wheel). Пароль для пользователя test1 мы выбираем простенький 12345.

Этот скрипт мы теперь должны преобразовать в base64 формат:

cat startup.sh | base64 -w0

Полученную строку в формате BASE64 мы помещаем в манифест создания виртуальной машины opescaler-2203sp3-disk-1.yaml в строку userDataBase64:

apiVersion: kubevirt.io/v1
kind: VirtualMachine
metadata:
  labels:
    kubevirt.io/vm: vm-openscaler2203sp3-1
  name: vm-openscaler2203sp3-1
spec:
  running: true
  template:
    metadata:
      labels:
        kubevirt.io/vm: vm-openscaler2203sp3-1
    spec:
      domain:
        devices:
          disks:
          - disk:
              bus: virtio
            name: datavolumedisk1
            bootOrder: 1
          - disk:
              bus: virtio
            name: cloudinitdisk
        machine:
          type: "q35"
        resources:
          requests:
            memory: 2Gi
      terminationGracePeriodSeconds: 60
      volumes:
      - dataVolume:
          name: openscaler-2203sp3-1
        name: datavolumedisk1
      - name: cloudinitdisk
        cloudInitNoCloud:
          userDataBase64: XXXXXXXXXXXXXXXXXXXXXX
  dataVolumeTemplates:
  - metadata:
      name: openscaler-2203sp3-1
  spec:
    pvc:
      accessModes:
      - ReadWriteOnce
      resources:
        requests:
          storage: 50Gi
    source:
      pvc:
        namespace: vm-images
        name: openscaler2203sp3-cloud-base
---
apiVersion: v1
kind: PersistentVolume
metadata:
  name: openscaler-2203sp3-1
  labels:
    system: openscaler-2203sp3-1
    node: w1
spec:
  capacity:
    storage: 50Gi
  accessModes:
  - ReadWriteOnce
  persistentVolumeReclaimPolicy: Retain
  local:
    path: /persist/pv/vm-images/os-vm-1
  nodeAffinity:
    required:
      nodeSelectorTerms:
      - matchExpressions:
        - key: kubernetes.io/hostname
          operator: In
          values:
          - kubeos-oe2203sp3-k8s-w1
---
apiVersion: v1
kind: Service
metadata:
  name: openscaler-2203sp3-1
spec:
  ports:
  - port: 22
    name: ssh
    protocol: TCP
    targetPort: 22
    nodePort: 31229
  selector:
    kubevirt.io/vm: vm-openscaler2203sp3-1
  type: NodePort

И применяем этот манифест:

kubectl apply -f opescaler-2203sp3-disk-1.yaml

Наблюдательный читатель могу заметить, что помимо самой виртуальной машины и диска для неё, мы создаём ещё и service (типа NodePort), чтобы мы смогли с использованием ssh получить на неё доступ, просто обратившись по протоколу SSH на рабочий узел и используя порт подключения 31229.

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

Процесс создания тома с данными виртуальной машины мы можем наблюдать используя команду kubectl get dv.

Здесь мы видим, что клонирование запланировано:

Чуть погодя клонирование уже в процессе:

Когда клонирование будет закончено, мы увидим, что наша виртуалка перешла в состояние Running.

После старта виртуальной машины у нас появился объект типа VirtualMachineInstance:

К созданной виртуальной машине мы можем подключиться несколькими способами.

К примеру используя консоль:

Или открыв консоль VNC:

Давайте залогинемся внутрь нашей виртуальной машины и создадим файл test-file

И перезагрузим виртуальную машину (просто сказав systemctl reboot)

Убедившись, что виртуальная машина перезагрузилась (а мы можем наблюдать весь процесс перезагрузки подключившись к консоли), пробуем теперь зайти на неё с использованием протокола ssh:

Да, мы подключились, и увидели, что все данные сохраняются при перезагрузках и наш тестовый файл на месте.

Заключение

Сегодня мы рассмотрели довольно интересный подход к организации обновлений ОС на рабочих узлах кластера k8s, позволяющий единовременно и автоматически обновлять эти узлы просто создав новую версию образа ОС и опубликовав его с использованием yaml манифест файла. Продемонстрированный подход идеологически верен принципам работы k8s, когда у нас всё есть объекты k8s (CRD) и управление обновлениями происходит путём создания и модификации этих объектов (типа OS) с применением привычных инструментов типа kubectl.