鸡某某的剪贴板

在Docker下运行Systemd init

曾经,在挂载了/var/run/docker.sock之后,在Docker容器中即可控制整个Docker Daemon了。此时,在这个容器中,便有了启动更多容器的可能,配上一个合适的hostname,会让使用者在使用时,陷入“盗梦空间”式迷雾之中。

既然一个容器提供的cli可以无限接近一台实机,那么其实不妨跳出容器的哲学,让它在可控的范围之内运行一个完整的init,以提供一部分类似虚拟机的功能,比如服务,或者cron、rsyslog、DBus一类的。

必备条件

运行这个Systemd容器,必备的条件有以下的几点:

  • 系统支持CGroup,并挂载
  • 拥有SYS_ADMIN能力
  • 禁用对容器的安全上下文限制

不过,如果你使得这个容器进入特权模式,也是可取的。

Systemd在使用CGroup时,会用以区分不同的Slice的进程,从而对其资源消耗进行统计。

Systemd用分层的cgroup来描述不同的slice
Systemd用分层的cgroup来描述不同的slice

Dockerfile参考

FROM ubuntu:18.04

# necessary envs
ENV container docker
ARG LC_ALL=C
ARG DEBIAN_FRONTEND=noninteractive

# modify source.list
RUN sed -i 's/# deb/deb/g' /etc/apt/sources.list \
    && sed -i 's/archive.ubuntu.com/mirrors.163.com/g' /etc/apt/sources.list

# install systemd and ssh
RUN apt-get update \
    && apt-get install -y systemd ubuntu-minimal \
    && apt-get clean \
    && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*

# remove unnecessary units
RUN rm -f /lib/systemd/system/sysinit.target.wants/*.mount \
    && systemctl disable networkd-dispatcher.service

STOPSIGNAL SIGRTMIN+3
WORKDIR /
VOLUME ["/sys/fs/cgroup", "/tmp", "/run", "/run/lock"]
CMD ["/sbin/init"]

运行相关

在上述的Dockerfile中,有许多内容是与Systemd在Docker下运行息息相关的。

ENV container docker
STOPSIGNAL SIGRTMIN+3
VOLUME ["/sys/fs/cgroup", "/tmp", "/run", "/run/lock"]
CMD ["/sbin/init"]

第一行指定了环境变量container="docker",经测试,这一条环境变量会使得Systemd识别到Docker是它的虚拟化环境。同时,也会使得对init发送信号SIGRTMIN+3可以使Systemd正常关闭,也就是第二行里面的STOPSIGNAL SIGRTMIN+3

Systemd检测到了容器虚拟环境为Docker

第三行里面指定了四个Volumes。其中,/sys/fs/cgroup是系统CGroup的挂载点,Systemd依赖CGroup,这里显式地提示用户挂载CGroup目录(只读即可)。同时,Systemd会尝试在以下三个路径挂载tmpfs:

  • /tmp
  • /run
  • /run/lock

之后容器镜像在启动时,是需要运行Systemd进程的,往往系统都会将其软链接到/sbin/init中以兼容旧的Sysvinit模式。因而运行这个路径即可启动容器中的init也就是Systemd。

镜像构建相关

镜像构建过程,影响到的变量有这两个:

ARG LC_ALL=C
ARG DEBIAN_FRONTEND=noninteractive

第一个LC_ALL=C会将容器的地区设置为无,兼容所有基本的地区设置。

第二个DEBIAN_FRONTEND=noninteractive告知apt相关应用目前的进程是不可交互的,避免出现需要变更配置文件或者交互式配置情况出现,从而保证构建顺利进行。

不必要的unit

在上文已经阐述过了,既然是在容器内又无特权,那么需要将一部分和完全虚拟相关的部分——挂载去除。因而可以直接删除*.mount型unit。同时,因为容器网络由外部管理,其也没有NET_ADMIN的权限,无需响应变更,所以可以去除networkd-dispatcher.service这个unit。

# remove unnecessary units
RUN rm -f /lib/systemd/system/sysinit.target.wants/*.mount \
    && systemctl disable networkd-dispatcher.service

运行方式

运行方式这里,只提出docker-compose的方法,使用命令或者像Nomad一样的调度平台也是类似的。

version: "2.1"

services:
  test:
    tty: true
    build: ./build/bionic
    restart: on-failure
    network_mode: bridge
    mem_limit: 1G
    security_opt:
      - seccomp=unconfined
    cap_add:
      - SYS_ADMIN
    volumes:
      - /sys/fs/cgroup:/sys/fs/cgroup:ro
    tmpfs:
      - /tmp
      - /run
      - /run/lock

对于一些security_optcap_addvolumes以及tmpfs在上文其实也已经讲述过许多了。这里主要讲一个tty的问题。

这个很简单,就是只有分配了tty,Systemd才会在前台模式打出启动过程方便调试。

Systemd的启动过程

后记

在Docker里运行Systemd其实是一种禁术,使用之前记得评估自己的使用场景,合理使用。同时,如果将hostname也挂载进来,请提供一种方式来判断自己处于容器中抑或是宿主机上,以防造成误操作。

3 评论

  1. 有一说一,Docker 里面跑 systemd 确实牛逼

  2. 嗯,测试了一下,实际上这种玩法在Debian buster 以及之前的版本是可行的,因为内核默认用的 cgroupv1,但是从 Debian bullseye 之后内核默认用的是 cgroupv2,对于一些比较旧的镜像(比如 CentOS 7)自带的 systemd 还不支持 cgroupv2,就会出现问题。实际上从 cgroupv2 开始就不应该把 /sys/fs/cgroup 映射到容器内了。具体怎么操作可以参考这个GitHub链接:https://gist.github.com/pinkeen/bba0a6790fec96d6c8de84bd824ad933
    以及这个链接 https://serverfault.com/questions/1053187/systemd-fails-to-run-in-a-docker-container-when-using-cgroupv2-cgroupns-priva/1054414#1054414

发表回复