Издательский дом ООО "Гейм Лэнд"ЖУРНАЛ ХАКЕР 125, МАЙ 2009 г.

Аренда от собственника. Поднимаем сервис по сдаче в аренду виртуальных FreeBSD-серверов

Евгений «j1m» Зобнин (zobnin@gmail.com)

Сегодня мы увидим возможности FreeBSD jail в несколько необычном свете – создадим сервис по сдаче в аренду виртуальных FreeBSD-машин. Он будет полностью автоматизированным, позволит использовать разные версии виртуальных окружений на одной физической машине и обособленные настройки для каждого сервера. И все это - с помощью нескольких простых шелл-скриптов.

Один из способов поднять $$$ в кризисное время

Виртуальная FreeBSD-машина – это не что иное, как полнофункциональное jail-окружение, которому выделен глобально маршрутизируемый IP-адрес. Задумка сервиса в том, чтобы раздавать такие окружения на манер номеров в отеле. Клиент заходит на сайт, заполняет необходимые поля формы, переводит чуток WMZ на наш кошелек и получает в ответ IP-адрес своего сервера, домен N-ого уровня и пароль/ключ к ssh-сервису. После чего он волен делать со своим окружением все, что душе заблагорассудится – ровно до того момента, пока не наступит время съезжать, то есть истечет срок аренды, указанный клиентом в одном из полей регистрационной формы. По истечению этого срока сервер останавливается и удаляется.

Идея виртуальных отелей далеко не нова и, по сути, представляет собой пример «Облачной обработки данных» (en.wikipedia.org/wiki/Cloud_computing), когда необходимое клиенту программное обеспечение предоставляется как сервис. Чтобы организовать свой бизнес по сдаче в аренду виртуальных FreeBSD-серверов от тебя потребуется наличие N-го количества белых IP-адресов (по одному на каждый виртуальный сервер), домен, закрепленный за одним из них, а также базовое понимание принципов работы FreeBSD.

Немного теории, или Сформулируем требования

Пользуясь знаниями, почерпнутыми из прошлой моей статьи, организовать сервис не так уж и сложно. Окружения jail подкупают своей простотой и легкостью развертывания: несколько скриптов-обвязок – и дело сделано! Вот только долго такой сервис не протянет. Администраторы начнут плевать в монитор на второй день работы, клиенты завалят жалобами о низкой производительности, а сервер просто загнется, когда количество виртуальных серверов перевалит за первый десяток. Поэтому перед развертыванием и наймом SEO-шников необходимо тщательно продумать будущую инфраструктуру.

Для начала сформулируем требования к нашему сервису:

  1. Выделенный IP-адрес и доменное имя для каждого окружения. Имя выбирается клиентом во время заполнения формы на нашем сайте, а IP-адрес извлекается из специального файла.
  2. Полная свобода клиента в отношении jail-окружения. Это значит – никаких unionfs и nullfs для всего, кроме архива портов. Во время создания нового сервера окружение полностью копируется из специального каталога.
  3. Общие каталоги /usr/ports/distfiles и /usr/ports/packages для всех окружений, чтобы порт, загруженный одним из клиентов, автоматически был доступен другим.
  4. Автоматизированное создание, удаление, запуск и мониторинг серверов.
  5. Поддержка разных версий виртуальных FreeBSD-серверов на одной физической машине.
  6. Возможность быстрого переноса виртуального сервера на другой сетевой интерфейс или диск.
  7. Ежедневный бэкап виртуальных серверов.
  8. Доступ по ssh с аутентификацией на основе ключей. Ключ генерируется во время создания виртуального сервера и отдается клиенту по https.
  9. Разные типы аккаунтов: trial - для двухдневного испытания, base - обычный, extra, vip и т.д.

Теперь подумаем об инфраструктуре сервиса и о том, как реализовать все перечисленные требования и оставить задел на будущее, то есть обеспечить возможность добавления новых функций в случае необходимости. После долгих размышлений я пришел к варианту, который не только нагляден и прост, но и достаточно легок в реализации:

  1. Собираем FreeBSD-окружение в каталог /usr/jailbase/FreeBSD-версия.
  2. Подготавливаем набор скриптов, которые будут управлять виртуальными серверами (создание/удаление, запуск/остановка и т.д.)
  3. Пишем инициализационный скрипт, который будет запускать все существующие окружения.

Кроме самого FreeBSD-окружения, я поместил в каталог /usr/jailbase еще несколько каталогов и конфигурационных файлов, необходимых для управления виртуальными серверами:

  • /usr/jailbase/FreeBSD-версия - чистая сборка FreeBSD, копируется в каждое окружение при его создании.
  • /usr/jailbase/distfiles-версия - монтируется к /usr/ports/distfiles каждого окружения при помощи nullfs.
  • /usr/jailbase/packages-версия - монтируется к /usr/ports/packages каждого окружения при помощи nullfs.
  • /usr/jailbase/db - база данных jail-окружений. В первой колонке перечислены IP-адреса всех окружений, во второй - базовый каталог (например, /usr/jail), в третьей - сетевой интерфейс, в четвертой - доменное имя, в пятой - версия, в шестой – e-mail владельца, в седьмой - тип аккаунта, в восьмой - время истечения строка аренды в формате ГГММДДЧЧММ, в девятой - состояние.
  • /usr/jailbase/defaults - дефолтовые значения для различных полей конфигурационного файла.
  • /usr/jailbase/conf/ - каталог с настройками и ограничениями для разных типов аккаунтов: файлы trial, base, extra, vip, которые будут прочитаны скриптом запуска виртуального сервера.

Файл db - база данных для всех виртуальных серверов в формате passwd (поля, разделенные двоеточием). Девятое поле «состояние» предназначено для упрощения администрирования серверов и может принимать следующие значения: none - не существует, disabled - временно отключен, ok - окружение создано и готово к запуску (или уже работает). В свежеустановленной системе, пока еще не существует виртуальных серверов, этот файл исполняет роль базы доступных IP-адресов, когда каждая строка не содержит других полей, кроме первого и девятого (пример: 192.168.0.1::::::::none). Скрипт, создающий новый сервер, просто находит первую строку с состоянием none и заполняет ее (тогда строка принимает примерно такой вид: 192.168.0.1:/usr/jail:ed0:jail.host.com:7.1-RELEASE:vasya@mail.ru:trial:0903031700:ok). В дальнейшем скрипт запуска виртуальных серверов найдет запись с состоянием ok, перейдет в каталог, прописанный во втором поле, найдет каталог нужного окружения по IP-адресу и запустит в нем виртуальный сервер.

Собираем все вместе

Перво-наперво мы должны создать базовое окружение в каталоге /usr/jailbase/FreeBSD-версия. Для этого переходим в /usr/src и вводим следующую последовательность команд:

# JAIL=/usr/jailbase/FreeBSD-`uname -r`
# mkdir -p $JAIL
# make world DESTDIR=$JAIL
# make distribution DESTDIR=$JAIL

В моем распоряжении находится машина с FreeBSD 7.1, поэтому каталог базового окружения получил имя «/usr/jailbase/FreeBSD-7.1-RELEASE». Войдем в окружение и проведем базовую конфигурацию (доменное имя и IP-адрес на данном этапе не имеют значения):

# jail $JAIL base.jail 192.168.0.1 /bin/sh
# touch /etc/fstab
# newaliases
# tzsetup
# echo nameserver 127.0.0.1 > /etc/resolv.conf

Файл rc.conf не трогаем, он будет генерироваться скриптом addvserver автоматически для каждого виртуального сервера. Пароль суперпользователя не устанавливаем: для окружения будет создаваться пара ключей, и доступ по паролю по умолчанию закрыт. Отредактируем /etc/motd и включим туда всю необходимую информацию о пользовании сервисом:

# vi /etc/motd

Выйдем из окружения, набрав exit. Скопируем дерево портов из базовой системы в $JAIL/usr:

# cp -a /usr/ports $JAIL/usr

Теперь освободим точки монтирования дистфайлов и пакетов от лишнего мусора:

# rm -Rf $JAIL/usr/ports/distfiles $JAIL/usr/ports/packages
# mkdir $JAIL/usr/ports/distfiles $JAIL/usr/ports/packages

Базовое окружение готово! Конфигурационный файл будет отличаться для каждого окружения, поэтому заботу о его создании мы возложим на плечи скрипта addvserver.

Пойдем дальше и добавим каталоги /usr/jailbase/distfiles-версия и /usr/jailbase/packages-версия, которые будут подключаться к окружениям с помощью nullfs:

# mkdir /usr/jailbase/{distfiles,packages}-`uname -r`

Затем создадим файл дефолтовых значений для скрипта создания виртуального сервера (addvserver):

# vim /usr/jailbase/defaults

# Дефолтовый сетевой интерфейс. На него будут вешаться вновь созданные окружения.
IF=nfe0
# Дефолтовый каталог для виртуальных серверов.
JAILDIR=/usr/jail

Формат базы данных виртуальных серверов я описал выше. Отмечу лишь, что чистая база должна выглядеть как набор записей вида IP::::::::none, по одной записи на каждый доступный внешний IP-адрес. Всю работу по ее заполнению возьмут на себя соответствующие скрипты.

Теперь у нас есть все необходимое для создания и манипулирования виртуальными серверами, – осталось только установить и настроить DNS-сервер на физической машине. Он нужен для привязки доменного имени к каждому виртуальному серверу.

Мы будем использовать BIND9, поскольку это единственный вменяемый DNS-сервер, позволяющий обновлять зоны, не останавливая работу демона. Пройдем через простые этапы его настройки:

1. Сгенерируем пару ключей, которые необходимы для аутентификации клиента, пожелавшего обновить зоны:

$ dnssec-keygen -a HMAC-MD5 -b 128 -r /dev/urandom -n USER NSUPDATE

Команда запишет в текущий каталог два файла с именами примерно такого вида – Knsupdate.+157+36521.key и Knsupdate.+157+36521.private. Они понадобятся скриптам addvserver и delvserver для обновления зон, поэтому скопируем их в каталог /usr/jailbase:

# cp Knsupdate.* /usr/jailbase/

Откроем конфиг /etc/namedb/named.conf и запишем в него:

# vim /etc/namedb/named.conf

// Позволим управлять сервером только с локальной машины
controls {
inet 127.0.0.1 allow { localhost; } keys { "NSUPDATE"; };
};
// Объявим ключ, значение опции secret можно взять из строки 'Key:' файла Knsupdate.+157+36521.private
key NSUPDATE {
algorithm HMAC-MD5.SIG-ALG.REG.INT;
secret VF5l+ZIdDcPjhDz1+eG3Jw==;
};

// Объявим зону host.com, которую ты должен заменить на свой домен
zone "host.com" {
type master;
file "host.com.db";
allow-update { key "NSUPDATE"; };
notify yes;
};
// Объявим обратную зону, в которой должен быть прописан сегмент, выделенный для твоих внешних IP-адресов
zone "67.16.172.in-addr.arpa" {
type master;
file "host.com-reverse.db";
allow-update { key "NSUPDATE"; };
notify yes;
};

Настроим зоны host.com (локальные уже существуют):

# vim /etc/namedb/host.com.db

$TTL 86400
@ IN SOA @ root (
1 28800 7200 604800 86400 )
IN NS dns
dns IN A 172.16.67.1

# vim /etc/namedb/host.com-reverse.db

$TTL 86400
@ IN SOA @ root (
1 28800 7200 604800 86400 )
IN NS dns
1 IN PTR dns.host.com.

Укажи в них все хосты, имеющие статические имена, и больше эти файлы мы трогать не будем, за нас все сделают скрипты. Запустим bind:

# /etc/rc.d/named start

Скрипты

Чтобы наш сервис соответствовал всем предъявленным требованиям и функционировал правильно, необходимо написать, как минимум, восемь скриптов:

  1. addvserver - создает виртуальный сервер путем добавления нового окружения, обновляет /usr/jailbase/db, добавляет нужную запись в DNS-таблицы.
  2. delvserver - удаляет виртуальный сервер, принцип действия обратный.
  3. disablevserver - отключает виртуальный сервер, не удаляя его (флаг 'disabled').
  4. enablevserver - включает отключенный виртуальный сервер (ставит флаг 'ok').
  5. startvserver - запускает существующий виртуальный сервер, сверяясь с /usr/jailbase/db.
  6. stopvserver - останавливает существующий виртуальный сервер.
  7. vservers - скрипт для каталога /usr/local/etc/rc.d, который запускает виртуальные сервера во время загрузки системы и останавливает во время шатдауна.
  8. watchvservers - скрипт, исполняемый cron, следит за состоянием виртуальных серверов и останавливает их в случае необходимости (например, при истечении срока аренды).

По-хорошему, первые шесть скриптов лучше объединить в один, принимающий аргументы add, del, start и stop, но для сохранения простоты изложения оставим их в отдельных файлах.

Итак, скрипт номер один, addvserver (здесь и далее - только ключевые элементы скриптов; полные версии ты найдешь на прилагаемом к журналу диске):

# vim /usr/local/bin/addvserver

# Настраиваем переменные
HOSTNAME=$1; OSVER=$2; EMAIL=$3; ACTYPE=$4; TIME=$5; KEY=$6
JAILBASE=/usr/jailbase
DB=$JAILBASE/db

# Читаем дефолтовые настройки
. $JAILBASE/defaults

# Находим свободный IP-адрес
STRING=`grep ':none$' $DB`
IP=`echo $STRING | cut -d ':' -f 1`

# Копируем окружение…
cp -a $JAILBASE/FreeBSD-$OSVER $JAILDIR/$IP
# …и создаем для него файл инициализации
RCCONF=$JAILDIR/$IP/etc/rc.conf
echo "hostname="$HOSTNAME"" > $RCCONF
echo "network_interfaces=""" >> $RCCONF
echo "rpcbind_enable="NO"" >> $RCCONF
echo "sshd_enable="YES"" >> $RCCONF

# Копируем публичный ключ в каталог /root/.ssh
mkdir -p $JAILDIR/$IP/root/.ssh
cat $KEY >> /root/.ssh/authorized_keys2

# Добавляем новый сервер в базу DNS
UPREQ=$JAILBASE/upreq
echo "update add $HOSTNAME 86400 A $IP" > $UPREQ
echo "send" >> $UPREQ
nsupdate -k $JAILBASE/Knsupdate.*.private $UPREQ
rm -f $UPREQ

# Записываем информацию об окружении в базу данных
sed "/^$IP.*/s##$IP:$JAILDIR:$IF:$HOSTNAME:$OSVER:$EMAIL:$ACTYPE:$TIME:ok#" $DB > ${DB}.new
mv ${DB}.new $DB

# Возвращаем IP сервера
echo $IP

Скрипт принимает шесть аргументов (доменное имя виртуального сервера, версия ОС, e-mail владельца, тип аккаунта, время истечения срока аренды, файл с публичным ключом клиента) и возвращает IP нового сервера.

Скрипт delvserver выполняет обратную процедуру. Сначала он находит сервер в базе db, проверяет, существует ли сервер вообще (статус не должен быть none), а затем выполняет следующую последовательность действий (переменная STRING - это строка сервера из db):

# vim /usr/local/bin/delvserver

# Удаляем сервер
JAILDIR=`echo $STRING | cut -d ':' -f 2`
rm -Rf $JAILDIR/$IP $JAILDIR/${IP}.ipfw

# Удаляем сервер из базы DNS
HOSTNAME=`echo $STRING | cut -d ':' -f 4`
UPREQ=$JAILBASE/upreq
echo "update delete $HOSTNAME A" > $UPREQ
echo "send" >> $UPREQ
nsupdate -k $JAILBASE/Knsupdate.*.private $UPREQ
rm -f $UPREQ

# Устанавливаем для сервера статус none
NEWSTRING=`echo $STRING | sed "/$STATUS/s##none#"`
sed "/^$IP.*/s##$NEWSTRING#" $DB > ${DB}.new
mv ${DB}.new $DB

Скрипт disablevserver, предназначенный для временного отключения виртуального сервера, очень похож на delvserver, с тем исключением, что он не удаляет сервер, а просто ставит на него флаг «disabled». То есть, переменная NEWSTRING примет вид: «NEWSTRING=`echo $STRING | sed "/$STATUS/s##disabled#"`».

Скрипт enablevserver - брат-близнец disablevserver, единственное отличие которого - установка флага 'ok' вместо 'disabled'.

Скрипт startvserver – довольно примитивен. Он ищет переданный ему IP-адрес в базе, проверяет сервер на готовность (статус 'ok') и запускает его. Набор правил номер 4 (ruleset 4) для команды devfs создан специально для jail-окружений и содержится в файле /etc/defaults/devfs.rules. Чтобы скрипт работал корректно, этот файл необходимо положить в /etc.

# vim /usr/local/bin/startvserver

# Извлекаем данные, необходимые для запуска
JAILDIR=`echo $STRING | cut -d ':' -f 2`
IF=`echo $STRING | cut -d ':' -f 3`
HOSTNAME=`echo $STRING | cut -d ':' -f 4`
OSVER=`echo $STRING | cut -d ':' -f 5`

# Запускаем jail-сервер
ifconfig $IF inet alias $IP
mount -t devfs none $JAILDIR/$IP/dev
devfs -m $JAILDIR/$IP/dev ruleset 4
mount -t procfs none $JAILDIR/$IP/proc
mount_nullfs $JAILBASE/distfiles-$OSVER $JAILDIR/$IP/usr/ports/distfiles
mount_nullfs $JAILBASE/packages-$OSVER $JAILDIR/$IP/usr/ports/packages
jail $JAILDIR/$IP $HOSTNAME $IP /bin/sh /etc/rc

Скрипт stopvserver перед остановкой проделывает те же шаги и, плюс к этому, проверяет, запущен ли сервер с помощью команды jls – и извлекает его JID (первая колонка вывода jls):

# vim /usr/local/bin/stopvserver

# Проверяем, запущен ли сервер
STRING=`jls | grep $IP`
if [ ! "$STRING" ]; then
echo "Сервер $IP не запущен"
exit 3
fi

# Узнаем jid сервера
JID=`echo $STRING | cut -d ' ' -f 1`

# Останавливаем jail-сервер
killall -j $JID -TERM > /dev/null 2>&1
sleep 1
killall -j $JID -KILL > /dev/null 2>&1
umount $JAILDIR/$IP/usr/ports/distfiles
umount $JAILDIR/$IP/usr/ports/packages
umount $JAILDIR/$IP/dev
umount $JAILDIR/$IP/proc
ifconfig $IF inet -alias $IP

Чтобы не заморачиваться с ручным запуском серверов, напишем скрипт vservers, который проверяет опцию vservers_enable в /etc/rc.conf, запускает все готовые виртуальные серверы во время загрузки ОС и останавливает во время шатдауна. Ключевые строки этого файла:

# vim /usr/local/etc/rc.d/vservers

# Получаем список работоспособных серверов
VSERVERS=`cat /usr/jailbase/db | grep -e ':ok$' | cut -d ':' -f 1`

# Процедура запуска серверов
vservers_start()
{
for IP in $VSERVERS; do
/usr/local/bin/startvserver $IP
done
}

# Процедура остановки серверов
vservers_stop()
{
for IP in $VSERVERS; do
/usr/local/bin/stopvserver $IP
done
}

Вот и все. Рассмотренные скрипты автоматизируют всю грязную работу. Больше не нужно компилировать окружение исполнения, добавлять IP-псевдонимы и редактировать файлы зон! Достаточно выполнить всего две команды, – и виртуальный сервер создан, запущен и полностью готов к использованию:

# IP=`addvserver new.host.com 7.1-RELEASE vasya@mail.ru base 0906061200 /публичный/ключ/Васи`
# startvserver $IP

Остановить и удалить сервер еще проще:

# stopvserver $IP
# delvserver $IP

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

Каркас создан

Мы создали вполне работоспособный каркас будущего сервиса. В следующей статье мы рассмотрим, как прикрутить к нему полноценный мониторинг, настройки для разных типов аккаунтов, систему бэкапа, наложим всевозможные ограничения и создадим гетерогенную систему, в которой смогут сосуществовать разные версии FreeBSD-окружений.

Содержание
ttfb: 594.11287307739 ms