Работая над более менее сложным проектом, приходится поддерживать окружение необходимое для корректного функционирования системы. Нередко это выливается в ситуацию, когда на машине установлено такое количество пакетов и зависимостей, что возникает масса проблем:
На Java-платформе эти проблемы менее заметны за счёт развитых инструментов управления зависимостей, а также замкнутости платформы. В экосистеме Java не очень жалут C/C++ библиотеки. Все популярные драйвера и библиотеки написаны непосредственно на Java, поэтому не возникает проблем с управлением системными зависимостями. А для управления библиотеками в Java есть масса зрелых инструментов.
На других платформах PHP/Python проблема более актуальна, даже не смотря на наличие таких инструментов как composer
и virtualenv
.
Но в той или иной степени эти проблемы характерны для всех платформ, и разница лишь в том, при каком уровне сложности системы вы начнёте эти проблемы испытывать. Где-то раньше, где-то позже.
Виртуализация отчасти помогает решить проблему. Установив разные приложения в разные виртуальные машины, в изолируете их друг от друга, тем самым избавляя себя от лишних проблем. Разделяй и влавствуй, так сказать. Виртуализация так же позволяет стандартизировать окружение в котором работает приложение. На одной операционной системе проще поддерживать работоспособность приложения, чем на трех.
VirtualBox отличная, полноценная система виртуализации, которую я могу рекомендовать всем, кому она нужна. Но для меня полная аппаратная виртуализация это overkill, так как в подавляющем большинстве случаев я работаю с одной и той же операционной системой, — Linux.
Со временем, я сформулировал свои хотелки:
Используя VirtualBox’овский differencing image можно вполне адекватно закрыть третий пункт. В остальном же, это решение как из пушки по воробям.
Отчасти Vagrant позволяет решить задачу автоматизация создания новых машин и их первоначальной настройки. Цена за это — отсутствие поддержки differencing images, и как следствие, много занятого места под образы виртуальных машин.
Решением для меня стал LXC (Linux Containers). Это система виртуализация уровня ОС, которая позволяет внутри хост операционной системы запустить дочерние ОС, у каждой из которых:
При этом ядро остаётся от операционной системы хоста. Последний аспект для меня имеет 2 важных преимущества:
Также LXC умеет работать с BTRFS, которая поддерживает Copy-on-Write. Это позволяет мгновенно скопировать образ базового контейнера, который я использую как шаблон для создания новых, и при этом объем занятого места на SSD не увеличивается.
Таким образом, я закрыл все 4 своих хотелки:
VirtualBox | Vagrant | LXC | |
---|---|---|---|
Время создания (с.) | <1 | ~3 | <1 |
Время запуска (с.) | 3-5 | 3-5 | <1 |
Накладные расходы на VM (Мб.) | ~0 | >100 | ~0 |
По большому счёту “разработчищеский шмурдяк”. На данный момент у меня контейнеризированы следующие сервисы:
Так как я работаю на Mac OS X, то мне всё равно требуется промежуточная виртуальная машина с Linux’ом, и как следствие, VirtualBox никуда не пропадает. Из двух зол выбирают меньшее, что поделать. Вся схема выглядит следующим образом:
Для упрощения дальнейшего повествования определимся с терминологией:
В кратце, процедура подготовки “полигона” для контейнеризации сводится к следующему:
После успешной установки, на Linux необходимо поставить все необходимые пакеты.
# apt-get install bridge-utils btrfs-tools lxc
Для быстрого и бездупликатного клонирования контейнеров нам необходим 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 не зависит от порядка регистрации блочных устройств в системе.
Наиболее удобный, на мой взгляд, способ организации сетевого взаимодействия хоста и виртуальных машин для задач разработки следующий. На каждой виртуальной машине и каждом контейнере имеется два интерфейса:
eth0
– который NAT’ится и обеспечивает доступ машины в Интернет;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
закоментирован специально.
Для того чтобы контейнеры можно было создавать быстро, необходимо подготовить базовую копию, которая и будет клонироваться. Делается это командной:
# 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
В данный момент контейнер готов к запуску. Для работы с контейнерами используются команды 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
и прочие), установлены, и если нет, установить их.
Для быстрого создания нового контейнера, необходимо ввести следующую команду:
# 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
Для подключения к контейнеру можно воспользоваться следующей командой:
# 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’а, — контейнер уничтожается. Для различного рода небольших экспериментов, самое оно.
Для того, чтобы вся эта схема не просто работала, а работала удобно, я дополнительно устанвливаю Avahi и OpenSSH.
Для того, чтобы к машинам можно было обращаться по имени и не заниматься поиском их 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
.
Обычно я использую 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
Для управления контейнерами на виртуальной машине доступны команды 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 указанных команд становятся доступны на хосте.
Схема была бы намного проще, если бы моей основной операционной системой был бы Linux. В этом случае, нет необходимости в промежуточном VirtualBox’е и становится возможным запускать в контейнерах GUI приложения. По сумме факторов, Mac OS X для меня работает лучше, как компромиссное решение одновременно рабочих и личных задач. Поэтому, приходится мириться с такой матрёшкой.