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

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

На Java-платформе эти проблемы менее заметны за счёт развитых инструментов управления зависимостей, а также замкнутости платформы. В экосистеме Java не очень жалут C/C++ библиотеки. Все популярные драйвера и библиотеки написаны непосредственно на Java, поэтому не возникает проблем с управлением системными зависимостями. А для управления библиотеками в Java есть масса зрелых инструментов.

На других платформах PHP/Python проблема более актуальна, даже не смотря на наличие таких инструментов как composer и virtualenv.

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

Виртуализация отчасти помогает решить проблему. Установив разные приложения в разные виртуальные машины, в изолируете их друг от друга, тем самым избавляя себя от лишних проблем. Разделяй и влавствуй, так сказать. Виртуализация так же позволяет стандартизировать окружение в котором работает приложение. На одной операционной системе проще поддерживать работоспособность приложения, чем на трех.

Что не так с VirtualBox? Link to heading

VirtualBox отличная, полноценная система виртуализации, которую я могу рекомендовать всем, кому она нужна. Но для меня полная аппаратная виртуализация это overkill, так как в подавляющем большинстве случаев я работаю с одной и той же операционной системой, — Linux.

Со временем, я сформулировал свои хотелки:

  1. хочу иметь возможность быстро создавать новые виртуальные машины для экспериментов. Быстро по времени и количеству команд, которые надо ввести;
  2. хочу чтобы виртуальные машины быстро загружались (1-2 секунды, не дольше);
  3. хочу чтобы создание новой виртуальной машины не требовало сотен мегабайт на моём и так не очень большом SSD;
  4. хочу чтобы у виртуальной машины был низкий memory footprint, чтобы было возможным держать параллельно 5-6 запущенных машин без существенных издержек для host-операционной системы.

Используя VirtualBox’овский differencing image можно вполне адекватно закрыть третий пункт. В остальном же, это решение как из пушки по воробям.

Отчасти Vagrant позволяет решить задачу автоматизация создания новых машин и их первоначальной настройки. Цена за это — отсутствие поддержки differencing images, и как следствие, много занятого места под образы виртуальных машин.

Решение к которому я пришёл Link to heading

Решением для меня стал LXC (Linux Containers). Это система виртуализация уровня ОС, которая позволяет внутри хост операционной системы запустить дочерние ОС, у каждой из которых:

  • своя виртуальная память;
  • своя файловая система
  • свой сетевой стек.

При этом ядро остаётся от операционной системы хоста. Последний аспект для меня имеет 2 важных преимущества:

  • молниеносная скорость загрузки и останова (менее 1 секунды);
  • отсутствие накладных расходов на обслуживание отдельного для каждой виртуальной машины ядра (в первую очередь, по памяти).

Также LXC умеет работать с BTRFS, которая поддерживает Copy-on-Write. Это позволяет мгновенно скопировать образ базового контейнера, который я использую как шаблон для создания новых, и при этом объем занятого места на SSD не увеличивается.

Таким образом, я закрыл все 4 своих хотелки:

  • контейнер создается одной командой, которая выполняется менее секунды;
  • загружается контейнер тоже не более секунды;
  • за счёт BTRFS на SSD не дублируются общие между контейнерами системные файлы, коих большинство;
  • контейнеры сравнительно эффективны по памяти, что позволяет их держать запущенными в фоновом режиме без особых проблем для хоста.
VirtualBoxVagrantLXC
Время создания (с.)<1~3<1
Время запуска (с.)3-53-5<1
Накладные расходы на VM (Мб.)~0>100~0

Что у меня стоит в контейнерах Link to heading

По большому счёту “разработчищеский шмурдяк”. На данный момент у меня контейнеризированы следующие сервисы:

  • MySQL, для тестирования приложений;
  • MongoDB, по аналогичной причине;
  • Jekyll, который я использую для написания этого блока;
  • IPython, который я в последнее время всё чаще испольщую как бесплатный аналог Mathematica;
  • несколько проектов с настроенным окружением для прогона интеграционных тестов, над которыми я работаю.

Как настроить LXC? Link to heading

Так как я работаю на Mac OS X, то мне всё равно требуется промежуточная виртуальная машина с Linux’ом, и как следствие, VirtualBox никуда не пропадает. Из двух зол выбирают меньшее, что поделать. Вся схема выглядит следующим образом:

Схема контейнеров

Для упрощения дальнейшего повествования определимся с терминологией:

  • хост — Mac OS X, которая установлена непосредственно на ноутбуке;
  • VM – аппаратно виртуализированный Ubuntu, который стоит под VirtualBox’ом на Mac OS X;
  • контейнер — LXC-контейнер, который стоит на виртуализированном Ubuntu под VirtualBox’ом, в доме, который построил Джек.

В кратце, процедура подготовки “полигона” для контейнеризации сводится к следующему:

  1. поставить VirtualBox;
  2. создать новую виртуальную машину со следующими особенностями:
    • два сетевых интерфейса: NAT и Host-only network. Первый нужен для доступа в интернет, второй для того, чтобы на виртуальную машину можно было попасть с хоста;
    • два HDD. Первый непосредственно под ОС, второй для хранения образов контейнеров. Второй HDD будет размечен под BTRFS. Теоретически, можно обойтись одним HDD на BTRFS. Проверка работоспособности такой схемы остаётся на совести читателя.
  3. на виртуальную машину поставить любимый Ubuntu. Я использовал 14.04 Server;

После успешной установки, на Linux необходимо поставить все необходимые пакеты.

# apt-get install bridge-utils btrfs-tools lxc

Подготовка BTRFS тома Link to heading

Для быстрого и бездупликатного клонирования контейнеров нам необходим BTRFS-том. Если вы подключили к VM два жестких диска, то второй будет доступен под именем /dev/sdb. Создаём на нём файловую систему:

# mkfs.btrfs -m single /dev/sdb

После успешного завершения команды добавляем запись в /etc/fstab, чтобы том автоматически монтировался в /var/lib/lxc (директория по-умолчанию с образами LXC-контейнеров) при старте системы.

UUID=476c0e89-f2cb-4660-89b1-f639354ea402 /var/lib/lxc  btrfs   defaults        0       1

В этом снипете требуется поменять UUID блочного устройства на свой. Узнать его можно командой blkid:

# blkid /dev/sdb
/dev/sdb: UUID="476c0e89-f2cb-4660-89b1-f639354ea402" UUID_SUB="3c20d5df-4e53-4aea-9749-b1e79e3204aa" TYPE="btrfs"

Вместо UUID=476c0e89-f2cb-4660-89b1-f639354ea402 можно использовать /dev/sdb. Первый вариант предпочтителен, так как UUID не зависит от порядка регистрации блочных устройств в системе.

Настройка сети на VM Link to heading

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

  1. eth0 – который NAT’ится и обеспечивает доступ машины в Интернет;
  2. eth1 – который привязан к Host-only сети VirtualBox и обеспечивает связь всех виртуальных машин между собой и с хостом.

Обратите внимание, инструкции в этой статье опираются на то, что интерфейсы сконфигурированны именно с такими именами (eth0 для NAT и eth1 для Host-only Network).

Для того, чтобы контейнеры могли получить доступ к Host-only сети VirtualBox’а, необходимо сконфигурировать сетевой мост поверх eth1. Делается это в файле /etc/network/interfaces.

auto lo
iface lo inet loopback

auto eth0
iface eth0 inet dhcp

#auto eth1
#iface eth1 inet dhcp

auto br1
iface br1 inet dhcp
  bridge_ports eth1
  bridge_fd 0
  bridge_maxwait 0

Обратите внимание, eth1 закоментирован специально.

Создание и настройка базового контейнера Link to heading

Для того чтобы контейнеры можно было создавать быстро, необходимо подготовить базовую копию, которая и будет клонироваться. Делается это командной:

# lxc-create -t download -n base

Системе понадобится некоторе время, чтобы скачать необходимые пакеты из Интернета и подготовить образ базового контейнера.

Теперь необходимо пробросить в LXC интерфейсы, которые мы сконфигурировали на VM. Редактируем /etc/default/lxc-net:

USE_LXC_BRIDGE="true"
LXC_BRIDGE="lxcbr0"
LXC_ADDR="192.168.10.1"
LXC_NETMASK="255.255.255.0"
LXC_NETWORK="192.168.10.0/24"
LXC_DHCP_RANGE="192.168.10.2,192.168.10.254"
LXC_DHCP_MAX="253"

Я выбрал сеть 192.168.10.0/24 произвольно, это решение можно без опаски изменить исходя из вашего контекста.

Необходимо также проверить что оба интерфейса корректно прокинуты в базовый контейнер, а также корректно скофигурированы внутри контейнера. Для этого, во-первых, убедитесь что в файле /var/lib/lxc/base/config есть следующие строки:

lxc.network.type = veth
lxc.network.flags = up
lxc.network.link = lxcbr0
lxc.network.hwaddr = 00:16:3e:13:85:c8

lxc.network.type = veth
lxc.network.flags = up
lxc.network.link = br1
lxc.network.hwaddr = 00:16:3e:c0:e6:94

MAC-адреса в вашем случае будут отличаться. Интерфейс lxcbr0 отвечает за доступ к Интернету посредством NAT, а br1 за доступ в Host-only сеть созданную VirtualBox’ом.

Во-вторых, проверьте, что интерфейсы сконфигурированы в файле /var/lib/lxc/base/rootfs/etc/network/interfaces.

auto lo
iface lo inet loopback

#auto eth0
iface eth0 inet dhcp

auto eth1
iface eth1 inet dhcp

Интерфейс eth0 внутри контейнера у меня отключен, то есть контейнер при загрузке не имеет доступа к Интернету. Причины сугубо лично-параноидальные. Если вам это не подходит, раскоментируйте строку auto eth0. Я же включаю Интернет когда мне необходимо:

# ifup eth0

Запуск базового контейнера Link to heading

В данный момент контейнер готов к запуску. Для работы с контейнерами используются команды lxc-*. Первая и самая простая выводит список контейнеров, а также их статус:

# lxc-ls -f
NAME     STATE    IPV4          IPV6  AUTOSTART
-----------------------------------------------
base     STOPPED  -             -     NO

Обратите внимание, что все LXC команды надо выполнять от root’а. Есть возможность работать с контейнерами от непривилегированного пользователя, но это несколько сложнее. Для запуска контейнера достаточно выполнить:

# lxc-start -n base -d
# lxc-attach -n base

Первая команда запустит контейнера, а вторая запустить shell в namespace’е этого контейнера. Вуаяля, вы в контейнере. Я советую всегда передавать ключ -d в lxc-start, в противном случае ваш текущий терминал будет привязан к терминалу контейнера и единственный вариант как его можно отвязать, — потушить контейнер.

На данном этапе целесообразно проверить что все системные утилиты, которые вам требуются в повседневной работе (например, curl, strace, sar и прочие), установлены, и если нет, установить их.

Клонирование и запуск дочерних контейнеров Link to heading

Для быстрого создания нового контейнера, необходимо ввести следующую команду:

# lxc-clone -o base -n test -s
Created container test as snapshot of base

Эта команда клонирует контейнер base под именем test. Обязательно надо указывать ключ -s, иначе магия BTRFS не сработает и будет выполняться полное копирование образа контейнера.

Если вы всё правильно настроили контейнер склонируется моментально. На моей машине это занимает десятки миллисекунд!

# time lxc-clone -o base -n test -s
Created container test as snapshot of base

real	0m0.027s
user	0m0.004s
sys	0m0.010s

Подключение к контейнеру и его останов Link to heading

Для подключения к контейнеру можно воспользоваться следующей командой:

# lxc-attach -n test

Эта команда запустит shell по-умолчанию в контейнере. Из этого shell’а вы уже можете делать всё что вам необходимо: устанавливать ПО, настраивать сам контейнер и т.д. Эту же команду можно использовать для запуска произвольных процессов в контейнере:

# lxc-attach -n test -- hostname
test

Ещё одна полезная команда lxc-start-ephemeral. Она позволяет запустить контейнер, который будет уничтожен как только будет остановлен. Очень удобно её использовать следующим образом:

# lxc-start-ephemeral -o base -- bash
root@base-m58tibcs:~# exit

В данном случае, на основании контейнера base создаётся новый временный контейнер с произвольным именем. Как только вы выходите из ассоциированного с ним shell’а, — контейнер уничтожается. Для различного рода небольших экспериментов, самое оно.

Доводка окружения Link to heading

Для того, чтобы вся эта схема не просто работала, а работала удобно, я дополнительно устанвливаю Avahi и OpenSSH.

Установка и настройка Avahi Link to heading

Для того, чтобы к машинам можно было обращаться по имени и не заниматься поиском их IP-адерсов, как на виртуальной машине, так и в базовом контейнере ставим Avahi. Это реализация mDNS протокола поддержка которого есть в том числе и в Mac OS X (Bonjour).

# apt-get install avahi-daemon

В файл /etc/avahi/avahi-daemon.conf в базовос контейнере прописываем:

allow-interfaces=eth1
deny-interfaces=eth0

В тот же файл на виртуальном машине:

allow-interfaces=eth1,br1,eth0,lxcbr0

Так же прописываем в /etc/default/avahi-daemon на обоих машинах:

AVAHI_DAEMON_DETECT_LOCAL=0

После изменений не забываем перезапускать avahi-daemon:

# service avahi-daemon restart

Теперь каждая машина будет доступна по своему имени (см. hostname) в зоне .local. Например, контейнер с именем foo будет доступен по имени foo.local.

Установка и настройка SSH Link to heading

Обычно я использую OpenSSH для администрирования всей этой чехарды. Для этого, на VM и в базовый образ контейнера ставится OpenSSH:

# apt-get install openssh-server

Убеждаемся, что в /etc/ssh/sshd_config прописано:

UseDNS no

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

$ ssh-copy-id ubuntu@containers.local
$ ssh-copy-id ubuntu@base.local

Это существенно убыстрит процесс авторизации на машине. Утилита ssh-copy-id, к сожалению, недоступна в базовой поставке Mac OS X. Её можно поставить через brew. Если у вас нет brew, вы можете добавить свой публичный ключ вручную.

После этого конфигурирем SSH-клиент на хосте для удобного доступа на виртуальную машину и контейнеры. Для этого в ~/.ssh/config прописываем:

Host *.local
	User ubuntu
	PreferredAuthentications publickey

Здесь задаётся имя пользователя по-умолчанию, и предпочитаемый способ аутентификации. В купе с тем что Avahi автоматически сообщает всем доменные имена контейнеров, для того чтобы попасть на контейнер с именем foo теперь достаточно ввести команду:

$ ssh foo.local

Управление контейнерами с хоста Link to heading

Для управления контейнерами на виртуальной машине доступны команды lxc-*. Ещё раз приведу краткий их список (полный список вы можете получить набрав в консоли виртуальной машины lxc- и нажав Tab):

  • lxc-start — запуск контейнера;
  • lxc-stop — останов контейнера;
  • lxc-clone — клонирование контейнера;
  • lxc-destroy — удаление контейнера;
  • lxc-attach — подключение к консоли контейнера/запуск процесса в namespace’е контейнера;
  • lxc-ls — список контейнеров и их статусы;
  • lxc-start — запуск контейнера;
  • lxc-wait — позволяет подождать пока контейнер не перейдёт в определённый статус (например, запустится).

Для выполнения этих команд, необходимо сначала зайти на виртуальную машину с Ubuntu. Это не всегда удобно всегда не удобно. Поэтому, я добавил в ~/.zshrc (~/.bashrc для Bash) следующие строки:

CONTAINERS_HOST="containers.local"
function lxc-ls() { ssh -q $CONTAINERS_HOST sudo lxc-ls "$@" }
function lxc-clone() { ssh -q $CONTAINERS_HOST sudo lxc-clone -s "$@" }
function lxc-start() { ssh -q $CONTAINERS_HOST sudo lxc-start -d "$@" }
function lxc-stop() { ssh -q $CONTAINERS_HOST sudo lxc-stop "$@" }
function lxc-destroy() { ssh -q $CONTAINERS_HOST sudo lxc-destroy "$@" }

Теперь 5 указанных команд становятся доступны на хосте.

Послесловие Link to heading

Схема была бы намного проще, если бы моей основной операционной системой был бы Linux. В этом случае, нет необходимости в промежуточном VirtualBox’е и становится возможным запускать в контейнерах GUI приложения. По сумме факторов, Mac OS X для меня работает лучше, как компромиссное решение одновременно рабочих и личных задач. Поэтому, приходится мириться с такой матрёшкой.