nspawn.org:简单的 systemd 发行版容器
如果你想要运行一个发行版容器,而又不想被 docker 一类的重量级方案打扰,现在有一个新的简单方案了。
nspawn.org 目前提供了 Arch、CentOS、Debian、Fedora、Ubuntu 的各版本镜像,并可以直接用 systemd-nspawn 的验证机制进行签名验证。
推荐的用法是使用其提供的 “nspawn” 工具。下面以创建一个 Fedora 30 容器为例:
1、获取工具:
$ wget https://raw.githubusercontent.com/nspawn/nspawn/master/nspawn
$ chmod +x nspawn
2、获取 Fedora 30 镜像:
$ sudo ./nspawn init fedora/30/tar
3、启动容器并获取 shell:
$ sudo machinectl start fedora-30-tar
$ sudo machinectl shell fedora-30-tar
Connected to machine fedora-30-tar. Press ^] three times within 1s to exit session.
[root@fedora30 ~]#
一些背景:容器默认的存储路径在 /var/lib/machines/。nspawn.org 的创建者是 shibumi,目前是 Arch Linux Trusted User。所有的镜像使用 mkosi 制作,定义文件均在 GitHub 上。除了 nspawn 容器镜像,这个站点还提供可引导的 GPT-UEFI 镜像。
如果你想要运行一个发行版容器,而又不想被 docker 一类的重量级方案打扰,现在有一个新的简单方案了。
nspawn.org 目前提供了 Arch、CentOS、Debian、Fedora、Ubuntu 的各版本镜像,并可以直接用 systemd-nspawn 的验证机制进行签名验证。
推荐的用法是使用其提供的 “nspawn” 工具。下面以创建一个 Fedora 30 容器为例:
1、获取工具:
$ wget https://raw.githubusercontent.com/nspawn/nspawn/master/nspawn
$ chmod +x nspawn
2、获取 Fedora 30 镜像:
$ sudo ./nspawn init fedora/30/tar
3、启动容器并获取 shell:
$ sudo machinectl start fedora-30-tar
$ sudo machinectl shell fedora-30-tar
Connected to machine fedora-30-tar. Press ^] three times within 1s to exit session.
[root@fedora30 ~]#
一些背景:容器默认的存储路径在 /var/lib/machines/。nspawn.org 的创建者是 shibumi,目前是 Arch Linux Trusted User。所有的镜像使用 mkosi 制作,定义文件均在 GitHub 上。除了 nspawn 容器镜像,这个站点还提供可引导的 GPT-UEFI 镜像。
---------------------------------
systemd-nspawn 踩坑记
最近把自己的网络服务都迁移到了一台新的服务器,尝试了全新的部署方式(
systemd-nspawn
),正好踩中了一些坑,所以随便写写记录一下,也算是重新开始做起博客这件事情了吧。What & Why
以前我使用的是一台在
online.net
捡来的特价独服,因为只有一个人使用,所以我直接在主机上开了很多个 KVM 虚拟机,使用(几乎是)一个服务一个虚拟机的方式来部署自己的服务。这在一个人使用的时候确实没有什么太大的问题,唯一的问题可能就是因为自己懒,而虚拟机的数量太多,所以经常忘记更新 / 维护那些虚拟机。
而这次则是捡特价弄了一台特别划算的
E5-2680v2
的独服(购买的时候下单的是 E5-2660v1
,但是不知道是商家特别有钱还是那天机房小哥心情好,给弄了一台 E5-2680v2
),几个人合用一台。因为是合用,所以大家各自开了一个 KVM 虚拟机,各自隔离。这时候,我就不能再使用虚拟机的方式来隔离自己的服务了,因为我本身已经被隔离在了一个虚拟机里,双重虚拟机可从来都不是什么好主意。
所以我转向了容器方案。其中,卖底裤粉丝(雾),而且自带
Docker
不太适合我的情况 —— 我并不是希望把所有服务都做成不可变的镜像然后到处部署,我的目的仅仅是简单的隔离(看起来整洁 / 给有些傲娇的应用提供最适合的环境)。因此,我转向了 systemd-nspawn
,毕竟我是 Systemd
的Systemd
的 ArchLinux
在安装完成后就自带这个东西。
于是,我成功开始了踩坑之旅。(大量
dirty fix
预警)非特权用户 (Private Users)
按照 ArchWiki 上的说明,使用各个发行版的 bootstrap 工具在
/var/lib/machines
下创建目录并部署系统是一个很快的工作。然而,当我部署成功并尝试启动容器的时候,我却根本看不到任何反应,无论 machinectl status
还是 systemctl status
都没法给出任何有用的信息。
再次查看
/var/lib/machines
下我部署的目录的时候,发现里面文件的权限全部被修改成了奇怪的 UID 和 GID 值。从 ArchWiki 上的描述来看,这似乎是启用了 Private Users
的正常现象。然而,死马当活马医,我尝试在 /etc/systemd/nspawn/myContainer.nspawn
(myContainer 是我的容器的名字) 里面加入了[Exec]
PrivateUsers=no
然后容器就神奇般地可以启动了。不过这样启动以后,尝试访问容器的时候,会发现里面的程序一定会报一大堆权限问题 —— 因为之前已经用 Private User 起过容器。所以,我的做法是,直接重新部署一遍……
然而这个问题并没有被彻底解决,我到现在也不能理解为什么使用 Private User 会导致无法启动,而且
systemd-nspawn
完全没有给我任何有用的错误信息。更奇怪的是,我直接用命令行的 systemd-nspawn
去启动容器是完全正常的,而使用 systemd-nspawn@.service
就必须关掉 Private Users 才能正常使用。鉴于我的使用场景并不需要多么严格的安全策略(另一方面,Linux 下的容器这个概念本身也不是用来做安全的),我暂时并没有去处理这个问题。所以,这算是一个 dirty fix
吧。无法访问容器的 TTY
容器起来了,我遇到的第二个问题就是无法访问容器的 TTY。
尝试执行
machinectl login myContainer
, 直接给我扔了一个 protocol error
出来。在 Google 上找了很久也没有找到任何一个人遇到类似的问题。最多只有进入了容器的登录界面却无法登录的问题,而遇到那种问题的人至少已经获得了容器的一个 Login Shell, 而我则是什么都没有……
是的,这个问题我也完全不知道怎么解决。当时我折腾了很久,然后怀疑是一个临时性的 bug —— 毕竟 ArchLinux 喜欢 break 东西。所以我决定作为一个临时的解决方案,先手动使用
systemd-nspawn
命令启动容器进去,配置好 openssh
,然后用 machinectl
启动容器,并在外面直接使用 ssh
访问容器内部的 shell。是的,你没有看错,直接在 /var/lib/machines
使用 systemd-nspawn -b -D myContainer
命令启动容器是完全可以访问容器内的 shell 的,而从外面使用 machinectl login
或者 machinectl shell
就是不行的……
而当我下笔写这篇博客的时候,我尝试再次复现这个错误,却发现现在已经完全正常了,使用
machinectl login
可以获取到容器的正常的 Login Shell... 天知道把我折腾的要死要活的那个问题是怎么回事…… 而且从出现那个问题到现在我并没有更新服务器上的任何软件,也没有针对这个问题做任何处理…… 而当时我重启了不知道多少遍都完全没有作用。
假如你遇到这个问题,也许你坐和放宽一下,就好了。
容器内的内核模块问题 (OverlayFS / ip6tables / FUSE ...)
遇到这种问题其实觉得自己很智障,但是还是要花几句话说一次,容器内是没有办法加载内核模块的,而 Linux 启动的时候默认很多模块都没有加载。那些模块正常情况下会被使用它们的程序自动加载,但是在容器里这是不行的。所以如果你在使用程序的时候遇到这种问题,请记住在主机里加载它需要的模块后再试 (推荐加入
/etc/modules-load.d/
自动载入)Systemd-nspawn 内运行 Docker
这个需求看起来很无厘头,但是我觉得我的使用场景是有道理的。我有一个
Mastodon
节点,这个东西是主要使用 Ruby on Rails
编写但同时有很多其他依赖的东西。在以前的机器上,我是使用 docker-compose
通过容器的组合在一个虚拟机里直接部署上这一系列依赖。而现在我需要迁移到我的新独服上,我不能使用虚拟机,也不想自找麻烦手动部署,也不想让 Docker
产生的一大堆网络接口之类的东西挂在主机的 namespace 里。总而言之,因为这种 Ruby on Rails
程序是好多个大怪兽,虽然各自有笼子,但是因为数量比较多,分散放置还是感觉很凌乱,所以我想进一步把它们的笼子也都关进一个动物园里统一管理。
但是问题来了,
systemd-nspawn
似乎并不支持在它内部再启动 Docker
容器。尝试使用 Docker
容器会直接带来 Operation not permitted
异常。从 https://github.com/opencontainers/runc/issues/1456 了解到,Docker
依赖了 cgroups
功能,并且需要比较高的权限,而在默认情况下,systemd-nspawn
是隔离了 cgroup
命名空间的,而且也没有给予不必要的权限。所以,作为测试,我在 /etc/systemd/nspawn/myContainer.nspawn
里加入了[Exec]
Capability=all
[Files]
Bind=/sys/fs/cgroup
这在事实上把主机的
cgroup
命名空间共享给了容器里的系统,并给予了所有可以给予的 Capabilities
。同时,还需要关闭 systemd-nspawn
的 cgroup
隔离功能,只需要 systemctl edit systemd-nspawn@myContainer
[Service]
Environment=SYSTEMD_NSPAWN_USE_CGNS=0
到了这一步,我期望
Docker
已经可以使用了,但是很不幸的是,并不能。这回出现的是一个莫名奇妙的 session key
无法创建的异常。这次这个异常我就完全没有看懂了……
还好,经过一番 Google,我了解到这其实是因为
Docker
在尝试使用 kernel keyring
,而这个功能是不支持(ref: https://github.com/moby/moby/issues/10939)命名空间隔离的。所以,为了安全,systemd-nspawn
默认把与此相关的系统调用都过滤掉了,不允许内部的系统调用。因此,只需要开启这两个系统调用的权限(在 /etc/systemd/nspawn/myContainer.nspawn
的 [Exec]
段中加入)SystemCallFilter=add_key keyctl
然后重启
nspawn
容器即可使用 Docker
。
在
Docker
正常运行之后,我发现一个问题,那就是它在使用非常慢、非常不科学的 vfs
作为存储后端。根据文档,这个存储后端会对每个 layer 都创建一个拷贝。于是我想起来了遇到的上一个问题 —— 主机没有加载 OverlayFS 的内核模块,因此默认的 overlay2
存储后端加载失败了。尝试在主机上加载 overlay
模块,然后重新启动容器里的 Docker
,发现 overlay2
存储后端果然已经在正确运行了。
以上文档我已经写入
ArchWiki
上的对应章节, 因为我发现我在整个网络上都找不到关于这件事情的文档,有的只是一段 Twitter 对话,而且他们其实还并没有解决这个问题……希望我并不是唯一一个有这种奇葩需求的人吧。
当然,这么做以后,这个
nspawn
容器就成为了名副其实的特权容器,拥有很多很多高权限操作的能力。考虑到我的本意仅仅是出于洁癖一般的理由,这个问题我觉得并不是非常大……总之,给大家一个参考。容器内使用 FUSE
FUSE
是指用户态文件系统,比如 sshfs
, ntfs-3g
等。想要直接在 systemd-nspawn
容器里使用它们是会直接失败的。当然,这个解决办法很简单,因为这仅仅是因为容器里没有 /dev/fuse
。
首先要确保主机上加载了
fuse
内核模块。然后,你需要在 /etc/systemd/nspawn/myContainer.nspawn
加入[Exec]
Capability=CAP_MKNOD
DeviceAllow=/dev/fuse rwm
(注:如果你前面已经
Capability
设为 all
了,那就不用再单独设置一次 CAP_MKNOD
了)
然后在容器里执行
mknod /dev/fuse c 10 229
即可。
其他:网络配置
网络配置这算是一点附加说明,就是如果你想要给容器使用静态 IP,或者你想给容器使用 IPv6,你需要首先在
/etc/systemd/nspawn/myContainer.nspawn
里给容器增加一个网络接口[Network]
VirtualEthernetExtra=name_on_host:name_in_container
然后分别在主机和容器里配置对应的网络接口即可。
当然,你可能也需要
[Network]
Private=true
VirtualEthernet=true
虽然这些应该是默认的。
结论
Systemd
坑很多,而且很玄学.
No comments:
Post a Comment