Метод оптимизации задач создания и поддержки однотипных Xen VM

в 7:57, , рубрики: linux, virtualization, xen, виртуализация, метки: , ,

Цель

У меня, как и у многих жителей хабра есть домашний «балконный» сервер, на котором крутится множество сервисов, как личных, так и публичных — начиная от архива фотографий и Gitolite, и заканчивая несколькими веб-сайтами. Однажды я озадачился вопросом отделения личного от публичного с целью наведения порядка и усиления безопасности системы. Было решено публичные сервисы вынести в отдельные виртуальные машины, которые если даже подвергнутся взлому, то остальные данные не пострадают, а VM может быть легко восстановлена из резервной копии.
При этом я не люблю избыточность в каких бы то ни было проявлениях, и завести по виртуальной машине на каждый сервис хотелось с минимальными затратами таких ресурсов, как мое время и дисковое пространство. В качестве дополнительной «хотелки» выступала возможность обновлять однотипное ПО на всех виртуальных машинах одновременно, а не по отдельности.

Исследование возможных вариантов

Сначала я исследовал возможность применения OpenVZ, однако с ним у меня отношения не сложились по нескольким причинам. Во-первых, необходимость использовать специальное ядро, а моя система — это Gentoo с hardened-профилем и системой Grsecurity, от которых не хотелось отказываться. Во-вторых, OpenVZ у меня вызвал антипатию после того, как внутри гостевой системы (контейнера), ограниченного по количеству максимально доступной ему памяти, я попытался занять всю эту память, что привело в итоге к «убийству» по OOM процесса, работающего в основной (!) системе, а не в контейнере. Я был весьма удивлен таким поведением, так как свободной памяти в основной системе было навалом, и снес OpenVZ от греха подальше.

К счастью, все необходимое для работы Xen уже включено в стандартное ядро Linux, и, забегая вперед, скажу, что как Dom0, так и DomU успешно работают с моим hardened-ядром (с небольшим изменением).

Реализация

Делаем образ эталонной виртуальной машины (AMI)

Как я уже упомянул выше, моя основная система — это Gentoo Linux amd64 3.8.6-hardened SMP, поэтому для виртуальных машин я буду использовать точно такую же систему и точно такое же ядро. Ядро я не пересобирал специально для domU, и оно у меня содержит как backend, так и frontend-драйверы гипервизора Xen.

Для «шаблона» виртуальной машины, на базе которой затем будут создаваться «инстансы» (можно провести аналогию с AMI и инстансами в Amazon EC2) я создал отдельный логический раздел LVM и назвал его vmtemplate. На этот раздел установил Gentoo (шаги по установке аналогичны шагам в стандартном руководстве Gentoo Handbook), а также дополнительный софт, который я выбирал исходя из того, какие сервисы будут в итоге работать внутри виртуальных машин (чтобы «разница» между ними была минимальной). В итоге у меня получился такой список дополнительного софта:

  • dev-lang/php
  • dev-lang/v8
  • dev-lang/erlang
  • dev-python/django
  • dev-ruby/rails
  • www-apache/passenger
  • www-apps/wordpress
  • www-servers/nginx

Для экономии места, при установке пакетов в виртуальных машинах удаляется вся документация (флаги noman nodoc noinfo в make.conf). Чтобы удобно было обновлять софт, я написал простой скрипт, позволяющий быстро входить в окружение нашей эталонной виртуальной машины и выходить из него:

#!/bin/sh

ROOT=/mnt/gentoo
DEV=/dev/xenguests/vm-template-gentoo

echo "Mounting filesystems"
mount $DEV $ROOT
mount -t proc none $ROOT/proc
mount --bind /dev $ROOT/dev
mount --bind /usr/portage $ROOT/usr/portage
cp /etc/resolv.conf $ROOT/etc/resolv.conf
chroot $ROOT /bin/bash

echo "Unmounting filesystems"
umount $ROOT/dev $ROOT/proc $ROOT/usr/portage
umount $ROOT

Процесс установки самого гипервизора Xen не отличается оригинальностью, и для этого можно воспользоваться любой инструкцией, которые в изобилии присутствуют на просторах Интернета. Однако, если вы, как и я, хотите использовать hardened-ядро, то обращаю ваше внимание на то, что у меня ядро под Xen категорически отказывалось загружаться на самых ранних стадиях (когда еще даже ничего не выводится на экран), так что я даже сначала думал, что до загрузки ядра дело не доходит.

Путем сопоставления адреса исключения, которое выдавал гипервизор перед смертью, с содержимым файла System.map, удалось выяснить, что причина кроется в функции, оперирующей со стеком ядра, и заработало все тогда, когда я отключил в ядре функцию CONFIG_PAX_RANDKSTACK (Randomize kernel stack base), относящуюся к PAX.

Создаем виртуальную машину на базе эталонной

Сначала небольшое отступление: для решения этой задачи я попробовал использовать LVM-слепки, однако это оказалось плохим решением, так как свободное место, выделенное в слепке для изменений (copy on write), заполняется довольно быстро, и, что логично, если внутри виртуальной машины дважды записать один и тот же файл, то и объем занятого места в слепке вырастет на два размера записанного файла. Плохая оптимизация. Для решения задачи оптимизации использования дискового пространства я решил использовать aufs, и теперь мы, наконец, займемся тем, что, собственно, было заявлено в заголовке статьи.

  • Для виртуальной машины создаем еще один логический раздел LVM — назовем его vm-site1. Размер этого раздела в моем случае составляет 1 Гб (размер раздела с эталонной VM — 8 Гб). ФС на обоих разделах — ext4.
  • Собираем модуль sys-fs/aufs3 на основной системе — он нам вскоре понадобится.

Для того, чтобы виртуальная машина загрузилась с этой экзотической конфигурацией, потребуется специальный ramdisk, который смонтирует правильно разделы в aufs. Для начала, в файле /etc/genkernel.conf раскомментируем строчку: ALLRAMDISKMODULES="1" — это нужно для того, чтобы недавно установленный модуль aufs скопировался в ramdisk, который мы будем создавать с помощью genkernel. Более элегантного способа сделать это я не нашел, а править системные файлы в /usr/share/genkernel не хотелось.

В рабочей директории создаем папку overlay, внутри нее файл с именем init, который делаем исполняемым:

#!/bin/busybox sh

mount -t proc -o noexec,nosuid,nodev proc /proc >/dev/null 2>&1
mount -o remount,rw / >/dev/null 2>&1

/bin/busybox --install -s

if [ "$0" = '/init' ]
then
        [ -e /linuxrc ] && rm /linuxrc
fi

modprobe xen-blkfront

RO=/dev/xvda1
RW=/dev/xvda2

mknod /dev/xvda1 b 202 1
mknod /dev/xvda2 b 202 2

modprobe aufs

mkdir /aufs
mkdir /rw
mkdir /ro

mount $RO /ro
mount $RW /rw
mount -t aufs -o dirs=/rw:/ro=ro aufs /aufs

[ -d /aufs/ro ] || mkdir /aufs/ro
[ -d /aufs/rw ] || mkdir /aufs/rw

mount --move /ro /aufs/ro
mount --move /rw /aufs/rw

cat /aufs/ro/etc/fstab | grep -v ' / ' | grep -v swap >> /aufs/etc/fstab

ROTYPE=$(cat /proc/mounts | grep $RO | cut -d' ' -f3)
ROOPTIONS=$(cat /proc/mounts | grep $RO | cut -d' ' -f4)

RWTYPE=$(cat /proc/mounts | grep $RW | cut -d' ' -f3)
RWOPTIONS=$(cat /proc/mounts | grep $RW | cut -d' ' -f4)

echo $RO /ro $ROTYPE $ROOPTIONS 0 0 >  /aufs/etc/fstab
echo $RW /rw $RWTYPE $RWOPTIONS 0 0 >> /aufs/etc/fstab

echo "cp /proc/mounts /etc/mtab" > /aufs/etc/local.d/mtab.start
chmod a+x /aufs/etc/local.d/mtab.start

echo "sysctl -w kernel.grsecurity.grsec_lock=1" > /aufs/etc/local.d/grsec.start
chmod a+x /aufs/etc/local.d/grsec.start

exec /sbin/switch_root -c "/dev/console" /aufs /sbin/init

После этого создаем модифицированный ramdisk с помощью скрипта наподобие представленного ниже:

#!/bin/sh

VERSION=`uname -r`
MODULE=`modprobe -nv aufs | cut -d' ' -f2`

if [ ! -f $MODULE ]; then
        echo "aufs module not found on your system"
fi

genkernel initramfs --no-install --no-postclear --initramfs-overlay=/home/xen/overlay
cp -v /var/tmp/genkernel/initramfs-${VERSION} /boot/initramfs-domU

Теперь виртуальная машина сможет загрузиться с корневой ФС на aufs, при этом все изменения будут записываться на раздел /dev/xvda2, а /dev/xvda1 — это наш эталонный образ, файлы в котором мы тоже при желании можем обновлять, и обновления «подцепятся» всеми виртуальными машинами (на время обновления эталонного образа машины следует остановить). Раздел с данными конкретной виртуальной машины (в нашем случае — LVM-раздел vm-site1, содержит только отличия от эталонной ФС, его также можно свободно примонтировать на хостовой системе, вносить изменения в файлы, делать резервные копии и т.д.

Осталось создать виртуальную машину. Для этого я использую такой конфиг:

kernel = "/boot/vmlinuz"
ramdisk = "/boot/initramfs-domU"
memory = 128
name = "site1"
vcpus = 1
disk = [
        "phy:/dev/xenguests/vm-template,xvda1,r",
        "phy:/dev/xenguests/vm-site1,xvda2,w"
        ]
root = "/dev/xvda1 ro"
extra = "xencons=tty"
on_poweroff = "destroy"
on_reboot = "restart"
on_crash = "destroy"
vif = [ "mac=0a:11:10:24:14:20,bridge=br1" ]
dhcp = "dhcp"

Разным виртуальным машинам на базе их уникальных MAC-адресов выдаются статические IP-адреса через DHCP-сервер на host-системе, а также происходит обновление записей в DNS, так чтобы на веб-сервер nginx внутри VM можно было легко пробрасывать соединения с nginx-а на host-системе, а также фильтровать трафик. Кроме того, host-nginx используется для терминирования SSL-трафика.

Итог

Достигнутые цели:

  • Данные индивидуальных виртуальных машин теперь занимают ровно столько места, сколько занимает разница между эталонной системой того, что есть внутри виртуалки. Для сайта с WordPress эта разница составляет у меня 45 Мб.
  • Используя скрипт входа в «эталонное» окружение, можно обновлять однотипный софт сразу на всех VM одновременно.

Автор: madprogrammer

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js