Обновление ОС рабочих узлов кластера 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
Несколько важных замечаний:
- Так как на момент написания статьи самая новая версия k8s для openEuler была v1.28.2, то мы будем использовать именно её. В качестве runtime для запуска контейнеров мы будем использовать containerd. Здесь следует заметить, что в репозитории openEuler находится версия containerd 1.2.0, тогда как актуальная версия 1.7.16. Поэтому мы в нашем кластере будет использовать версию 1.7.16, упаковав её в rpm пакет и добавив его в образ KubeOS рабочего узла.
- Так же, сразу стоит отметить, что в статье мы не будем приводить (чтобы излишне не перегружать её листингами файлов) конфигурацию для containerd для его работы с insecure registry 172.17.1.91:5000. Информацию по настройке containerd для подобных случаев можно подсмотреть в официальной документации по самой containerd.
- Для демонстрации смены версии KubeOS мы подготовим собственный rpm пакет, в котором будет повышать версию в строчке содержащейся в текстовом файле /usr/share/test-package
- В нашем кластере 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.