В этой статье, я хотел бы показать вам одну крутую технологию, я успешно использую ее для Kubernetes. Она может быть реально полезна для построения больших кластеров.
С этого момента вам больше не придется думать об установке ОС и отдельных пакетов на каждую ноду. Зачем? Вы можете сделать все это автоматически через Dockerfile!
Тот факт что вы можете купить сотню новых серверов, добавить их в рабочее окружение и почти моментально получить их готовыми к использованию — это действительно потрясающе!
Заинтриговал? Теперь давайте обо всем по порядку.
Для начала, нам нужно понимать, как именно эта схема работает.
Если кратко, то для всех нод мы подготавливаем единый образ с ОС, Docker, Kubelet и всем остальным.
Этот образ системы вместе с ядром собирается автоматически CI, используя Dockerfile.
Конечные ноды загружают ОС и ядро из этого образа прямо через сеть.
Ноды используют overlayfs в качестве корневой файловой системы, так что в случае перезагрузки любые изменения будут потеряны (так же как и в случае с docker-контейнерами).
Есть основной конфиг, в нем можно описать точки монтирования и некоторые команды, которые должны выполняться во время загрузки ноды (например, команда для добавления ssh-ключа и kubeadm join
)
Мы будем использовать проект LTSP, потому что он дает нам все что нужно для организации сетевой загрузки.
В целом LTSP это пачка shell-скриптов, который делает нашу жизнь намного проще.
Он предоставляет модуль initramfs, несколько вспомогательных скриптов и некую систему настройки, которая подготавливает систему на ранней стадии загрузки, еще перед вызовом init.
Так выглядит процедура подготовки образа:
ltsp-build-image
Сразу после этого вы получите сжатый образ из этого chroot со всем установленным софтом внутри.
Каждая нода будет скачивать этот образ во время загрузки и использовать его в качестве rootfs.
Для обновления, достаточно просто перезагрузить ноду, новый образ будет загружен и использован для rootfs.
Серверная часть LTSP в нашем случае включает всего два компонента:
Вам также необходимо иметь:
Описание процесса загрузки ноды
next-server
,filename
.Как я уже говорил, я подготавливаю LTSP-сервер со squashed образом автоматически, используя Dockerfile. Этот метод неплох, потому что все шаги для сборки могут быть описаны в вашем git репозитории. Вы можете управлять версиями, использовать тэги, применять CI и все тоже самое что бы вы использовали для подготовки обычных Docker-проектов.
С другой стороны вы можете развернуть LTSP-сервер вручную, выполнив все шаги вручную, это может быть хорошо в целях обучения и для понимания основных принципов.
Выполните перечисленные в статье команды вручную, если вы хотите просто попробовать LTSP без Dockerfile.
На данный момент у LTSP есть некоторые недоработки, а авторы проекта не очень охотно принимают исправления. К счастью LTSP легко кастомизируется, поэтому я подготовил несколько патчей для себя, я приведу их здесь.
Возможно когда-нибудь я созрею на форк, если сообщество тепло примет мое решение.
Мы будем использовать stage building в нашем Dockerfile для сохранения только необходимых частей нашего docker-образа, остальные неиспользованные части будут исключены из конечного образа.
ltsp-base
(установка основного софта для ltsp сервера)
|
|---basesystem
| (подготовка chroot-окружения с основным софтом и ядром)
| |
| |---builder
| | (сборка дополнительного софта из исходников, при необходимости)
| |
| '---ltsp-image
| (установка дополнительного софта, docker, kubelet и сборка squashed образа)
|
'---final-stage
(копирование squashed образа, ядра и initramfs в первый stage)
ОК, давайте начнем, это первая часть нашего 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-образ уже имеет установленные:
На этом этапе мы подготовим 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-скрипты пытаются вызвать привилегированные команды, которые могут завершаться с ошибками и блокировать установку всего пакета.
Решение:
На этом этапе мы можем собрать весь необходимый софт и модули ядра из исходников, очень классно, что есть возможность сделать это на этом этапе, в полностью автоматическом режиме.
Пропустите этот этап если вам не нужно ничего собирать из исхолников.
Приведу пример установки последней версии 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'
На этом этапе мы установим то, что мы собрали в предыдущем шаге:
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
На финальной стадии мы сохраним только наш 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-образ который включает:
ОК, теперь когда наш 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"
/etc/fstab
файл.nofail
опцию, она дает следующее поведение, что если раздел не существует загрузка продолжается без ошибок.rc.local
файл, который будет вызван systemd во время загрузки.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.
К сожалению, не доступен сервер mySQL