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

Комментарии (32)


  1. chemtech
    26.09.2019 10:23

    А когда мы сможем отказать от docker в kubernetes и заменить его на podman?


    1. vrutkovs
      26.09.2019 11:38

      Краткий ответ — никогда

      Более развернутый ответ — для того чтобы использовать программу как container engine в kubernetes эта программа должна иметь отдельный сокет для работы по интерфейсу CRI. Чтобы управлять этим сокетом программа должна быть запущена как демон. Потому придется превратить podman обратно в демон, как docker.

      Потому существует строгое разделение — podman для работы на пользовательских машинах (где демон только мешает и в большинстве случаев требует привелегий) и CRI-O — легковесный демон для работы с Kubernetes и специально заточенный под него. В итоге обе программы используют общий набор библиотек (libpod, containers/images) для работы.


      1. P6i
        26.09.2019 13:55

        Ну так через libpod и cri-o можно запускать поды на базе podman


        1. vrutkovs
          26.09.2019 14:08

          Пока нет, они используют общий список имаджей, но фактически информацию о контейнерах хранят в разных местах (планируется объединить)


  1. evg_krsk
    26.09.2019 11:28

    запрос на создания OCI-совместимой среды исполнения systemd-nspawn.

    О чём идет речь, не могли бы вы пояснить? Слова все известные, но смысл ускользает.


    1. vrutkovs
      26.09.2019 11:44

      Сейчас и docker и podman являются фактически обертками над runc, который собственно занимается созданием нужных неймспейсов. Разница в том, что в docker runc «впилен» в сам пакет докера, а в podman этот рантайм идет отдельно — и его можно заменять (к примеру, в Fedora 31 можно использовать crun).
      Кроме того, общение с runc происходит по стандартному протоколу, который регулируется runtime spec. С недавних пор этот протокол поддерживает systemd-nspawn, а значит его можно использовать вместо run


      1. P6i
        26.09.2019 13:58

        docker engine уже давно отделен от containerd, а у containerd, в свою очередь, runc тоже рантайм, который можно заменить, на тот же runv (https://github.com/hyperhq/runv) или любой OCI совместимый.
        Если под

        впилен
        имелось ввиду то, что, runc у докера идет в базе — то да, тут все верно


        1. vrutkovs
          26.09.2019 17:22

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


  1. amarao
    26.09.2019 12:45

    Что-то я его в новых убунтах не вижу. Оно RH-specific?


    1. past
      26.09.2019 15:41

      да


      1. amarao
        27.09.2019 14:18

        Тогда печально, потому что у докера один из сейлпоинтов — в универсальности. Улучшать докер для отдельного дистрибутива бесперспеткивно.


    1. vrutkovs
      26.09.2019 17:22

      Есть PPA


  1. r-moiseev
    26.09.2019 19:23
    +2

    Первый аргумент зачем очень странный. Какая нужда запускать сервисы в общем контейнере?


    1. mayorovp
      27.09.2019 09:13

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


      Когда контейнеры используются разработчиком — то же самое, скачать образ со всем необходимым dev-окружением намного проще, чем скачать три образа и настроить их связи.


      1. shadowlord
        27.09.2019 13:08

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


        1. mayorovp
          27.09.2019 13:34

          А вы что заказывали-то? Коробочное решение или что-то, что вы сможете масштабировать?


          Если первое — то сами и виноваты. Если второе — ну так пропишите в договоре-то в какой форме вы решение принимать будете.


          1. shadowlord
            28.09.2019 01:47

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


  1. gecube
    27.09.2019 10:19
    +1

    а не взламывая свою собственную init-службу.

    Извините, это перевод? Что подразумевалось? Или имелось в виду — hacking как "разрабатываю свою init-систему"?


  1. aleksey_c
    30.09.2019 11:40

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

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


    Каким образом нужно запускать Podman контейнер, чтобы его процессы были видны в systemctl status?

    Например, предложенный в официальной документации способ запуска приводит к тому, что systemd видит только сам процесс, запустивший контейнер, а именно "/usr/bin/podman start -a NAME". Точно так же ведёт себя Docker, если его запустить с ключом "-a". Как же systemd может мониторить процессы контейнера, если не видит их?


    1. gecube
      30.09.2019 11:59

      а зачем? Я проверил — systemd не мониторит, мониторит conmon


      root     26061  4.6  0.3 1418228 54512 ?       Ssl  11:58   0:00 /usr/bin/podman start -a redis_server
      root     26140  0.0  0.0  78012  1892 ?        Ssl  11:58   0:00 /usr/bin/conmon --api-version 1 -s -c 6e88c46f8c452e058c1bcfda8a77a9e1c36243d24a3149f16e5db1a7cba2f2a8 -u 6e88c46f8c452e058c1b
      999      26153  6.6  0.0  40700  4552 ?        Ssl  11:58   0:00  \_ redis-server *:6379

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


      1. aleksey_c
        30.09.2019 12:19

        В статье написано, что systemd может мониторить процессы контейнера. Мой вопрос, не о том, зачем это в принципе нужно, а о том, как именно этого добиться.

        Если под мониторингом подразумевается, что systemd видит процесс «podman start -a», который запускает контейнер, то это ничем не отличается от docker, процесс которого не завершается и systemd его видит, если запустить «docker start -a».


        1. gecube
          30.09.2019 13:23

          Не совсем понятно какую задачу хотите решить.
          Ведь нужно не просто мониторить дерево процессов (хотя conmon с этим худо-бедно вроде справляется), а еще смотреть на хелсчеки контейнера — и как они интегрируются с systemd — открытый вопрос (пока не проверял, но есть подозрение, что пока никак).
          Про forking — вижу, сами догадались.


          1. aleksey_c
            30.09.2019 13:41
            +1

            Задача — выяснить технические детали реализации в Podman. Более развёрнуто ответил ниже.


    1. vrutkovs
      30.09.2019 12:37
      +1

      systemd видит только сам процесс, запустивший контейнер, а именно "/usr/bin/podman start -a NAME". Точно так же ведёт себя Docker, если его запустить с ключом "-a".

      На первый взгляд — да, но самим контейнером управляет докер-демон — настраивает ему cgroup и другое. docker start -a здесь только команда демону запустить контейнер.


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


      1. aleksey_c
        30.09.2019 12:49
        +1

        Возможно, нашел правильный ответ в документации github.com/containers/libpod/blob/master/docs/podman-generate-systemd.1.md

        Вся магия в Type=forking и PIDFile=xxx.
        [Service]
        ...
        ExecStart=/usr/bin/podman start de1e3223b1b888bc02d0962dd6cb5855eb00734061013ffdd3479d225abacdc6
        Type=forking
        PIDFile=/run/user/1000/overlay-containers/de1e3223b1b888bc02d0962dd6cb5855eb00734061013ffdd3479d225abacdc6/userdata/conmon.pid
        ...


        В данном примере podman запускается без параметра "-a", то есть сам процесс, запустивший контейнер, завершается и systemd видит дерево процессов самого контейнера.

        К сожалению, проверить не удалось, так как текущая версия conmon из CentOS 7.7 не создаёт PID файл. Но в любом случае, теперь картинка сложилась.

        Кстати, для docker есть wrapper, который позволяет добиться того же результата, но другим способом, через манипуляции с cgroups контейнера github.com/ibuildthecloud/systemd-docker


        1. mayorovp
          30.09.2019 13:08
          +1

          Тут дело даже не в pid-файлах, а в дереве процессов. В случае podman systemd всегда знает запущен контейнер или остановлен. В случае docker systemd знает только запущен или остановлен клиент, а про контейнер не знает ничего.


          1. aleksey_c
            30.09.2019 13:34
            +1

            Только что проверил. Если запускать podman systemd unit через "/usr/bin/podman start -a NAME" и docker systemd unit через "/usr/bin/docker start -a NAME", то в результате будет одинаковое поведение systemd сервиса.

            Да, «под капотом» Podman и Docker контейнеры запустятся совершенно по разному, но с точки зрения systemd, сервис — это один процесс, который подключен к STDIN, STDOUT и STDERR дескрипторам процесса контейнера. Если podman или docker контейнер завершается с ошибкой, то systemd сервис так же завершится с ошибкой. Если podman или docker контейнер завершается успешно, то systemd сервис так же завершится успешно. В этом смысле поведение systemd сервиса одинаковое.

            При использовании github.com/containers/libpod/blob/master/docs/podman-generate-systemd.1.md в случае Podman или github.com/ibuildthecloud/systemd-docker в случае Docker, systemd сервисом будет не один процесс, инициировавший запуск контейнера, а всё дерево процессов самого контейнера.

            Для чего это нужно — вопрос отдельный. Мне были интересны технические детали реализации в Podman. Как мы выяснили, там это реализовано через создание процессом conmon PID файла, из которого systemd сам узнаёт PID контейнера и дерево процессов контейнера.


            1. mayorovp
              30.09.2019 13:51
              +1

              А в каких именно сценариях поведение оказалось одинаковое? Меня вот интересуют вот эти:


              1. Запустите сервис-контейнер, прибейте демон докера (dockerd кажется, но могу ошибаться), после чего попробуйте определить статус сервиса-контейнера через sysctl.


              2. Запустите сервис-контейнер, прибейте демон докера, после чего остановите сервис-контейнер через sysctl. Когда демон докера запустится снова, в каком состоянии сервис-контейнер окажется?


              3. Просто прибейте процесс docker и дождитесь пока systemd попытается его перезапустить.



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


              1. gecube
                30.09.2019 13:53

                Запустите сервис-контейнер, прибейте демон докера (dockerd кажется, но могу ошибаться), после чего попробуйте определить статус сервиса-контейнера через sysctl.

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


                --live-restore                          Enable live restore of docker when containers are still running

                https://docs.docker.com/engine/reference/commandline/dockerd/
                https://docs.docker.com/config/containers/live-restore/
                или я неправильно понял ее?


                sysctl

                небольшое замечание. Не sysctl, а systemctl


                предлагаю рассмотреть как будет работать зависимость сервисов друг от друга при падении демона докера, во всех вариантах

                Никак. Потому что ее на уровне докер-демона нет. Он стартует все пачкой, если стоит restart: always или unless-stopped


                1. mayorovp
                  30.09.2019 14:02
                  +1

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

                  Да нет, без неё поведение docker как раз больше похоже на поведение podman в видимой systemd части.


                  Никак. Потому что ее на уровне докер-демона нет. Он стартует все пачкой, если стоит restart: always или unless-stopped

                  Она есть на уровне systemd, и отслеживать зависимости — его прямая обязанность. Вот как раз использовать restart: always или unless-stopped в таких сценариях нельзя никак.


                  1. gecube
                    30.09.2019 14:08

                    systemd в состоянии отслеживать, что контейнер готов, а не просто запущен, но без танцов с бубном?


                    Она есть на уровне systemd, и отслеживать зависимости — его прямая обязанность. Вот как раз использовать restart: always или unless-stopped в таких сценариях нельзя никак.

                    да, я просто уточняю.


                    Пока самый внятный способ интеграции docker & systemd — это либо через systemd-docker, либо ручками, но с run --rm, чтобы в случае сбоя контейнер удалился (может быть опасно для данных, но главное компоуз в системди не пихать).


        1. gecube
          30.09.2019 14:01

          На opensuse создает


          linux-x1:~ # ls /var/run/containers/storage/btrfs-containers/
          6e88c46f8c452e058c1bcfda8a77a9e1c36243d24a3149f16e5db1a7cba2f2a8  b2c87cf4ebb2a2e09ea6de6487360598da76f85f82a61d5f2c497733b6784d0b

          linux-x1:~ # zypper if conmon
          
          Retrieving repository 'openSUSE-Tumbleweed-Source' metadata .............................................................................................................................[done]
          Building repository 'openSUSE-Tumbleweed-Source' cache ..................................................................................................................................[done]
          Loading repository data...
          Reading installed packages...
          
          Information for package conmon:
          -------------------------------
          Repository     : openSUSE-Tumbleweed-Oss         
          Name           : conmon                          
          Version        : 2.0.1-1.1                       
          Arch           : x86_64                          
          Vendor         : openSUSE                        
          Installed Size : 72.6 KiB                        
          Installed      : Yes (automatically)             
          Status         : up-to-date                      
          Source package : conmon-2.0.1-1.1.src            
          Summary        : An OCI container runtime monitor
          Description    :                                 
              Conmon is a monitoring program and communication tool between a
              container manager (like podman or CRI-O) and an OCI runtime (like
              runc or crun) for a single container.