Запускаем 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

Комментарии

Thank you for extensive info and concise article on this subject. It's nice to have it on one place to refer to like this.

Спасибо, есть интересные моменты, но есть и замечания.....

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