systemd的容器与VM组件

systemd-container是systemd的容器组件。它和LXC对标,比chroot更强大。它虚拟化了文件系统、进程树以及客户系统中的进程间通信。

systemd-container容器有五个原则性限制:

  1. 仅允许以只读方式访问例如/sys/proc/sys/sys/fs/selinux这样的内核文件系统。
  2. 禁止修改主机的网络接口以及系统时钟。
  3. 禁止创建设备节点。
  4. 禁止重启主机操作系统。
  5. 禁止加载内核模块。

它的吸引力在于由systemd-nspawn运行的容器中的systemd可以与宿主系统的systemd组件交流。举例来说,一个容器的日志可以输出到宿主系统的日志中。它的核心控制命令为systemd-nspawn

systemd-vmspawn则是systemd开发组制作的一个Qemu的Wrapper程序,其目的也是在于方便systemd,主要原因在于nspawn并不能测试systemd和固件的集成以及initrd的功能。

快速开始

在进入容器之前我们都需要一个最基本的根文件系统,对于systemd-nspawn/systemd-vmspawn来说,这个文件系统可以是一个.raw格式的镜像文件,也可以是一个目录。

下面主要以一个目录作为容器的根节点来说明systemd-nspawn/systemd-vmspawn的使用方法。

systemd-nspawn/systemd-vmspawn可以像Chroot一样以一个目录作为根来启动一个容器/VM,并且不需要手动挂载/proc/run/dev等目录,退出容器/VM也会自动清理资源,不容易使主机系统崩溃。

  • Chroot进入容器目录:systemd-nspawn -D /path-to-rootfs
  • 以指定目录为根启动一个VM:systemd-vmspawn -D /path-to-rootfs
    • 根文件系统的/boot目录中必须保存至少一个UKI,或是内核 + Initrd + Type1引导项。

systemd-nspawn不仅能像chroot一样以一个目录作为一个新的根,并且还能以一个完整的系统启动流程(即,从/sbin/init启动)来启动一个Chroot环境,就像LXC一样。

启动一个完整的Chroot系统环境:systemd-nspawn --resolv-conf=off -bD /path-to-rootfs,对应的.nspawn配置文件是:

1
2
3
4
5
6
7
8
[Network]
Private=no
VirtualEthernet=no

[Exec]
Boot=yes
PrivateUsers=no
ResolvConf=off

以Ubuntu-base为例,在 http://cdimage.ubuntu.com/ubuntu-base/releases/ 下载Ubuntu base版Tar包,解包并配置一些初始化文件(例如/etc/hosts/etc/hostname/etc/resolv.conf)后,使用systemd-nspawn -D /path-to-ubuntu命令进入容器,此时可以使用aptuseradd等命令。

exit退出容器后,使用systemd-nspawn -bD /path-to-ubuntu启动进入容器系统,此时可以和真实系统一样进行操作。除了正常退出容器,还可以使用Ctrl+三下]操作强制退出。

安装

1
2
3
4
5
# Debian
apt install systemd-container

# RHEL
yum install systemd-container

创建容器/VM

systemd-nspawn/systemd-vmspawn可以直接启动容器/VM,就像chroot一样,而不对容器/VM进行统一化管理。

如果用户希望对容器进行统一的服务化管理,则需要使用machinectl辅助;将容器镜像导入machinectl,它会调用systemd-nspawn/systemd-vmspawn启动容器/VM并对容器/VM镜像进行统一管理。

需要注意的是,对于systemd-nspawn来说,一个镜像(根文件系统)就对应一个可引导的容器,它们不是两个概念。

自行生成镜像

用户可以使用任意的可引导镜像启动一个nspawn容器,举例来说,用户可以使用debootstrap工具,执行debootstrap --include=systemd stable /var/lib/machines/debian创建一个基本Debian根文件系统。更推荐的工具是MKOSI。

然后,用户只需要执行importctl -m import-fs 根文件系统目录 [容器名]命令将容器的根文件系统目录导入为可用镜像即可。

手动下载镜像

和LXC一样,systemd-nspawn有自己官方的镜像发布站点,用户可以在 Images (nspawn.org)Index of /storage (nspawn.org) 寻找镜像,也可以使用LXC的镜像。

nspawn镜像也有自己的国内镜像站了:

systemd还提供了一个负责下载与导入容器镜像的D-Bus服务systemd-importd.service,它可以被用户下载/导入镜像的命令触发启动,并执行对应的操作。

在下载镜像Tar包/Raw包后,用户可以执行importctl -m import-tar|import-raw 归档文件路径 容器名 [--read-only]调用该服务进行导入。

当然,用户也可以将镜像解包,然后通过importctl -m import-fs 根文件系统目录 [容器名]命令进行导入。

直接拉取镜像

使用该命令拉取镜像:

1
importctl -m pull-[tar|raw] https://hub.nspawn.org/storage/发行版/版本/架构/文件名.xz [容器名]

默认(--keep-download=true)会在拉取后,将该镜像以.tar-完整链接|.raw-完整链接命名(注意该命名是隐藏的),以只读方式保存在/var/lib/machines/,并创建一个名为 NAME 的可写镜像。这是为了在之后提高下载速度(使用本地缓存)。

  • 如果只创建缓存,不需要可写镜像,可以把 NAME 设为-
  • 如果不需要创建缓存镜像,使用-N|--kepp-download=no选项。
  • 默认情况下会进行校验,如果不希望,使用--verify=no
  • 拉取的是容器镜像,所以需要使用-m|--class=machine选项。

也有一种简单的方法,就是使用 nspawn/nspawn 的封装脚本,它会自动导入密钥并进行拉取,使用方法为:

1
2
-i|--init  拉取镜像
-l|--list 列出可用镜像

具体可拉取的镜像可以看 https://hub.nspawn.org/storage/list.txt

Chroot环境

使用systemd-nspawn命令临时启动一个Chroot环境,它的选项影响了Chroot环境的初始化配置,一些选项如下:

  • -M|--machine=:如果容器镜像通过systemd-importd的标准方式(即通过importctl)导入为systemd-machined镜像(即可以通过machinectl list-images列出),那么容器镜像可以直接通过这一选项选中。此名称还会被设为容器的默认主机名(如果容器内没有/etc/hostname文件的话)。对应的.nspawn配置项为Hostname=
  • -i|--image=:指定磁盘镜像。
  • -D|--directory=:指定容器根文件系统目录。
  • --mstack:指定容器根文件系统的 Mstack。
  • -b|--boot:启动容器中的init进程。
  • -U:启用非特权容器,这和LXC的概念一样。
  • -j:尝试将容器内的日志收集到宿主机上。
  • -E NAME=VALUE|--setenv=NAME=VALUE:在启动容器时,设置一些环境变量。
  • --capability=:在启动容器时,给容器添加一些特权,这对非特权容器有用。all表示提供所有特权。
  • --read-only:只读启动。
  • -x|--ephemeral:临时模式,在退出容器时抹除任何操作。
  • --resolv-conf=off|auto:对容器内/etc/resolv.conf的处理。auto表示拷贝宿主机的/etc/resolv.confoff表示不进行操作。对应的.nspawn配置项为ResolvConf=
  • --timezone=off|auto:对容器内/etc/timezone的处理。auto表示拷贝宿主机的/etc/timezoneoff表示不进行操作。对应的.nspawn配置项为Timezone=

执行这条命令设置容器密码:

1
systemd-nspawn -D 根文件系统目录 passwd

然后使用这条命令启动容器即可:

1
systemd-nspawn --resolv-conf=off -D 根文件系统目录 -M 容器名 -b [-U]

启动虚拟机

使用systemd-vmspawn可以立刻调用Qemu启动一个虚拟机,它的选项影响了虚拟机环境的初始化配置,一些选项如下:

  • -M|--machine=:如果VM镜像通过systemd-importd的标准方式导入为systemd-machined镜像(即可以通过machinectl list-images列出),那么VM镜像可以直接通过这一选项选中。此名称还会被设为VM的默认主机名(如果VM内没有/etc/hostname文件的话)。
  • -i|--image=:指定VM磁盘镜像。
  • --forward-journal=:使用systemd-journal-remote将VM内的日志传递给宿主系统。
  • --cpus=:指定VM核心数量。
  • --ram=:指定VM内存量。
  • --tpm=:是否为VM启用TPM。
  • --secure-boot=:是否为VM启用安全启动。
  • -n|--network-tap:是否为VM启用TAP网络。

执行这条命令启动VM:

1
2
3
4
5
systemd-vmspawn -i Raw镜像路径 \
--cpus= \
--ram= \
--tpm=no \
--secure-boot=no

使用Ctrl + A,然后按X强制终止虚拟机。

统一管理

machinectl是容器统一管理命令,它的操作有以下几种:

镜像管理

列出可用镜像

1
machinectl list-images
  • -a|--all:列出所有镜像,包括本机.host

查看镜像情况

1
machinectl show-image|image-status 镜像名

克隆一个镜像

1
machinectl clone 镜像名 新镜像名

重命名一个镜像

1
machinectl rename 镜像名 新镜像名

把镜像只读化

1
machinectl read-only 镜像名 yes|no

删除镜像

1
machinectl remove 镜像名

一键清除旧版镜像

1
machinectl clean

容器/VM管理

容器在systemd-nspawn/systemd-vmspawn中指的是一个运行中的镜像实例。

服务化启动容器/VM

1
machinectl start [-V] 镜像名
  • 这会启动容器/VM并创建一个systemd-nspawn@容器名.service/systemd-vmspawn@VM名.service服务。
  • 隐含了这些systemd-nspawn选项:--boot --network-veth --link-journal=try-guest --settings=override -U
  • 要特别注意,systemd-nspawn@容器名.service中默认包含StandardInput=null,因此Parameters=/bin/*shBoot=no的容器在启动时Shell会因输入EOF而立刻退出。不过Boot=yes的容器会从init启动,则不受影响。

列出容器

1
machinectl list
  • -a|--all:列出所有容器/VM,包括本机.host

查看容器状态

1
machinectl show|status 容器/VM名

关机|重启|终止|杀死容器

1
machinectl poweroff|reboot|terminate|kill 容器/VM名
  • -s|--signal=:发送的信号。

这等价于systemctl stop systemd-nspawn@容器名/systemctl stop systemd-vmspawn@VM名

进入容器/VM终端

1
machinectl login 容器/VM名

和LXC类似,登入时会通过systemd-getty-generator创建一对伪终端对连接到容器中。容器中连接的终端总是使用/dev/console

  • 值得一提的是,登录会话不会创建用户的systemd实例

如果容器中的系统阻止登录,尝试在容器内的/etc/securetty添加:

1
2
3
4
5
pts/0
pts/1
pts/2
pts/3
pts/4

在容器/VM中执行命令

1
machinectl shell [用户名@]容器/VM名 [命令]
  • -E|--setenv=VAR[=VALUE]:设置一个环境变量。
  • 若没有命令,默认在容器中以指定用户身份启动Shell。

容器/VM开机启动

启用:

1
machinectl enable 容器/VM名
  • 这等价于systemctl enable systemd-nspawn@容器名|systemd-vmspawn@VM名

禁用:

1
machinectl disable 容器/VM名

这等价于systemctl disable systemd-nspawn@容器名|systemd-vmspawn@VM名

从主机上拷贝文件

1
machinectl copy-to 容器/VM名 主机路径 [容器/VM路径]

从容器/VM中拷贝文件

1
machinectl copy-from 容器/VM名 容器/VM路径 [主机路径]

临时配置目录映射

1
machinectl bind 容器/VM名 主机路径 [容器/VM路径]
  • --read-only:只读映射。
  • --mkdir:映射时,如果目录不存在则创建。

Mstack

260以后,systemd支持了Mstack镜像格式,这种镜像格式实际上就是OCI容器镜像的systemd本地格式。systemd-nspawn也引入了对从Mstack启动容器的直接支持。但是需要注意:

  • Mstack容器必须要么禁用UIDMAP,要么使用--private-users=managed模式来让systemd-nsresourced.service托管UIDMAP,--private-users=pick的Chown模式对分层镜像来说不可用。
  • 由于上一条原因,Mstack容器很难使用machinectl进行管理,因为systemd-nspawn@.service单元中配置了--private-users=pick,而且PrivateUsers=属于特权指令,又没办法通过.nspawn文件配置。

日常运行:

1
systemd-nspawn --network-veth --settings=override --mstack /镜像路径

你可以使用systemd-run来封装运行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
systemd-run \
--unit="$1" \
--service-type=notify \
--property=KillMode=mixed \
--property=Slice=machine.slice \
--property=Delegate=yes \
--property=DelegateSubgroup=supervisor \
--property=TasksMax=16384 \
--property=TimeoutSec=2min \
--property=WatchdogSec=3min \
--property=RestartForceExitStatus=133 \
--property=SuccessExitStatus=133 \
--property=CoredumpReceive=yes \
--property="ExecStopPost=systemd-nspawn --cleanup --machine=$1" \
--collect \
systemd-nspawn \
--quiet \
--keep-unit \
--link-journal=try-guest \
--network-veth \
--private-users=managed \ # 或者不设置
--settings=override \
--machine="$1"

容器注册表必须支持标准OCI协议(application/vnd.oci.image.manifest.v1+json)而不能只支持Docker私有协议(application/vnd.docker.distribution.manifest.v2+json),你可以这样测试:

1
2
3
4
curl -sS -H \
'Accept: application/vnd.oci.image.manifest.v1+json, application/vnd.oci.image.index.v1+json' \
-I \
https://<registry>/v2/<image>/manifests/<tag>

配置

文件路径

systemd-nspawn的配置存储在 /etc/systemd/nspawn/容器镜像名.nspawn/run/systemd/nspawn/容器镜像名.nspawn 和容器镜像所在的相同目录下的容器镜像名.nspawn文件。

systemd-vmspawn并没有设计独立的配置文件,但你可以通过修改systemd-vmspawn@.service单元来配置systemd-vmspawn的行为。

网络配置

和LXC不同,systemd-nspawn容器可以使用主机的网络命名空间,也就是说,systemd-nspawn容器可以使用主机网络或者私有网络两种模式。

主机网络

Chroot环境中默认采用这一模式。即容器直接使用主机的网络命名空间,容器共享主机的网络接口,容器将能够访问主机上的所有网络服务,来自容器的数据包将在外部网络中显示为来自主机(即共享同一IP地址)。

使用主机网络时,容器内是无权修改主机的网络状态(没有CAP_NET_ADMIN),例如IP地址的,所以不必担心网络配置发生变化。

主机网络模式下,容器中不需要进行网络配置,因此也不需要启动网络配置服务(例如systemd-networkdNetworkManagerifupdown,但一般情况下即使启动了,网络配置服务也能分辨出这种情况,不会报错。

在使用主机网络时,容器根文件系统中默认的DNS服务器往往不能用,需要手动进行配置

  • 如果使用systemd-resolved:执行sed -i '/^#DNS=/c\DNS=8.8.8.8\' /etc/systemd/resolved.conf,然后重启systemd-resolved.service
  • 如果不使用systemd-resolved:执行echo 'nameserver 8.8.8.8' > /etc/resolv.conf配置DNS。

私有网络模式

所有私有网络模式均强依赖容器中进行网络配置(手动配置,或是通过网络配置服务,例如systemd-networkdNetworkManagerifupdown)和宿主环境上的systemd-networkd.service服务(不必启用systemd-networkd.service对本体网络管理的托管功能(89-ethernet.network.example提供),只会用到systemd-networkd.service的容器网络配置功能(80-container-*.network提供))

你必须:

  • 在容器中,要么从/sbin/init启动以使其在初始化过程中启动网络配置服务,要么自行编写脚本配置网络(支持DHCP协议,ip link set host0 up后使用DHCP客户端直接从host0接口发起请求即可)。
    • dhclient -1 host0
    • udhcpc -i host0 -n -q -f
    • dhcpcd -1 -w host0
  • 在宿主环境中,启动systemd-network.socket套接字。

否则容器中将没有网络连接

仅本地模式

容器仅仅只有本地回环网络,不存在其他任何网络接口。

没什么意义的网络模式,在使用systemd-nspawn启动容器时,添加选项--private-network即可。

如果使用machinectl,创建容器配置容器镜像名.nspawn,添加以下内容:

1
2
[Network]
Private=yes
Veth(NAT)模式

给容器创建一个Veth设备,用于和主机之间通信,容器直接通过宿主机的路由和NAT对外部网络进行通信。

在使用systemd-nspawn启动容器时,添加选项-n|--network-veth即可。

通过systemd-nspawn@.service实例化而来的容器默认启用该模式,其对应的容器配置是:

1
2
3
[Network]
[Private=yes]
VirtualEthernet=yes

VirtualEthernet=yes隐式地包含了Private=yes配置项,不过,如果创建容器配置容器镜像名.nspawn,添加以下内容:

1
2
3
[Network]
Private=no
[VirtualEthernet=no]

那么便会覆盖默认配置,启用主机网络。

容器内的Veth设备名为host0@ifX,而宿主机侧的Veth设备名为ve-容器名@ifX

网络的自动配置依赖systemd-networkd.service实现:

  • 主机上的systemd-networkd.service会根据/usr/lib/systemd/network/80-container-ve.network启动DHCP服务端。NAT将通过/usr/lib/systemd/network/80-container-ve.network中的IPMasquerade=both选项自动完成。
  • 客户容器上的systemd-networkd.service会根据/usr/lib/systemd/network/80-container-host0.network配置文件启动DHCP客户端。这一方你也可以使用其他的网络配置服务。

以此实现容器网络的自动配置。注意,宿主机必须启动systemd-networkd.service服务以对Veth进行配置,否则容器中的Veth会处于NO-CARRIER未配置状态。

如果使用其他网络管理工具并且不愿意启动systemd-networkd.service服务,可以这样做:

  1. 手动在主机上配置DHCP服务端。
  2. 在容器中配置静态IP或是DHCP客户端。
  3. 配置类似iptables -t nat -A POSTROUTING -s 192.168.163.192/28 -j MASQUERADE这样的NAT规则。
区域(Zone)模式

这种模式会通过systemd-networkd.service在主机上自动创建一个“网络区域”(其实就是一个Independent网桥),容器内的Veth接入指定的网络区域,对外部网络的通信通过NAT实现。相同网络区域内的容器可以互通,但是不同网络区域内的容器不可以。

在使用systemd-nspawn启动容器时,添加选项--network-zone=区域名即可。

如果使用machinectl,创建容器配置容器镜像名.nspawn,添加以下内容:

1
2
3
[Network]
Private=yes
Zone=区域名

NAT实际上是通过/usr/lib/systemd/network/80-container-vz.network中的IPMasquerade=both选项配置由systemd-networkd.service自动完成的。

端口映射

如果为容器开启了私有网络,那么可以在使用systemd-nspawn启动容器时,添加选项-p|--port=[协议:]宿主端口[:容器端口]进行端口映射(默认协议为tcp,默认容器端口与宿主一致)。

如果使用machinectl,创建容器配置容器镜像名.nspawn,添加以下内容:

1
2
[Network]
Port=[协议:]宿主端口[:容器端口]

需要注意:

  1. 这个映射是通过宿主环境的systemd-networkd.service监听内核NETLINK_ROUTE事件触发的,无论容器中的网络配置是如何进行的(任意一种网络配置服务或是手动配置)。
  2. 这个映射是通过内核Netfilter实现的(不是Docker那种Proxy),不会在宿主环境下占用Socket,你可以通过nft list ruleset检查io.systemd.nat链确认规则。
  3. 这个映射的目标地址不接受127.0.0.0/8,请求127.0.0.1会导致访问拒绝。
桥接模式

将容器对应的主机端Veth直接接入到指定的网桥上,网桥应当是一个Host-shared网桥

在使用systemd-nspawn启动容器时,添加选项--network-bridge=网桥即可。

如果使用machinectl,创建容器配置容器镜像名.nspawn,添加以下内容:

1
2
3
[Network]
Private=yes
Bridge=网桥

容器内的Veth设备名为host0@ifX,而宿主机侧的Veth设备名为vb-容器名@ifX

专用网卡

直接将主机网卡分配给容器,直到容器停止前,主机都不能使用该网卡。

在使用systemd-nspawn启动容器时,添加选项--network-interface=设备名即可。

如果使用machinectl,创建容器配置容器镜像名.nspawn,添加以下内容:

1
2
[Network]
Interface=

IPVLAN/MACVLAN

systemd-nspawn容器还可以直接在主机的网络接口上创建IPVLAN/MACVLAN,只需要添加选项--network-ipvlan=网络接口--network-macvlan=网络接口即可。

如果使用machinectl,创建容器配置容器镜像名.nspawn,添加以下内容:

1
2
3
[Network]
#MACVLAN=网络接口
#IPVLAN=网络接口

IPVLAN设备在容器中被命名为iv-主机接口@ifX,MACVLAN设备在容器中被命名为mv-主机接口@ifX。需要注意的是,NetworkManager没有办法配置容器内的MACVLAN/IPVLAN网络接口(NetworkManager配置MACVLAN需要父接口位于同一命名空间),因此在容器中必须对其进行排除,具体方法为编辑/etc/NetworkManager/NetworkManager.conf,添加以下内容:

1
2
3
4
5
[main]
plugins=keyfile

[keyfile]
unmanaged-devices=type:macvlan;type:ipvlan

然后,在容器中使用systemd-networkd配置网络,或者直接使用iproute2进行手动配置,具体为执行:

1
ip addr add IP/CIDR dev iv-XXX|mv-XXX

MACVLAN网卡可以直接使用DHCP;IPVLAN不能使用DHCP,必须手动配置静态IP。

目录映射

在使用systemd-nspawn启动容器时,添加选项:

  • --bind=源路径:目标路径[:挂载选项]:添加目录映射。
  • --bind-ro=源路径:目标路径[:挂载选项]:添加只读目录映射。
  • --tmpfs=挂载路径[:挂载选项]:在容器内挂载一个Tmpfs内存文件系统。
  • --overlay=|--overlay-ro=:映射OverlayFS,接受一系列冒号分隔的目录路径列表,最后一个路径表示容器内映射路径。

如果使用machinectl,创建容器配置容器镜像名.nspawn,添加以下内容:

1
2
3
4
5
[Files]
Bind=源路径:目标路径[:挂载选项]
BindReadOnly=源路径:目标路径[:挂载选项]
TemporaryFileSystem=挂载路径[:挂载选项]
Overlay=|OverlayReadOnly=

设备节点映射

对于设备节点,除了设置Bind=/dev/设备外,还需要编辑对应的systemd-nspawn@容器名.service,添加:

1
2
[Service]
DeviceAllow=/dev/设备 rw

资源限制

使用systemctl set-property systemd-nspawn@容器名 限制项=值命令对容器使用的资源进行限制。

常用的限制项有:

  • MemoryMax=:限制内存使用。
  • AllowedCPUs=:允许使用的CPU核心。
  • CPUQuota=:最大CPU占用率。

VM配置

要对VM进行配置,创建/etc/systemd/system/systemd-vmspawn@.service.d/(或针对特定VM创建/etc/systemd/system/systemd-vmspawn@VM名.service.d/)目录并在其中保存Drop-in配置文件,例如:

1
2
3
[Service]
ExecStart=
ExecStart=systemd-vmspawn --quiet --register=yes --keep-unit --network-tap --secure-boot=no --tpm=no --machine=%i

示例

一个没有任何权限限制的超级容器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
[Exec]
PrivateUsers=no
Capability=all
ResolvConf=bind-host
Timezone=bind

[Network]
Private=no

[Files]
BindReadOnly=/etc/passwd
BindReadOnly=/etc/group
BindReadOnly=/etc/shadow
BindReadOnly=/etc/gshadow
Bind=/home
Bind=/root
Bind=/tmp

固定容器/VM的子网

同一子网下的容器/VM才可以互相通信。默认情况下,容器/VM的网络会随机分配。

拷贝容器/VM的网络配置,调高优先级:

1
2
3
cp /usr/lib/systemd/network/80-container-xx.* /etc/systemd/network/30-container-xx.*
# 或者
cp /usr/lib/systemd/network/80-vm-xx.* /etc/systemd/network/30-vm-xx.*

然后编辑/etc/systemd/network/30-xx-xx.network,修改Address=0.0.0.0/28Address=特定子网下的IP/28