Мы давно следим за темой использования systemd в контейнерах. Еще в 2014 году наш инженер по безопасности Дэниел Уолш (Daniel Walsh) написал статью Running systemd within a Docker Container, а еще через пару лет – другую, которая называлась Running systemd in a non-privileged container, в которой он констатировал, что ситуация не очень-то и улучшилась. В частности, он писал, что «к сожалению, и два года спустя, если погуглить «Docker system», то первым делом всплывает всё та же его старая статья. Значит, пора что-то менять». Кроме того, мы уже как-то рассказывали о конфликте между разработчиками Docker и systemd.
В этой статье мы покажем, что изменилось за прошедшее время и как нам может помочь в этом вопросе Podman.
Есть много причин для того, чтобы запускать systemd внутри контейнера, такие как:
При этом есть и много причин для того, чтобы не запускать systemd в контейнерах. Основная заключается в том, что systemd/journald контролирует вывод контейнеров, а инструменты вроде Kubernetes или OpenShift рассчитывают, что контейнеры будут писать лог непосредственно в stdout и stderr. Поэтому, если вы собираетесь управлять контейнерами через средства оркестрации типа указанных выше, то надо серьезно обдумать вопрос использования контейнеров на базе systemd. Кроме того, разработчики Docker и Moby часто были резко против использования systemd в контейнерах.
С радостью сообщаем, что ситуация наконец-то сдвинулась с мертвой точки. Команда, отвечающая в 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.
Вот выдержка из мануала:
Теперь посмотрите, как выглядит Dockerfile для запуска systemd в контейнере при использовании Podman’а:
Вот и всё.
Теперь собираем контейнер:
Говорим SELinux разрешить systemd модифицировать конфигурацию Cgroups:
Многие, кстати, забывают про этот шаг. К счастью, это достаточно сделать всего один раз и настройка сохраняется после перезагрузки системы.
Теперь просто запускаем контейнер:
Всё, сервис запустился и работает:
ПРИМЕЧАНИЕ: Не пытайтесь повторить это на Docker’е! Там по-прежнему нужны танцы с бубном, чтобы запускать такого рода контейнеры через демона. (Потребуются дополнительные поля и пакеты, чтобы все это бесшовно заработало в Docker, либо надо будет запускать в привилегированном контейнере. Подробности см. в статье.)
Если контейнеры надо запускать при загрузке системы, то можно просто вставить соответствующие команды Podman в юнит-файл systemd, тот запустит сервис и будет его мониторить. Podman использует стандартную модель ветвления при исполнении (fork-exec). Иначе говоря, контейнерные процессы являются дочерними по отношению к процессу Podman’а, поэтому systemd легко может их мониторить.
Docker использует модель клиент-сервер, и CLI-команды Docker тоже можно размещать прямо в юнит-файле. Однако после того, как Docker-клиент подключается к Docker-демону, он (клиент) становится просто еще одним процессом, обрабатывающим stdin и stdout. В свою очередь, systemd понятия не имеет о связи между Docker-клиентом и контейнером, который работает под управлением Docker-демона, и поэтому в рамках этой модели 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 и иметь полное ощущение, что контейнеры выполняются на локальной машине.
Systemd позволяет отложить запуск вспомогательных сервисов до того момента, пока не стартует необходимый им контейнеризованный сервис. Podman может пробросить сокет SD_NOTIFY в контейнеризованный сервис, чтобы это сервис уведомил systemd о своей готовности к работе. И опять же Docker, использующий модель клиент-сервер, так не умеет.
Мы планируем добавить команду podman generate systemd CONTAINERID, который будет генерировать юнит-файл systemd для управления конкретным заданным контейнером. Это должно работать как в root-, так и в rootless-режимах для непривилегированных контейнеров. Мы даже видел запрос на создания OCI-совместимой среды исполнения systemd-nspawn.
Запуск systemd в контейнере – это вполне понятная потребность. И благодаря Podman у нас наконец-то есть среда запуска контейнеров, которая не враждует с systemd, а позволяет легко его использовать.
В этой статье мы покажем, что изменилось за прошедшее время и как нам может помочь в этом вопросе Podman.
Есть много причин для того, чтобы запускать systemd внутри контейнера, такие как:
- Мультисервисные контейнеры – многие хотят вытащить свои мультисервисные приложения из виртуальных машин и запускать их в контейнерах. Лучше бы, конечно, разбить такие приложения на микросервисы, но не все пока это умеют или просто нет времени. Поэтому запуск таких приложений в виде сервисов, запускаемых systemd из юнит-файлов, вполне имеет смысл.
- Юнит-файлы Systemd – большинство приложений, работающих внутри контейнеров, собраны из кода, который до этого запускался на виртуальных или физических машинах. У этих приложений есть юнит-файл, который писался под эти приложения и понимает, как их надо запускать. Так что сервисы все же лучше запускать с помощью поддерживаемых методов, а не взламывая свою собственную init-службу.
- 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 xmlns="http://www.w3.org/1999/xhtml" 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, а позволяет легко его использовать.