systemd的容器与VM组件
systemd-container是systemd的容器组件。它和LXC对标,比chroot更强大。它虚拟化了文件系统、进程树以及客户系统中的进程间通信。
systemd-container容器有五个原则性限制:
- 仅允许以只读方式访问例如
/sys,/proc/sys,/sys/fs/selinux这样的内核文件系统。 - 禁止修改主机的网络接口以及系统时钟。
- 禁止创建设备节点。
- 禁止重启主机操作系统。
- 禁止加载内核模块。
它的吸引力在于由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 | [Network] |
以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命令进入容器,此时可以使用apt,useradd等命令。
exit退出容器后,使用systemd-nspawn -bD /path-to-ubuntu启动进入容器系统,此时可以和真实系统一样进行操作。除了正常退出容器,还可以使用Ctrl+三下]操作强制退出。
安装
1 | # Debian |
创建容器/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 | -i|--init 拉取镜像 |
具体可拉取的镜像可以看 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.conf,off表示不进行操作。对应的.nspawn配置项为ResolvConf=。--timezone=off|auto:对容器内/etc/timezone的处理。auto表示拷贝宿主机的/etc/timezone,off表示不进行操作。对应的.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 | systemd-vmspawn -i Raw镜像路径 \ |
使用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/*sh且Boot=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 | pts/0 |
在容器/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 | systemd-run \ |
容器注册表必须支持标准OCI协议(application/vnd.oci.image.manifest.v1+json)而不能只支持Docker私有协议(application/vnd.docker.distribution.manifest.v2+json),你可以这样测试:
1 | curl -sS -H \ |
配置
文件路径
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-networkd,NetworkManager或ifupdown),但一般情况下即使启动了,网络配置服务也能分辨出这种情况,不会报错。
在使用主机网络时,容器根文件系统中默认的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-networkd,NetworkManager或ifupdown)和宿主环境上的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 host0udhcpc -i host0 -n -q -fdhcpcd -1 -w host0
- 在宿主环境中,启动
systemd-network.socket套接字。
否则容器中将没有网络连接。
仅本地模式
容器仅仅只有本地回环网络,不存在其他任何网络接口。
没什么意义的网络模式,在使用systemd-nspawn启动容器时,添加选项--private-network即可。
如果使用machinectl,创建容器配置容器镜像名.nspawn,添加以下内容:
1 | [Network] |
Veth(NAT)模式
给容器创建一个Veth设备,用于和主机之间通信,容器直接通过宿主机的路由和NAT对外部网络进行通信。
在使用systemd-nspawn启动容器时,添加选项-n|--network-veth即可。
通过systemd-nspawn@.service实例化而来的容器默认启用该模式,其对应的容器配置是:
1 | [Network] |
VirtualEthernet=yes隐式地包含了Private=yes配置项,不过,如果创建容器配置容器镜像名.nspawn,添加以下内容:
1 | [Network] |
那么便会覆盖默认配置,启用主机网络。
容器内的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服务,可以这样做:
- 手动在主机上配置DHCP服务端。
- 在容器中配置静态IP或是DHCP客户端。
- 配置类似
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 | [Network] |
NAT实际上是通过/usr/lib/systemd/network/80-container-vz.network中的IPMasquerade=both选项配置由systemd-networkd.service自动完成的。
端口映射
如果为容器开启了私有网络,那么可以在使用systemd-nspawn启动容器时,添加选项-p|--port=[协议:]宿主端口[:容器端口]进行端口映射(默认协议为tcp,默认容器端口与宿主一致)。
如果使用machinectl,创建容器配置容器镜像名.nspawn,添加以下内容:
1 | [Network] |
需要注意:
- 这个映射是通过宿主环境的
systemd-networkd.service监听内核NETLINK_ROUTE事件触发的,无论容器中的网络配置是如何进行的(任意一种网络配置服务或是手动配置)。 - 这个映射是通过内核Netfilter实现的(不是Docker那种Proxy),不会在宿主环境下占用Socket,你可以通过
nft list ruleset检查io.systemd.nat链确认规则。 - 这个映射的目标地址不接受
127.0.0.0/8,请求127.0.0.1会导致访问拒绝。
桥接模式
将容器对应的主机端Veth直接接入到指定的网桥上,网桥应当是一个Host-shared网桥。
在使用systemd-nspawn启动容器时,添加选项--network-bridge=网桥即可。
如果使用machinectl,创建容器配置容器镜像名.nspawn,添加以下内容:
1 | [Network] |
容器内的Veth设备名为host0@ifX,而宿主机侧的Veth设备名为vb-容器名@ifX。
专用网卡
直接将主机网卡分配给容器,直到容器停止前,主机都不能使用该网卡。
在使用systemd-nspawn启动容器时,添加选项--network-interface=设备名即可。
如果使用machinectl,创建容器配置容器镜像名.nspawn,添加以下内容:
1 | [Network] |
IPVLAN/MACVLAN
systemd-nspawn容器还可以直接在主机的网络接口上创建IPVLAN/MACVLAN,只需要添加选项--network-ipvlan=网络接口或--network-macvlan=网络接口即可。
如果使用machinectl,创建容器配置容器镜像名.nspawn,添加以下内容:
1 | [Network] |
IPVLAN设备在容器中被命名为iv-主机接口@ifX,MACVLAN设备在容器中被命名为mv-主机接口@ifX。需要注意的是,NetworkManager没有办法配置容器内的MACVLAN/IPVLAN网络接口(NetworkManager配置MACVLAN需要父接口位于同一命名空间),因此在容器中必须对其进行排除,具体方法为编辑/etc/NetworkManager/NetworkManager.conf,添加以下内容:
1 | [main] |
然后,在容器中使用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 | [Files] |
设备节点映射
对于设备节点,除了设置Bind=/dev/设备外,还需要编辑对应的systemd-nspawn@容器名.service,添加:
1 | [Service] |
资源限制
使用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 | [Service] |
示例
一个没有任何权限限制的超级容器:
1 | [Exec] |
固定容器/VM的子网
同一子网下的容器/VM才可以互相通信。默认情况下,容器/VM的网络会随机分配。
拷贝容器/VM的网络配置,调高优先级:
1 | cp /usr/lib/systemd/network/80-container-xx.* /etc/systemd/network/30-container-xx.* |
然后编辑/etc/systemd/network/30-xx-xx.network,修改Address=0.0.0.0/28为Address=特定子网下的IP/28。