В этой статье, я хотел бы показать вам одну крутую технологию, я успешно использую ее для Kubernetes. Она может быть реально полезна для построения больших кластеров.
С этого момента вам больше не придется думать об установке ОС и отдельных пакетов на каждую ноду. Зачем? Вы можете сделать все это автоматически через Dockerfile!
Тот факт что вы можете купить сотню новых серверов, добавить их в рабочее окружение и почти моментально получить их готовыми к использованию — это действительно потрясающе!
Заинтриговал? Теперь давайте обо всем по порядку.
Резюме
Для начала, нам нужно понимать, как именно эта схема работает.
Если кратко, то для всех нод мы подготавливаем единый образ с ОС, Docker, Kubelet и всем остальным.
Этот образ системы вместе с ядром собирается автоматически CI, используя Dockerfile.
Конечные ноды загружают ОС и ядро из этого образа прямо через сеть.
Ноды используют overlayfs в качестве корневой файловой системы, так что в случае перезагрузки любые изменения будут потеряны (так же как и в случае с docker-контейнерами).
Есть основной конфиг, в нем можно описать точки монтирования и некоторые команды, которые должны выполняться во время загрузки ноды (например, команда для добавления ssh-ключа и kubeadm join
)
Процесс подготовки образа
Мы будем использовать проект LTSP, потому что он дает нам все что нужно для организации сетевой загрузки.
В целом LTSP это пачка shell-скриптов, который делает нашу жизнь намного проще.
Он предоставляет модуль initramfs, несколько вспомогательных скриптов и некую систему настройки, которая подготавливает систему на ранней стадии загрузки, еще перед вызовом init.
Так выглядит процедура подготовки образа:
- Разворачиваем базовую систему в chroot-окружении.
- Вносим необходимые изменения, устанавливаем софт.
- Запускаем команду
ltsp-build-image
Сразу после этого вы получите сжатый образ из этого chroot со всем установленным софтом внутри.
Каждая нода будет скачивать этот образ во время загрузки и использовать его в качестве rootfs.
Для обновления, достаточно просто перезагрузить ноду, новый образ будет загружен и использован для rootfs.
Серверные компоненты
Серверная часть LTSP в нашем случае включает всего два компонента:
- TFTP-сервер — TFTP является протоколом инициализации, он используется для загрузки ядра, initramfs и основного конфига — lts.conf.
- NBD-сервер — протокол NBD, используется для доставки сжатого образа rootfs клиентам. Это самый быстрый метод, но при желании, его можно заменить на NFS или AoE.
Вам также необходимо иметь:
- DHCP-сервер — он будет раздавать IP-конфигурацию и несколько дополнительных опций, которые понадобятся нашим клиентам, чтобы те смогли загружаться с нашего LTSP-сервера.
Процесс загрузки ноды
Описание процесса загрузки ноды
- Первым делом нода запросит с DHCP IP-адрес и опции
next-server
,filename
. - Затем нода применит настройки и скачает загрузчик (pxelinux или grub)
- Загрузчик скачает и загрузит конфиг с ядром и initramfs.
- Затем он загрузит ядро и initramfs с определенными опциями указанными для ядра.
- Во время загрузки модули initramfs обработают параметры из cmdline и выполнят некоторые действия, такие как подключение nbd-устройсва, подготовка overlay rootfs и т. д.
- После этого, вместо обычного init, будет вызван специальный ltsp-init.
- Скрипты ltsp-init подготовят систему на ранней стадии, прежде чем будет вызван основной init. В основном здесь применяются опции из lts.conf (основной файл конфигурации): это обновление записей в fstab и rc.local и т.п.
- Дальше будет вызов основного init (systemd), который загрузит уже настроенную систему как обычно, смонтирует общие ресурсы из fstab, запустит таргеты и сервисы, выполнит команды из rc.local.
- В итоге мы получим полностью сконфигурированную и загруженную систему, готовую к дальнейшим действиям.
Подготовка сервера
Как я уже говорил, я подготавливаю LTSP-сервер со squashed образом автоматически, используя Dockerfile. Этот метод неплох, потому что все шаги для сборки могут быть описаны в вашем git репозитории. Вы можете управлять версиями, использовать тэги, применять CI и все тоже самое что бы вы использовали для подготовки обычных Docker-проектов.
С другой стороны вы можете развернуть LTSP-сервер вручную, выполнив все шаги вручную, это может быть хорошо в целях обучения и для понимания основных принципов.
Выполните перечисленные в статье команды вручную, если вы хотите просто попробовать LTSP без Dockerfile.
Список использованных патчей
На данный момент у LTSP есть некоторые недоработки, а авторы проекта не очень охотно принимают исправления. К счастью LTSP легко кастомизируется, поэтому я подготовил несколько патчей для себя, я приведу их здесь.
Возможно когда-нибудь я созрею на форк, если сообщество тепло примет мое решение.
- feature-grub.diff
По умолчанию LTSP не поддерживает EFI, поэтому я подготовил патч, который добавляет GRUB2 с поддержкой EFI. - feature_preinit.diff
Этот патч добавляет опцию PREINIT в lts.conf, которая позволяет запускать произвольные команды перед вызовом основного init. Это может быть полезно для модификации systemd юнитов и настройки сети. Примечательно, что все переменные из загрузочной среды сохраняются, и вы можете использовать их в своих скриптах вызванных через эту опцию. - feature_initramfs_params_from_lts_conf.diff
Решает вопрос с неработающей опцией NBD_TO_RAM, после этого патча вы можете указать ее в lts.conf внутри chroot. (не тот который в директории tftp) - nbd-server-wrapper.sh
Это не патч, а просто shell-скрипт, он позволяет запускать nbd-server в foregroud, он вам понадобится если вы захотите запускать nbd-сервер внутри Docker-контейнера.
Dockerfile stages
Мы будем использовать stage building в нашем Dockerfile для сохранения только необходимых частей нашего docker-образа, остальные неиспользованные части будут исключены из конечного образа.
ltsp-base
(установка основного софта для ltsp сервера)
|
|---basesystem
| (подготовка chroot-окружения с основным софтом и ядром)
| |
| |---builder
| | (сборка дополнительного софта из исходников, при необходимости)
| |
| '---ltsp-image
| (установка дополнительного софта, docker, kubelet и сборка squashed образа)
|
'---final-stage
(копирование squashed образа, ядра и initramfs в первый stage)
Stage 1: ltsp-base
ОК, давайте начнем, это первая часть нашего Dockerfile:
FROM ubuntu:16.04 as ltsp-base
ADD nbd-server-wrapper.sh /bin/
ADD /patches/feature-grub.diff /patches/feature-grub.diff
RUN apt-get -y update && apt-get -y install ltsp-server tftpd-hpa nbd-server grub-common grub-pc-bin grub-efi-amd64-bin curl patch && sed -i 's|in_target mount|in_target_nofail mount|' /usr/share/debootstrap/functions # Добавим поддержку EFI и загрузчик Grub (#1745251)
&& patch -p2 -d /usr/sbin < /patches/feature-grub.diff && rm -rf /var/lib/apt/lists && apt-get clean
На данный момент наш docker-образ уже имеет установленные:
- NBD-сервер
- TFTP-сервер
- LTSP-скрипты с поддержкой загрузчика grub (для EFI)
Stage 2: basesystem
На этом этапе мы подготовим chroot окружение с базовой системой и установим основной софт с ядром.
Мы будем использовать обычный debootstrap вместо ltsp-build-client для подготовки образа, потому что ltsp-build-client установит GUI и некоторые другие ненужные вещи, которые нам явна не пригодятся для развертывания серверов.
FROM ltsp-base as basesystem
ARG DEBIAN_FRONTEND=noninteractive
# Подготовим основную систему
RUN mkdir -p /opt/ltsp/amd64/proc/self/fd && touch /opt/ltsp/amd64/proc/self/fd/3 && debootstrap --arch amd64 xenial /opt/ltsp/amd64 && rm -rf /opt/ltsp/amd64/proc/*
# Установим обновления
RUN echo " deb http://archive.ubuntu.com/ubuntu xenial main restricted universe multiverse\n deb http://archive.ubuntu.com/ubuntu xenial-updates main restricted universe multiverse\n deb http://archive.ubuntu.com/ubuntu xenial-security main restricted universe multiverse" > /opt/ltsp/amd64/etc/apt/sources.list && ltsp-chroot apt-get -y update && ltsp-chroot apt-get -y upgrade
# Установим пакеты LTSP
RUN ltsp-chroot apt-get -y install ltsp-client-core
# Применим патчи initramfs
# 1: Чтение параметров из /etc/lts.conf во время загрузки (#1680490)
# 2: Добавим поддержку PREINIT опций в lts.conf
ADD /patches /patches
RUN patch -p4 -d /opt/ltsp/amd64/usr/share < /patches/feature_initramfs_params_from_lts_conf.diff && patch -p3 -d /opt/ltsp/amd64/usr/share < /patches/feature_preinit.diff
# Запишем LTSP_NBD_TO_RAM опцию в локальный конфиг, для загрузки образа в ram:
RUN echo "[Default]\nLTSP_NBD_TO_RAM = true" > /opt/ltsp/amd64/etc/lts.conf
# Установим пакеты
RUN echo 'APT::Install-Recommends "0";\nAPT::Install-Suggests "0";' >> /opt/ltsp/amd64/etc/apt/apt.conf.d/01norecommend && ltsp-chroot apt-get -y install software-properties-common apt-transport-https ca-certificates ssh bridge-utils pv jq vlan bash-completion screen vim mc lm-sensors htop jnettop rsync curl wget tcpdump arping apparmor-utils nfs-common telnet sysstat ipvsadm ipset make
# Установим ядро
RUN ltsp-chroot apt-get -y install linux-generic-hwe-16.04
Обратите внимание, что с некоторыми пакетами, например lvm2, могут возникнуть проблемы. Они не полностью оптимизированы для установки в непривилегированном chroot. Их postinstall-скрипты пытаются вызвать привилегированные команды, которые могут завершаться с ошибками и блокировать установку всего пакета.
Решение:
- Некоторые могут установиться без проблем если устанавливать их до установки ядра (например, lvm2)
- Но для некоторых из них вам потребуется использовать такой обходной путь для установки без postinstall-скрипта.
Stage 3: builder
На этом этапе мы можем собрать весь необходимый софт и модули ядра из исходников, очень классно, что есть возможность сделать это на этом этапе, в полностью автоматическом режиме.
Пропустите этот этап если вам не нужно ничего собирать из исхолников.
Приведу пример установки последней версии MLNX_EN драйвера:
FROM basesystem as builder
# Скопируем cpuinfo (для сборки из исходников)
RUN cp /proc/cpuinfo /opt/ltsp/amd64/proc/cpuinfo
# Скачаем и скомпилируем Mellanox driver
RUN ltsp-chroot sh -cx ' VERSION=4.3-1.0.1.0-ubuntu16.04-x86_64 && curl -L http://www.mellanox.com/downloads/ofed/MLNX_EN-${VERSION%%-ubuntu*}/mlnx-en-${VERSION}.tgz | tar xzf - && export DRIVER_DIR="$(ls -1 | grep "MLNX_OFED_LINUX-\|mlnx-en-")" KERNEL="$(ls -1t /lib/modules/ | head -n1)" && cd "$DRIVER_DIR" && ./*install --kernel "$KERNEL" --without-dkms --add-kernel-support && cd - && rm -rf "$DRIVER_DIR" /tmp/mlnx-en* /tmp/ofed*'
# Сохраним модули ядра
RUN ltsp-chroot sh -c ' export KERNEL="$(ls -1t /usr/src/ | grep -m1 "^linux-headers" | sed "s/^linux-headers-//g")" && tar cpzf /modules.tar.gz /lib/modules/${KERNEL}/updates'
Stage 4: ltsp-image
На этом этапе мы установим то, что мы собрали в предыдущем шаге:
FROM basesystem as ltsp-image
# Получим модули ядра
COPY --from=builder /opt/ltsp/amd64/modules.tar.gz /opt/ltsp/amd64/modules.tar.gz
# Установим модули ядра
RUN ltsp-chroot sh -c ' export KERNEL="$(ls -1t /usr/src/ | grep -m1 "^linux-headers" | sed "s/^linux-headers-//g")" && tar xpzf /modules.tar.gz && depmod -a "${KERNEL}" && rm -f /modules.tar.gz'
Теперь внесем дополнительные изменения чтобы завершить наш LTSP-образ:
# Установим docker
RUN ltsp-chroot sh -c ' curl -fsSL https://download.docker.com/linux/ubuntu/gpg | apt-key add - && echo "deb https://download.docker.com/linux/ubuntu xenial stable" > /etc/apt/sources.list.d/docker.list && apt-get -y update && apt-get -y install docker-ce=$(apt-cache madison docker-ce | grep 18.06 | head -1 | awk "{print $ 3}")'
# Настроим опции для docker
RUN DOCKER_OPTS="$(echo --storage-driver=overlay2 --iptables=false --ip-masq=false --log-driver=json-file --log-opt=max-size=10m --log-opt=max-file=5 )" && sed "/^ExecStart=/ s|$| $DOCKER_OPTS|g" /opt/ltsp/amd64/lib/systemd/system/docker.service > /opt/ltsp/amd64/etc/systemd/system/docker.service
# Установим kubeadm, kubelet и kubectl
RUN ltsp-chroot sh -c ' curl -s https://packages.cloud.google.com/apt/doc/apt-key.gpg | apt-key add - && echo "deb http://apt.kubernetes.io/ kubernetes-xenial main" > /etc/apt/sources.list.d/kubernetes.list && apt-get -y update && apt-get -y install kubelet kubeadm kubectl cri-tools'
# Отключим автоматические обновления
RUN rm -f /opt/ltsp/amd64/etc/apt/apt.conf.d/20auto-upgrades
# Отключим профили apparmor
RUN ltsp-chroot find /etc/apparmor.d -maxdepth 1 -type f -name "sbin.*" -o -name "usr.*" -exec ln -sf "{}" /etc/apparmor.d/disable/ \;
# Опишем опции ядра (cmdline)
RUN KERNEL_OPTIONS="$(echo init=/sbin/init-ltsp forcepae console=tty1 console=ttyS0,9600n8 nvme_core.default_ps_max_latency_us=0 )" && sed -i "/^CMDLINE_LINUX_DEFAULT=/ s|=.*|=\"${KERNEL_OPTIONS}\"|" "/opt/ltsp/amd64/etc/ltsp/update-kernels.conf"
Теперь сделаем squased-образ из нашего chroot:
# Очистим кэши
RUN rm -rf /opt/ltsp/amd64/var/lib/apt/lists && ltsp-chroot apt-get clean
# Соберем squashed образ
RUN ltsp-update-image
Stage 5: Final stage
На финальной стадии мы сохраним только наш squashed-образ и ядро с initramfs
FROM ltsp-base
COPY --from=ltsp-image /opt/ltsp/images /opt/ltsp/images
COPY --from=ltsp-image /etc/nbd-server/conf.d /etc/nbd-server/conf.d
COPY --from=ltsp-image /var/lib/tftpboot /var/lib/tftpboot
Отлично, теперь у нас есть docker-образ который включает:
- TFTP-сервер
- NBD-сервер
- настроенный загрузчик
- ядро с initramfs
- squashed образ rootfs
Использование
ОК, теперь когда наш Docker-образ с LTSP-сервером, ядром, initramfs и squashed rootfs полностью готов, мы можем запустить deployment с ним.
Мы можем сделать это как обычно, но есть еще один вопрос который нам предстоит решить.
К сожалению мы не можем использовать обычный Kubernetes service для нашего deployment, потому что во время загрузки ноды не являются частью Kubernetes кластера и им необходимо использовать externalIP, но Kubernetes всегда применяет NAT для externalIP и на данный момент нет возможности изменить это поведение.
Я знаю два способа что бы избежать этого: использовать hostNetwork: true
или использовать pipework, второй вариант предоставит нам также отказоустойчивость, т.к. в случае отказа IP-адрес переедет на другую ноду вместе контейнером. К сожалению pipework — это не нативный и менее безопасный метод.
Если вы знаете о каком-нибудь более подходящем решении, пожалуйста расскажите о нем.
Приведу пример deployment с hostNetwork:
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: ltsp-server
labels:
app: ltsp-server
spec:
selector:
matchLabels:
name: ltsp-server
replicas: 1
template:
metadata:
labels:
name: ltsp-server
spec:
hostNetwork: true
containers:
- name: tftpd
image: registry.example.org/example/ltsp:latest
command: [ "/usr/sbin/in.tftpd", "-L", "-u", "tftp", "-a", ":69", "-s", "/var/lib/tftpboot" ]
lifecycle:
postStart:
exec:
command: ["/bin/sh", "-c", "cd /var/lib/tftpboot/ltsp/amd64; ln -sf config/lts.conf ." ]
volumeMounts:
- name: config
mountPath: "/var/lib/tftpboot/ltsp/amd64/config"
- name: nbd-server
image: registry.example.org/example/ltsp:latest
command: [ "/bin/nbd-server-wrapper.sh" ]
volumes:
- name: config
configMap:
name: ltsp-config
Как вы могли бы заметить, здесь также используется configmap с lts.conf файлом.
В качестве примера, приведу часть моего конфига:
apiVersion: v1
kind: ConfigMap
metadata:
name: ltsp-config
data:
lts.conf: |
[default]
KEEP_SYSTEM_SERVICES = "ssh ureadahead dbus-org.freedesktop.login1 systemd-logind polkitd cgmanager ufw rpcbind nfs-kernel-server"
PREINIT_00_TIME = "ln -sf /usr/share/zoneinfo/Europe/Prague /etc/localtime"
PREINIT_01_FIX_HOSTNAME = "sed -i '/^127.0.0.2/d' /etc/hosts"
PREINIT_02_DOCKER_OPTIONS = "sed -i 's|^ExecStart=.*|ExecStart=/usr/bin/dockerd -H fd:// --storage-driver overlay2 --iptables=false --ip-masq=false --log-driver=json-file --log-opt=max-size=10m --log-opt=max-file=5|' /etc/systemd/system/docker.service"
FSTAB_01_SSH = "/dev/data/ssh /etc/ssh ext4 nofail,noatime,nodiratime 0 0"
FSTAB_02_JOURNALD = "/dev/data/journal /var/log/journal ext4 nofail,noatime,nodiratime 0 0"
FSTAB_03_DOCKER = "/dev/data/docker /var/lib/docker ext4 nofail,noatime,nodiratime 0 0"
# Each command will stop script execution when fail
RCFILE_01_SSH_SERVER = "cp /rofs/etc/ssh/*_config /etc/ssh; ssh-keygen -A"
RCFILE_02_SSH_CLIENT = "mkdir -p /root/.ssh/; echo 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDBSLYRaORL2znr1V4a3rjDn3HDHn2CsvUNK1nv8+CctoICtJOPXl6zQycI9KXNhANfJpc6iQG1ZPZUR74IiNhNIKvOpnNRPyLZ5opm01MVIDIZgi9g0DUks1g5gLV5LKzED8xYKMBmAfXMxh/nsP9KEvxGvTJB3OD+/bBxpliTl5xY3Eu41+VmZqVOz3Yl98+X8cZTgqx2dmsHUk7VKN9OZuCjIZL9MtJCZyOSRbjuo4HFEssotR1mvANyz+BUXkjqv2pEa0I2vGQPk1VDul5TpzGaN3nOfu83URZLJgCrX+8whS1fzMepUYrbEuIWq95esjn0gR6G4J7qlxyguAb9 admin@kubernetes' >> /root/.ssh/authorized_keys"
RCFILE_03_KERNEL_DEBUG = "sysctl -w kernel.unknown_nmi_panic=1 kernel.softlockup_panic=1; modprobe netconsole netconsole=@/vmbr0,@10.9.0.15/"
RCFILE_04_SYSCTL = "sysctl -w fs.file-max=20000000 fs.nr_open=20000000 net.ipv4.neigh.default.gc_thresh1=80000 net.ipv4.neigh.default.gc_thresh2=90000 net.ipv4.neigh.default.gc_thresh3=100000"
RCFILE_05_FORWARD = "echo 1 > /proc/sys/net/ipv4/ip_forward"
RCFILE_06_MODULES = "modprobe br_netfilter"
RCFILE_07_JOIN_K8S = "kubeadm join --token 2a4576.504356e45fa3d365 10.9.0.20:6443 --discovery-token-ca-cert-hash sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
- KEEP_SYSTEM_SERVICES — во время загрузки LTSP автоматически удаляет некоторые сервисы, эта переменная нужна что бы не допустить этого поведения, для перечисленных здесь сервисов.
- PREINIT_* — команды перечисленные здесь будут выполнены перед запуском systemd (эта возможность была добавлена feature_preinit.diff патчем)
- FSTAB_* — строки перечисленные здесь будут добавлены в
/etc/fstab
файл.
Вы можете заметить что я используюnofail
опцию, она дает следующее поведение, что если раздел не существует загрузка продолжается без ошибок. - RCFILE_* — эти команды будут добавлены в
rc.local
файл, который будет вызван systemd во время загрузки.
Тут я загружаю необходимые модули ядра, запускаю некоторые sysctl-настройки и затем выполняюkubeadm join
команду, которая добавляет ноду в kubernetes кластер.
Более детальную информацию обо всех переменных вы можете получить из lts.conf страницы руководства.
Теперь вы можете настроить ваш DHCP. По сути вче что там нужно — это указать next-server
и filename
опции.
Я использую ISC-DHCP сервер, приведу пример dhcpd.conf
:
shared-network ltsp-netowrk {
subnet 10.9.0.0 netmask 255.255.0.0 {
authoritative;
default-lease-time -1;
max-lease-time -1;
option domain-name "example.org";
option domain-name-servers 10.9.0.1;
option routers 10.9.0.1;
next-server ltsp-1; # write ltsp-server hostname here
if option architecture = 00:07 {
filename "/ltsp/amd64/grub/x86_64-efi/core.efi";
} else {
filename "/ltsp/amd64/grub/i386-pc/core.0";
}
range 10.9.200.0 10.9.250.254;
}
Можно начинать с этого, но что касается меня у меня есть несколько LTSP-серверов и для каждой ноды я настраиваю отдельный статический IP-адрес и нужные опции с помощью Ansible-плейбука.
Попробуйте запустить вашу первую ноду и если все было сделано правильно, вы получите загруженную систему на ней. Нода также будет добавлена Kubernetes кластер.
Теперь вы можете попробовать внести свои собственные изменения.
Если вам нужно что-то большее, обратите внимание что LTSP может быть очень легко изменен под ваши нужды. Не стесняйтесь заглядывать в исходники, там вы сможете найти довольно много ответов.
Присоединяйтесь к нашему Telegram-каналу: @ltsp_ru.
aol-nnov
Если честно, немного непонятно, чем не угодил matchbox, ignition и coreos. Тоже самое, только в профиль и уже готовое :)
kvaps Автор
Про CoreOS и ignition отвечу. Здесь основная фишка в подготовке образа а не в конфигурации.
Под капотом у LTSP обычная убунта, а следовательно все что можно установить на обычной убунте, можно так же прописать и здесь в Dockerfile.
Когда как CoreOS не позволяет с той же лёгкостью добавлять кастомные модули ядра и пакеты на этапе сборки загрузочного образа.
aol-nnov
Мне известно, что такое LTSP, даже пользовал её по назначению. Потому и вопрос — зачем он для нодов кластера k8s, когда для этого есть специализированные готовые решения.
То есть — в чем профит от LTSP против CoreOs? Просто я придерживаюсь мнения, что хостовая система на ноде должна быть минимальной.
kvaps Автор
Ну, как я уже писал раньше — LTSP это просто пачка готовых скриптов для организации сетевой загрузки. Плюс в том, что тут мы получаем обычную Ubuntu в минимальной версии и к тому же сохраняем возможность установить любой пакет, собрать любой модуль ядра.
Специализированные готовые решения не предоставляют такой возможности установить любой модуль, любое ядро с той же легкостью как здесь.
Здесь я с вами согласен, и если вы подготавливаете систему с debootstrap у вас есть полный контроль над происходящим. Внутрь образа попадут только действительно нужные пакеты.