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

Мы давно следим за темой использования systemd в контейнерах. Еще в 2014 году наш инженер по безопасности Дэниел Уолш (Daniel Walsh) написал статью Running systemd within a Docker Container, а еще через пару лет – другую, которая называлась Running systemd in a non-privileged container, в которой он констатировал, что ситуация не очень-то и улучшилась. В частности, он писал, что «к сожалению, и два года спустя, если погуглить «Docker system», то первым делом всплывает всё та же его старая статья. Значит, пора что-то менять». Кроме того, мы уже как-то рассказывали о конфликте между разработчиками Docker и systemd.

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

В этой статье мы покажем, что изменилось за прошедшее время и как нам может помочь в этом вопросе Podman.

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

  1. Мультисервисные контейнеры – многие хотят вытащить свои мультисервисные приложения из виртуальных машин и запускать их в контейнерах. Лучше бы, конечно, разбить такие приложения на микросервисы, но не все пока это умеют или просто нет времени. Поэтому запуск таких приложений в виде сервисов, запускаемых systemd из юнит-файлов, вполне имеет смысл.
  2. Юнит-файлы Systemd – большинство приложений, работающих внутри контейнеров, собраны из кода, который до этого запускался на виртуальных или физических машинах. У этих приложений есть юнит-файл, который писался под эти приложения и понимает, как их надо запускать. Так что сервисы все же лучше запускать с помощью поддерживаемых методов, а не взламывая свою собственную init-службу.
  3. Systemd – это диспетчер процессов. Он осуществляет управление сервисами (завершает работу, перезапускает сервисы или выкашивает зомби-процессы) лучше, чем любой другой инструмент.

При этом есть и много причин для того, чтобы не запускать systemd в контейнерах. Основная заключается в том, что systemd/journald контролирует вывод контейнеров, а инструменты вроде Kubernetes или OpenShift рассчитывают, что контейнеры будут писать лог непосредственно в stdout и stderr. Поэтому, если вы собираетесь управлять контейнерами через средства оркестрации типа указанных выше, то надо серьезно обдумать вопрос использования контейнеров на базе systemd. Кроме того, разработчики Docker и Moby часто были резко против использования systemd в контейнерах.

Пришествие Podman’а

С радостью сообщаем, что ситуация наконец-то сдвинулась с мертвой точки. Команда, отвечающая в Red Hat за запуск контейнеров, решила разработать свой собственный контейнерных движок. Он получил имя Podman и предлагает такой же интерфейс командной строки (CLI) как у Docker’а. И практически все команды Docker точно так же можно использовать в Podman. Мы часто проводим семинары, которые теперь называются Меняем Docker на Podman, и первый же слайд призывает прописать: alias docker=podman.

Многие так и делают.

Мы со своим Podman’ом ни в коей мере не против контейнеров на основе systemd. Ведь Systemd чаще других используется в качестве init-подсистемы Linux, и не давать ей нормально работать в контейнерах значит игнорировать то, как тысячи людей привыкли запускать контейнеры.

Podman знает, что надо делать, чтобы systemd нормально работала в контейнере. Ей нужны такие вещи, как монтирование tmpfs на /run и /tmp. Ей нравится, когда включена «контейнерная» среда, и она ждет прав на запись в свою часть каталога cgroup и в папку /var/log/journald.

При запуске контейнера, в котором первой командой идет init или systemd, Podman автоматически настраивает tmpfs и Cgroups для того, чтобы запуск systemd прошел без проблем. Чтобы заблокировать такой авторежим запуска, используется опция —systemd=false. Обратите внимание, что Podman использует systemd-режим только тогда, когда видит, что надо выполнить команду systemd или init.

Вот выдержка из мануала:

man podman run

–systemd=true|false

Запуск контейнера в режиме systemd. По умолчанию включен.

Если внутри контейнера выполняется команда systemd или init, Podman настроит точки монтирования tmpfs в следующих каталогах:

/run, /run/lock, /tmp, /sys/fs/cgroup/systemd, /var/lib/journal

Также в качестве сигнала остановки по умолчанию будет использоваться SIGRTMIN+3.

Все это позволяет systemd работать в замкнутом контейнере без каких-либо модификаций.

ПРИМЕЧАНИЕ: systemd пытается выполнить запись в файловую систему cgroup. Однако SELinux по умолчанию запрещает контейнерам это делать. Чтобы разрешить запись, включите логический параметр container_manage_cgroup:

setsebool -P container_manage_cgroup true

Теперь посмотрите, как выглядит Dockerfile для запуска systemd в контейнере при использовании Podman’а:

# cat Dockerfile

FROM fedora

RUN dnf -y install httpd; dnf clean all; systemctl enable httpd

EXPOSE 80

CMD [ "/sbin/init" ]

Вот и всё.

Теперь собираем контейнер:

# podman build -t systemd .

Говорим SELinux разрешить systemd модифицировать конфигурацию Cgroups:

# setsebool -P container_manage_cgroup true

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

Теперь просто запускаем контейнер:

# podman run -ti -p 80:80 systemd

systemd 239 running in system mode. (+PAM +AUDIT +SELINUX +IMA -APPARMOR +SMACK +SYSVINIT +UTMP +LIBCRYPTSETUP +GCRYPT +GNUTLS +ACL +XZ +LZ4 +SECCOMP +BLKID +ELFUTILS +KMOD +IDN2 -IDN +PCRE2 default-hierarchy=hybrid)

Detected virtualization container-other.

Detected architecture x86-64.

Welcome to Fedora 29 (Container Image)!

Set hostname to <1b51b684bc99>.

Failed to install release agent, ignoring: Read-only file system

File /usr/lib/systemd/system/systemd-journald.service:26 configures an IP firewall (IPAddressDeny=any), but the local system does not support BPF/cgroup based firewalling.

Proceeding WITHOUT firewalling in effect! (This warning is only shown for the first loaded unit using IP firewalling.)

[  OK ] Listening on initctl Compatibility Named Pipe.

[  OK ] Listening on Journal Socket (/dev/log).

[  OK ] Started Forward Password Requests to Wall Directory Watch.

[  OK ] Started Dispatch Password Requests to Console Directory Watch.

[  OK ] Reached target Slices.

…

[  OK ] Started The Apache HTTP Server.

Всё, сервис запустился и работает:

$ curl localhost

<html  xml_lang="en" lang="en">

…

</html>

ПРИМЕЧАНИЕ: Не пытайтесь повторить это на Docker’е! Там по-прежнему нужны танцы с бубном, чтобы запускать такого рода контейнеры через демона. (Потребуются дополнительные поля и пакеты, чтобы все это бесшовно заработало в Docker, либо надо будет запускать в привилегированном контейнере. Подробности см. в статье.)

Еще пара крутых вещей о Podman и systemd

Podman работает лучше Docker в юнит-файлах systemd

Если контейнеры надо запускать при загрузке системы, то можно просто вставить соответствующие команды Podman в юнит-файл systemd, тот запустит сервис и будет его мониторить. Podman использует стандартную модель ветвления при исполнении (fork-exec). Иначе говоря, контейнерные процессы являются дочерними по отношению к процессу Podman’а, поэтому systemd легко может их мониторить.

Docker использует модель клиент-сервер, и CLI-команды Docker тоже можно размещать прямо в юнит-файле. Однако после того, как Docker-клиент подключается к Docker-демону, он (клиент) становится просто еще одним процессом, обрабатывающим stdin и stdout. В свою очередь, systemd понятия не имеет о связи между Docker-клиентом и контейнером, который работает под управлением Docker-демона, и поэтому в рамках этой модели systemd принципиально не может мониторить сервис.

Активация systemd через сокет

Podman корректно отрабатывает активирование через сокета. Поскольку Podman использует модель fork-exec, он может пробрасывать сокет своим дочерним контейнерным процессам. Docker так не умеет, поскольку использует модель клиент-сервер.

Сервис varlink, который Podman использует для взаимодействия удаленных клиентов с контейнерами, на самом деле активируется через сокет. Пакет cockpit-podman, написанный на Node.js и входящий в состав проекта cockpit, позволяет людям взаимодействовать с контейнерами Podman через веб-интерфейс. Веб-демон, на котором крутится cockpit-podman, посылает сообщения на varlink-сокет, который прослушивается systemd. После чего systemd активирует программу Podman для получения сообщений и начала управления контейнерами. Активация systemd через сокет позволяет обойтись без постоянно работающего демона при реализации удаленных API.

Кроме того, мы разрабатываем еще один клиент для Podman’а под названием podman-remote, который реализует тот же самый Podman CLI, но вызывает varlink для запуска контейнеров. Podman-remote может работать поверх SSH-сеансов, что позволяет безопасно взаимодействовать с контейнерами на различных машинах. Со временем мы планируем задействовать podman-remote для поддержки MacOS и Windows наряду с Linux, чтобы разработчики на этих платформах могли запускать виртуальную машину Linux с работающим Podman varlink и иметь полное ощущение, что контейнеры выполняются на локальной машине.

SD_NOTIFY

Systemd позволяет отложить запуск вспомогательных сервисов до того момента, пока не стартует необходимый им контейнеризованный сервис. Podman может пробросить сокет SD_NOTIFY в контейнеризованный сервис, чтобы это сервис уведомил systemd о своей готовности к работе. И опять же Docker, использующий модель клиент-сервер, так не умеет.

В планах

Мы планируем добавить команду podman generate systemd CONTAINERID, который будет генерировать юнит-файл systemd для управления конкретным заданным контейнером. Это должно работать как в root-, так и в rootless-режимах для непривилегированных контейнеров. Мы даже видел запрос на создания OCI-совместимой среды исполнения systemd-nspawn.

Заключение

Запуск systemd в контейнере – это вполне понятная потребность. И благодаря Podman у нас наконец-то есть среда запуска контейнеров, которая не враждует с systemd, а позволяет легко его использовать.

Источник: habr.com