Skip to content

Latest commit

 

History

History
296 lines (192 loc) · 25.7 KB

setting_up_containers_with_systemd-nspawn.md

File metadata and controls

296 lines (192 loc) · 25.7 KB

Настройка контейнеров с systemd-nspawn

Источник: Setting up containers with systemd-nspawn

Julian Harttung · 25 марта 2020 · 10 мин

Вокруг контейнеров уже довольно давно много шума. Их часто видят как «VM lite» (облегчённую виртуальную машину): они позволяют запускать сервисы в выделенных окружениях, изолируя их друг от друга и от хост-системы на нескольких уровнях.

Некоторые, вероятно, сейчас подумают: «Эй, нельзя вот так сравнивать виртуальные машины и контейнеры! Они же совершенно разные!». И я согласен: с технической точки зрения они довольно сильно различаются. Например, контейнеры работают на том же ядре операционной системы, что и хост. Однако если смотреть только на то, какие средства изоляции предоставляют и те и другие, сравнение всё равно имеет смысл.

Если вы уже читали о контейнерах, вы, вероятно, натыкались на Docker — самое популярное на сегодня контейнерное ПО — или на Kubernetes, который часто используется вместе с Docker для оркестрации окружений с множеством разных контейнеров. И хотя Docker и Kubernetes хороши сами по себе, не всегда хочется устанавливать и настраивать полноценную Docker-среду только для того, чтобы контейнеризовать один процесс.

О чём многие не знают: у systemd на самом деле есть собственный набор инструментов для создания контейнеров. Если вы используете актуальный дистрибутив Linux, например Debian 10 (buster), systemd у вас скорее всего уже установлен, так что для запуска контейнеров понадобится установить совсем немного дополнительного. В этой статье я хочу показать, как поднять простой контейнер с помощью инструментов, предоставляемых systemd.

Как работают контейнеры?

Лучший способ представить, что делают контейнеры, — думать в терминах пространств имён (namespaces). В вашей системе есть несколько пространств имён, доступ к которым можно ограничить. Рассмотрим некоторые из них:

Файловая система. Один из важнейших шагов по изоляции сервиса — ограничение доступа к файловой системе. Контейнеры предоставляют функциональность, подобную chroot, позволяя указать произвольный корень файловой системы. Это значит, что вы можете решать, какие программы установить в контейнер, полностью независимо от хост-системы. Однако это также значит, что нужно развернуть второе дерево ОС для контейнера, содержащее все инструменты, необходимые для работы операционной системы. Но это несложно, как мы увидим дальше. В итоге внутри указанного вами корневого каталога будет типичная иерархия файловой системы Unix, содержащая /bin, /etc, /home, /usr и так далее.

Идентификаторы процессов (PID). Когда вы выполняете ps -ef, вы видите все процессы, запущенные в системе, вместе с их уникальными PID. Однако мы не хотим, чтобы то, что запущено в контейнере, видело процессы хоста или других контейнеров. Кроме того, мы хотим, чтобы процесс в контейнере мог иметь, например, PID 1 (init), даже если на хосте уже есть свой PID 1.

Идентификаторы пользователей (UID). У каждого пользователя в системе есть идентификатор. У пользователя root обычно ID 0, а ID обычных пользователей начинаются с 1000. Пространства имён пользователей (user namespacing) — это в основном мера предосторожности: если кто-то сможет выбраться из вашего контейнера в хост-систему, у него не будет UID существующего на хосте пользователя. Без user namespacing, например, root-пользователь контейнера будет виден хост-системе как UID 0. Включение этой возможности прибавляет произвольное большое число ко всем UID внутри контейнера, так что root-пользователь контейнера (который внутри контейнера выглядит как UID 0) в хост-системе будет фактически иметь, скажем, UID 64789232.

Сетевые интерфейсы. Запуск ip addr на хосте показывает все доступные сетевые интерфейсы вашей машины. Запуск той же команды в контейнере даст ровно тот же результат, то есть контейнер имеет те же сетевые права, что и хост. Чтобы этого избежать, можно запретить контейнеру доступ к сетевым интерфейсам хоста и добавить для контейнера bridge, чтобы он всё же мог выходить в интернет и локальную сеть.

Все пространства имён управляются ядром Linux. Поддержку этих пространств имён можно включать или отключать при компиляции ядра. Если какие-то из них у вас не работают, возможно, они отключены или ваше ядро слишком старое.

Заметка о контейнерах с одним процессом

Некоторые в сообществе Docker продвигают модель «один процесс на контейнер», когда каждый создаваемый вами контейнер посвящён запуску ровно одного процесса внутри него. Однако, как предлагает и эта статья, хорошая идея — иметь в контейнере хотя бы минимальную init-систему. Проблема в том, что процессы могут порождать (fork()) дочерние процессы, и если родитель не вызывает корректно wait() для своих детей, ответственность за уборку ложится на процесс с PID 1 (init-процесс). Но если init-процесса нет, такие осиротевшие дочерние процессы становятся зомби. Некоторые процессы полагаются на init как на сборщика зомби — например, демоны при двойном форке. В этом руководстве systemd будет init-системой внутри нашего контейнера.

Создаём контейнер шаг за шагом

Я покажу, как создавать контейнеры на Debian, но шаги для других дистрибутивов Linux должны быть похожими. Обратите внимание: для выполнения команд из следующих разделов нужны права root.

Установка нужных пакетов

Нам понадобятся несколько пакетов для установки и запуска контейнера. Сначала установим их:

# apt install debootstrap systemd-container bridge-utils

debootstrap устанавливает минимальную систему Debian в указанный каталог. systemd-container содержит инструменты systemd для запуска и настройки контейнеров. bridge-utils позволяет легко настроить bridge, чтобы дать контейнеру сетевой доступ.

Разворачиваем дерево ОС

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

systemd ожидает, что контейнеры располагаются в каталоге /var/lib/machines. Они могут находиться и в другом месте, но тогда некоторые инструменты не распознают контейнер автоматически. Давайте развернём контейнер с именем helloworld:

# mkdir -p /var/lib/machines/helloworld
# debootstrap stable /var/lib/machines/helloworld http://deb.debian.org/debian/

Это установит всё необходимое для запуска Debian в наш новый контейнер (может занять некоторое время).

Также убедитесь, что только root имеет доступ к каталогу machines:

# chown root:root /var/lib/machines
# chmod 700 /var/lib/machines

Обзор контейнерных команд systemd

Теперь, когда операционная система развёрнута, самое время познакомить вас с командами, которыми вы будете управлять контейнером. Подробнее я объясню их по мере использования далее в руководстве.

systemd-nspawn используется для прямого запуска контейнера. По умолчанию он просто запускает root-shell внутри окружения контейнера. С ключом -b контейнер вместо этого загружается, после чего вы получаете login shell. Однако как только команда завершится, контейнер тоже выключится. Кроме того, если вы хотите использовать user namespacing, я настоятельно рекомендую всегда запускать с ключом -U, который включает user namespacing. При первом запуске systemd скорректирует все права на файлы внутри контейнера, и отключение user namespacing позже может привести к очень странным ошибкам (и файлам, принадлежащим nobody:nogroup). Наконец, ключом -M machine_name вы указываете, какой контейнер запустить.

machinectl очень похож на systemctl от systemd, но предназначен специально для работы с контейнерами. На самом деле контейнеры можно запускать, останавливать и включать точно так же, как любой другой сервис systemd. Файл шаблона сервиса находится в /lib/systemd/system/systemd-nspawn@.service. Стоит заглянуть внутрь, особенно на указанный исполняемый файл:

[Service]
ExecStart=/usr/bin/systemd-nspawn --quiet --keep-unit --boot --link-journal=try-guest --network-veth -U --settings=override --machine=%i

Как видите, при запуске контейнера через machinectl по умолчанию уже задаётся изрядное количество аргументов командной строки. Один из них — --boot (то же, что -b), поэтому контейнер загрузится, а не просто запустит root shell. --network-veth создаёт в контейнере виртуальный ethernet-интерфейс (без bridging) — об этом позже. Обратите внимание, что указан -U, то есть user namespacing будет включён.

/etc/systemd/nspawn — каталог, который может содержать конфигурационные файлы с настройками контейнеров. Все эти настройки можно задавать и через командную строку systemd-nspawn, но файлы избавляют от необходимости каждый раз набирать их вручную. Кроме того, конфигурационные файлы учитываются и при запуске контейнеров через machinectl. Если каталога не существует, просто создайте его:

# mkdir /etc/systemd/nspawn

Настройка контейнера

Теперь дерево ОС развёрнуто, и у вас есть базовое понимание инструментов systemd. Давайте запустим контейнер.

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

# systemd-nspawn -UM helloworld

Ваша командная строка теперь должна выглядеть так (или, по крайней мере, похоже):

root@helloworld:~# _

Теперь установите пароль с помощью

# passwd

Введите пароль root и снова выйдите из контейнера:

# exit

Теперь проверим, можем ли мы загрузить всё это:

# systemd-nspawn -UbM helloworld

Вы должны увидеть обычные сообщения загрузки системы. Если всё работает, через несколько секунд появится login shell. Войдите как root с только что установленным паролем.

Далее рекомендую установить dbus. Он нужен, если вы хотите заходить в запущенные контейнеры по machinectl с хоста.

# apt install dbus

Сейчас контейнер должен видеть те же сетевые интерфейсы, что и хост. Проверить это можно с помощью ip addr. Виртуальный ethernet мы настроим в следующем разделе. Однако уже сейчас можно сказать контейнеру автоматически поднимать виртуальный ethernet-интерфейс:

# echo 'auto host0' >> /etc/network/interfaces
# echo 'iface host0 inet dhcp' >> /etc/network/interfaces

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

# echo 'helloworld' > /etc/hostname

Ок, пока выйдем из контейнера:

# poweroff

Настройка хоста

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

В этом руководстве мы настроим на хосте сетевой bridge и подключим виртуальный ethernet-интерфейс контейнера к мосту. Это значит, что хост и контейнер будут видны в сети как две разные машины. Если ваш компьютер подключён к роутеру, он назначит контейнеру отдельный IP-адрес.

Обратите внимание: этот метод, скорее всего, не сработает, если вы на сервере со статическим IP-адресом. В этом случае вам понадобится настроить NAT на хосте или какой-нибудь прокси — здесь это рассматриваться не будет.

Сначала скажем systemd, что наш контейнер теперь будет использовать сетевой bridge. Это поддерживается systemd напрямую: он позаботится о подключении виртуального ethernet контейнера к bridge-интерфейсу хоста при запуске контейнера. Для настройки откройте /etc/systemd/nspawn/helloworld.nspawn в вашем любимом текстовом редакторе и добавьте следующие строки:

[Network]
VirtualEthernet=yes
Bridge=br0

Это укажет systemd подключать виртуальный ethernet host0 контейнера к сетевому интерфейсу br0 хоста.

Теперь у нас есть связь между контейнером и хостом, но всё ещё нужно создать и настроить bridge br0, чтобы контейнер действительно смог выйти в интернет. Убедитесь, что на хосте включена пересылка IPv4-пакетов: откройте /etc/sysctl.conf в вашем любимом редакторе и найдите net.ipv4.ip_forward. Убедитесь, что значение установлено в 1 и строка не закомментирована:

net.ipv4.ip_forward=1

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

Теперь откройте /etc/network/interfaces и добавьте следующие строки:

auto br0
iface br0 inet dhcp
    bridge_ports eth0

Определение интерфейса eth0 в этом файле уже должно быть. Убедитесь, что оно установлено в manual, изменив его на:

iface eth0 inet manual

Обратите внимание: если у вас установлен network-manager или что-то подобное, он может помешать нашей сетевой настройке. Вероятно, вам придётся отключить его или настроить так, чтобы он не трогал используемые здесь интерфейсы.

Отлично, теперь поднимем наш bridge:

# ifdown eth0
# ifup br0

Используйте ip addr, чтобы проверить, что всё сработало как надо. Вывод должен выглядеть примерно так (выделение моё):

1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host
       valid_lft forever preferred_lft forever
2: **eth0** : <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast **master br0 state UP** group default qlen 1000
    link/ether 08:00:27:3e:cf:c0 brd ff:ff:ff:ff:ff:ff
    inet 10.0.2.15/24 brd 10.0.2.255 scope global enp0s3
       valid_lft forever preferred_lft forever
3: **br0** : <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue **state UP** group default qlen 1000
    link/ether 08:00:27:3e:cf:c0 brd ff:ff:ff:ff:ff:ff
    inet 10.0.2.15/24 brd 10.0.2.255 scope global br0
       valid_lft forever preferred_lft forever
    inet6 fe80::a00:27ff:fe3e:cfc0/64 scope link
       valid_lft forever preferred_lft forever

Отлично, теперь и у хоста, и у контейнера должен быть доступ в интернет. Проверим:

$ ping google.com -c 4
# systemd-nspawn -UbM helloworld
$ ping google.com -c 4

Оба пинга должны пройти. Снова выйдем из контейнера:

# poweroff

Запуск контейнеров в фоне

Теперь мы умеем запускать контейнеры и дали им интернет через сетевой bridge. Это отличное начало, но нам, вероятно, хочется большего. Если вы планируете использовать контейнер, например, для запуска изолированного веб-сервера, вы, скорее всего, захотите, чтобы контейнер работал в фоне постоянно и автоматически загружался при каждой (пере)загрузке хост-машины.

Как я уже упоминал ранее в этой статье, systemd-контейнеры работают так же, как другие сервисы systemd, а значит, мы можем их start (запускать), stop (останавливать), enable (включать) или disable (отключать).

Запустить контейнер в фоне:

# machinectl start helloworld

Получить информацию о состоянии запущенного контейнера:

# machinectl status helloworld

Получить login shell в запущенном контейнере (требуется dbus на хосте и в контейнере):

# machinectl login helloworld

Покинуть сессию можно (как любезно сообщает systemd), нажав Ctrl + ] 3 раза в течение одной секунды.

Выключить запущенный контейнер:

# machinectl stop helloworld

Настроить контейнер на загрузку при каждом старте системы:

# machinectl enable helloworld

… и снова отключить автозапуск:

# machinectl disable helloworld

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

Устранение неполадок

В: При запуске контейнера я получаю ошибку «failed to become subreaper». Что делать?

О: Ваше ядро Linux, вероятно, слишком старое: для запуска контейнеров нужна версия ≥ 3.4. Подробнее о subreaper см. этот пост на stackoverflow.

Полезные man-страницы

  • man systemd-nspawn — опции запуска контейнеров
  • man machinectl — взаимодействие хоста с контейнерами и управление контейнерами
  • man systemd.nspawn — опции и синтаксис конфигурационных файлов
  • man bridge-utils-interfaces — больше информации о настройке мостов

systemd linux networking