Без так называемой живой системы Linux невозможно решать рутинные технические задачи в хостинговой компании: LiveCD нужны для сокращения нагрузки на инженеров, повышения стабильности предоставления услуг и упрощения внесения изменений в процесс деплоя. К сожалению, доступные в Сети универсальные образы плохо подходят для наших потребностей, поэтому пришлось создать собственный, получивший название SpeedTest. Поначалу мы использовали его для измерения производительности машин при расформировании, но потом функциональность системы была расширена для решения других проблем с разнообразным оборудованием.
Рост потребностей выявил недостатки системы с интегрированными статическими скриптами. Главный из них — отсутствие простоты и удобства развития продукта. У нас не было своей системы сборки, возможности добавлять поддержку нового (или старого) оборудования, не было возможности менять поведение одного и того же образа в разных условиях запуска.
Проблемы состава ПО в образе
Поскольку в нашей инфраструктуре в основном использовался CentOS (на тот момент седьмой версии), регулярное создание образов через Jenkins мы организовали на основе этого дистрибутива. Кухня сборки образов на RHEL/CentOS прекрасно автоматизируется с помощью Anaconda Kickstart. Структура kickstart подробно описана в документации RHEL — не стоит подробно о ней рассказывать, хотя на некоторых моментах мы заострим внимание.
Заголовочная часть файла KS стандартна, за исключением описания репозиториев для загрузки ПО, из которого будет составлен образ. В этом блоке присутствуют директивы следующего вида:
repo --name=elrepo --baseurl=http://elrepo.reloumirrors.net/elrepo/el8/x86_64/
В блок packages мы включаем директиву excludedocs, а чтобы уменьшить размер образа, обязательно основываем его на @core и указываем пакеты-исключения:
%packages --excludedocs
@core
vim
-audit
-man-db
-plymouth
-selinux-policy-targeted
-tuned
-alsa-firmware
-iwl1000-firmware
-iwl105-firmware
-iwl100-firmware
-iwl135-firmware
-iwl2000-firmware
-iwl2030-firmware
-iwl5000-firmware
-iwl3160-firmware
-iwl5150-firmware
-iwl6000-firmware
-iwl6050-firmware
-iwl6000g2a-firmware
-iwl7260-firmware
В состав образа из приведенного выше примера войдет группа @core + пакет vim с зависимостями, но будет исключен ряд ненужных пакетов. Далее на этапах post и post(nochroot) выполняется доводка конфигурации скриптами. Рядом с kickstart в репозитории располагаются файлы, которые должны попасть в образ.
Сборка осуществляется с помощью входящей в состав стандартного репозитория CentOS утилиты livecd-creator. В результате мы получаем образ squashfs (приведем часть исполняемого в Jenkins сценария):
echo -e "\\nSpeedtest release ver ${BUILD_NUMBER}\\n" >> motd
sudo livecd-creator --verbose -f speedtest-${BUILD_NUMBER} speedtest.ks
7z x speedtest-${BUILD_NUMBER}.iso -oisoroot
mv isoroot/LiveOS/squashfs.img ./speedtest-${BUILD_NUMBER}.squashfs
На этом отрывке стоит заострить внимание: обязательно нумеруйте образы и вставляйте номер билда в файл motd (его добавление в образ должно быть прописано в kickstart). Это позволит вам четко понимать, на каком именно билде вы работаете, и отслеживать изменения в нем во время отладки. Вопрос поддержки оборудования и дополнительного ПО мы решаем с помощью собственного репозитория RPM с пакетами, отсутствующими в штатных репозиториях или измененными нашими специалистами.
Неявные проблемы запуска системы
- Ядро и его зависимости приходят в систему через группу @core, поэтому при каждой новой сборке в образ попадают последние доступные версии ПО. Соответственно, нам необходимо это ядро и initramfs для него.
- Сборка initramfs требует привилегий root, а в системе, на которой она происходит, нужен тот же самый билд ядра, который будет в squashfs.
Наш совет: чтобы избежать проблем с безопасностью и ошибками в скриптах, проводить сборку стоит в изолированном окружении. Крайне нежелательно делать это на мастер-сервере Jenkins.
Сборку initramfs мы приводим из задачи в формате Jenkins DSL:
shell (
'''
set -x
echo -e '\\n'ENVIRONMENT INJECTION'\\n'
if [ $KERNELVERSION ];then
echo "KERNEL=$KERNELVERSION" >> $WORKSPACE/env
else
echo "KERNEL=$(uname -r)" >> $WORKSPACE/env
fi
short_branch=$(echo $long_branch | awk -F/ '{print $3}')
cat <<EOF>> $WORKSPACE/env
WEBPATH=live-${short_branch}
BUILDSPATH=live-${short_branch}/builds/${JOB_BASE_NAME}
FTPSERVER=repo-app01a.infra.hostkey.ru
EOF
'''.stripIndent().trim()
)
environmentVariables { propertiesFile('env') }
shell (
'''
echo -e '\\n'STARTING INITRAMFS GENERATION'\\n'
yum install kernel-${KERNEL} -y
dracut --xz --add "livenet dmsquash-live bash network rescue kernel-modules ssh-client base" --omit plymouth --add-drivers "e1000 e1000e" --no-hostonly --verbose --install "lspci lsmod" --include /usr/share/hwdata/pci.ids /usr/share/hwdata/pci.ids -f initrd-${KERNEL}-${BUILD_NUMBER}.img $KERNEL
'''.stripIndent().trim()
)
Итак, у нас сгенерированы образ squashfs, initramfs и есть последнее ядро. Этих компонентов достаточно для запуска системы через PXE.
Доставка и ротация образов
Для доставки образов мы применили интересную систему, на которой стоит остановиться подробнее. Есть центральный репозиторий — это наш внутренний сервер из приватной сети, который отвечает по нескольким протоколам (FTP, RSYNC и т. д.) и отдает информацию по HTTPS через nginx.
На сервере была создана структура каталогов следующего вида:
├── builds
│ ├── build_dracut_speedtest_el8.dsl
│ │ ├── initrd-${VERSION}.img
│ │ └── vmlinuz-${VERSION}
│ ├── build_iso_speedtest_el8.dsl
│ │ ├── speedtest-${BUILDNUMBER}.iso
│ │ └── speedtest-${BUILDNUMBER}.squashfs
├── initrd -> builds/build_dracut_speedtest_el8.dsl/initrd-${VERSION}.img
├── speedtest.iso -> builds/build_iso_speedtest_el8.dsl/speedtest-${BUILDNUMBER}.iso
├── speedtest.squashfs -> builds/build_iso_speedtest_el8.dsl/speedtest-${BUILDNUMBER}.squashfs
├── vmlinuz -> builds/build_dracut_speedtest_el8.dsl/vmlinuz-${VERSION}
В каталог builds с соответствующими именам задач по сборке подкаталогами мы складываем три последних удачных билда, а в корневом каталоге находятся символические ссылки на последний билд без указания версии (именно с ними работают клиенты). Если нам необходимо откатить версию, быстро поменять ссылки можно вручную.
Доставка на сервер и работа со ссылками входят в задачу Jenkins по сборке: в качестве клиента используется ncftp, а в качестве сервера — proftpd (данные передаются по FTP). Последнее важно, поскольку здесь требуется связка сервера и клиента, поддерживающая работу с симлинками. Клиенты не взаимодействуют с центральным репозиторием напрямую: они подключаются к зеркалам, которые привязаны к географическим локациям. Такой подход нужен для снижения объема трафика и ускорения деплоя.
Раздача на зеркала также организована достаточно интересно: используется конфигурация с проксированием и директивной proxy-store:
location ^~ /livecd {
try_files $uri @central-repo;
}
location @central-repo {
proxy_pass https://centralrepourl.infra.hostkey.ru;
proxy_store /mnt/storage$uri;
}
Благодаря этой директиве копии образов сохраняются на зеркалах после первой загрузки клиентом. Наша система не содержит лишней скриптовой обвязки, а последняя сборка образа при обновлении доступна во всех локациях, к тому же ее нетрудно мгновенно откатить.
Модификация поведения образа через Foreman
Развертывание систем проводится через Foreman, то есть у нас есть API и возможность прокидывать переменные в конфигурационные файлы загрузчиков PXE. С таким подходом нетрудно сделать один образ для решения целого спектра задач:
- для загрузки на «железе» и исследования аппаратных проблем;
- для установки ОС (см. нашу предыдущую статью);
- для автоматического тестирования «железа»;
- для расформирования оборудования и полной очистки жесткого диска после отказа клиента от сервера.
Понятно, что все задачи нельзя зашить в образ и заставить исполниться одновременно. Мы поступили иначе: в кухню сборки добавили сервисы systemd и запускающие выполнение нужных задач сценарии. Скрипт и сервис носят одно название (для примера покажем старт инсталляции Linux):
Lininstall.service
[Unit]
Description=Linux installation script
After[email protected]
Requires=sshd.service
[Service]
Type=forking
RemainAfterExit=yes
ExecStartPre=/usr/bin/bash -c "if [[ $SPEEDISO == lininstall ]];then exit 0;else exit 1;fi"
ExecStart=/usr/bin/bash -c "/usr/local/bin/lininstall.sh | tee /dev/console 2>&1"
TimeoutSec=900
[Install]
WantedBy=multi-user.target
Сервис запускает задачу, только если существует переменная окружения SPEEDISO со значением linintsall.
Теперь нам необходимо передать эту переменную в образ, что нетрудно сделать через командную строку ядра в загрузчике. Пример приводится для PXE Linux, но решение не привязано к загрузчику, поскольку нам нужна только командная строка ядра:
LABEL <%= @host.operatingsystem.name %>
KERNEL <%= @kernel %>
MENU LABEL Default install Hostkey BV image <%= @host.operatingsystem.name %>
APPEND initrd=<%= @initrd %> <%= mainparams %> root=live:<%= host_param('livesystem_url') %>/<%= host_param('live_squash') %> systemd.setenv=SPEEDISO=<%= host_param('hk_speedtest_autotask') %> <%= modprobe %> noeject
IPAPPEND 2
Переменная hk_speedtest_autotask должна содержать lininstall. В этом случае при старте системы запускается одноименный сервис. Если же переменная не существует или имеет случайное значение, из образа запустится система, к которой можно будет подключиться по ssh (если старт сервиса был настроен через kickstart при сборке образа).
Итоги
Потратив некоторое время на разработку, мы получили управляемую систему сборки/доставки LiveCD, которая позволяет изящно решать проблемы поддержки оборудования и обновления ПО в образе. С ее помощью можно быстро откатывать назад изменения, менять поведение образа через API Foreman, а также экономить трафик и иметь высокую автономность сервиса для разных площадок. Географически разнесенные зеркала содержат последние удачные билды всех используемых образов и репозиториев, а система удобна, надежна и не раз выручала нас на протяжении трех лет эксплуатации.