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

12/16/2018

На просторах интернета вы можете найти некоторое количество руководств, описывающих, как запустить systemd в docker контейнере. Однако нашей лаборатории не удалось выявить что-то действительно стоящее, так как некоторые из них предлагают вам запускать контейнер в привилигированном режиме (серьезно?), некоторые предлагают поудалять в полуручном режиме все юниты из /var/lib/systemd/system и /etc/systemd/system, которые при первом же апдейте будут восстановлены пакетным менеджером... Вообщем держите наш рецеп по запуску systemd в docker контейнере.

1. Устанавливаем правильный default.target

Вместо того, чтобы удалять файлы юнитов из каталогов /usr/lib/systemd/system и /etc/systemd/system, которые при обновлении пакетов будут успешно восстановлены пакетным менеджером, мы создадим свой собвственный default.target. Напомним, default.target - это юнит типа ".target", который сам по себе ничего не делает, а лишь является поинтером - своего рода состоянием системы. Выставляя в качестве зависимостей к таргету другие юниты, вы заставляете systemd загружать группу сервисов, точек монтировая, таймеров и т.д. А default таргет является конечной точкой загрузки системы. Весь процесс можно представить ввиде елочки, где вершиной является default.target. Systemd начинает строить дерево зависимостей сверху вниз, начиная с default.target, а стартует юниты само-собой снизу. О таргетах мы, кстати, уже писали тут.

Не трудно догадаться, что default.target, который назначен в вашем образе операционной системы будет пытаться стартануть контейнер как настоящую операционную систему: монтировать файловые системы, создавать сокеты, запускать терминалы (что, кстати, приводит к захвату контейнером основной системы)... Поэтому-то люди и пытаются удалить юниты, которые это делают. А мы просто меняем default.target не приводя систему в неконсистентное состояние.

Итак, создадим файл с названием container.target следующего содержания:

$ cat container.target
 
[Unit]
Description=Container target  - special target for running systemd in docker containers

Как вы видите, наш таргет не делает ничего. А как же dbus, спросите вы, как же journald? Известны случаи, когда люди получают вот такое сообщение об ошибке, если не запущен dbus:

Failed to get D-Bus connection: Operation not permitted

Ответ таков: во-первых, systemd сам себе dbus. Да-да, systemd является еще и dbus сервером, но особым "только для своих". Утилита systemctl не использует тот самый dbus, а подключается к dbus, встроенному в systemd. Если попытаться повысить уровень лога systemd до дебага, то среди прочего будет сообщение

Successfully created private D-Bus server

Это значит, systemd готов принимать сообщения от утилиты systemctl. Кстати, если кто не знал, сокет подключения ко встроенному в systemd dbus-серверу находится тут: /run/systemd/private.

Во-вторых, если ваш сервис нуждается в нормальном dbus, journald или еще-какой-либо зависимости, то пишите ваши юнит-файлы правильно - всегда перечисляйте все зависимости в "Requires=" или "Wants=". Это поможет избежать ситуации, когда сервису не хватает чего-то для нормальной работы.

Итак, создадим Dockerfile и соберем контейнер на примере CentOS

$ cat Dockerfile
 
FROM centos
 
ENV container=docker  
 
COPY container.target /etc/systemd/system/container.target
 
RUN ln -sf /etc/systemd/system/container.target /etc/systemd/system/default.target
 
ENTRYPOINT ["/sbin/init"]
 
CMD ["--log-level=info"]
 
STOPSIGNAL SIGRTMIN+3
 
$ docker build --tag centos:systemd .

Маленькое пояснение по поводу "ENV container=docker" и "STOPSIGNAL SIGRTMIN+3". Первая команда делает возможным для systemd определить, что он запущен внутри контейнера и автоматически делать или наоборот не далеть некторые вещи. А стопсигнал позволяет правильно завершать работу. Docker обычно посылает процессу, запущенному внутри контейнера, SIGTERM при вызове команды "docker stop". И большинство процессов воспринимают этот сигнал в качестве требования корректно завершить работу. Systemd же для этих целей использует SIGTMIN+3 и не реагирует на SIGTERM

2. Монтируем необходимые файловые системы

Вместо того, чтобы как многие рекомендуют, запускать docker контейнер с systemd в привилигированном режиме или с расширенными привилегиями типа CAP_SYS_ADMIN, мы дадим systemd все, что ему нужно для работы в непривилигированном режиме, а именно специальные ядерные файловые системы, которые в случае с реальным компьютером systemd пытается монтировать в процессе загрузки. В их числе /sys/fs/cgroup, /sys/fs/fuse и /var/run

$ docker run \
  --detach \
  --name=centos-systemd \
  --mount type=bind,source=/sys/fs/cgroup,target=/sys/fs/cgroup \
  --mount type=bind,source=/sys/fs/fuse,target=/sys/fs/fuse \
  --mount type=tmpfs,destination=/run \
  --mount type=tmpfs,destination=/run/lock centos:systemd

Что плохого вас тут может поджидать... На некоторых системах вы можете встретить вот такое сообщение

Failed to determine whether /sys is a mount point: Operation not permitted
Failed to determine whether /proc is a mount point: Operation not permitted
Failed to determine whether /dev is a mount point: Operation not permitted
Failed to determine whether /dev/shm is a mount point: Operation not permitted
Failed to determine whether /run is a mount point: Operation not permitted
Failed to mount tmpfs at /run/lock: Operation not permitted
Failed to determine whether /sys/fs/cgroup is a mount point: Operation not permitted
Failed to determine whether /sys/fs/cgroup/systemd is a mount point: Operation not permitted
[!!!!!!] Failed to mount API filesystems, freezing.
Freezing execution.

Страшного ничего в этом нет. Это просто означает, что вы стали жертвой стечения обстоятельств. С некоторой версии докер стал блокировать системный вызов "name_to_handle_at", который используется systemd для определения примонтированных файловых систем. И решений у этой проблемы ровно три. Во-первых, убедитесь, что вы используете самую свежую версии образа операционной системы и дополнительно выясните, нет ли в ней еще отдельного обновления для systemd - разработчики systemd нашли способ обхода проблемы, поэтому, если ваш дистрибьютор оперативно доставляет обновления в систему, то это скорее всего поможет. Если нет, тогда либо выключаем для контейнера фильтрацию системных вызовов путем добавления дополнительного ключа запуска

--security-opt seccomp:unconfined

Любо, если вы немного параноик и все еще хотите оставить фильтрацию включенной, тогда скачиваем профиль seccomp по-умолчанию отсюда, листаем до "syscalls", далее "names", находим длиннющий список разнообразных системных вызовов и в любое его место вставляем "name_to_handle_at". Файл в формате json - если вставляете в середину, запятая нужна, если в конец, то нет. Далее запускаем контейнер с этим профилем - нам понадобится добавить ключ запуска, всё тот же "--security-opt seccomp:/${PATH_TO_NEW_SECCOMP_PROFILE}", только всемто "unconfined" как в прошлом примере, мы передаем путь до новго файла профиля seccomp.

Всё! Имеем docker контейнер с systemd внутри, работающей утилитой systemctl и даже возможностью произвести shutdown

П.с. вы можете найти уже готовые образы для запуска systemd в docker контейнере в нашем репозитории на Docker Hub

Добавить комментарий