无根 Podman 中的无根 Systemd

作者:Chris Evich

假设您正在测试一个需要与无根 Podman 套接字交互的服务。该服务可能对容器、卷或镜像具有破坏性,因此您不想冒主机安装损坏的风险。如何在无根容器内完成这一切?答案有点复杂,但在这篇文章中,我将尝试逐步引导您解决每个挑战。

Podman 内部的嵌套 Podman 并不是什么新鲜事。在主机端,我们必须运行 --privileged 并从主机提供容器存储(-v container-storage:/home/<name>/.local/share/containers:Z)。这是为了避免出现糟糕的层叠覆盖(overlay-in-overlay)情况。无论如何,这相当简单且不足为奇,如下面的 Containerfile 所示

FROM registry.fedoraproject.org/fedora:latest
RUN dnf upgrade -y && \
    dnf install -y podman && \
    rpm --setcaps shadow-utils 2>/dev/null && \
    dnf clean all
RUN groupadd -g 1000 fred && \
    useradd -u 1000 -g 1000 fred && \
    echo -e "fred:1:999\nfred:1001:64535" | \
        tee /etc/subuid > /etc/subgid
VOLUME /home/fred/.local/share/containers

基础镜像包的升级和安装应该不足为奇。shadow-utils 上的 --setcaps 是 Fedora 特有的一个长期存在的 bug 的解决方法。接下来,我们添加用户并设置嵌套的 subuid/subgid 映射。这是必需的,这样嵌套容器就不会占用与命名空间 UID 0(root 用户)和 UID 1000(Fred)重叠的 ID。如果发生这种情况,嵌套容器中的用户可能会对外部容器的文件/目录拥有意外的访问权限。因此,这种 subuid/gid 设置非常重要。

由于 Fred 是我们唯一的嵌套用户和组命名空间,我们可以简单地覆盖 /etc/sub{uid,gid} 文件中的默认内容。如果存在其他具有已知 ID 的用户,也应该将它们从 Fred 的命名空间范围中排除,以避免意外冲突。最后,该镜像请求主机上的一个卷来提供上述容器存储。

在此容器镜像内部运行嵌套容器很简单,不足为奇

$ podman build -t piptest .
...cut...
$ podman run -it --rm --privileged --user fred --hostname outer piptest
[fred@outer /]$ podman run -it --rm --privileged --hostname inner fedora:latest
...cut...
Writing manifest to image destination
Storing signatures
[root@inner /]# echo "no magic here"
no magic here
[root@inner /]# exit
exit
[fred@outer /]$ exit
exit

接下来是棘手的部分。由于 Podman 没有守护进程,因此需要一个正在运行的 Podman 进程来处理 API 套接字请求。虽然我们可以运行 podman system service -t0 作为容器的命令,但这不允许我们同时从应用程序中使用套接字。对于容器,任何时候您考虑需要多个高级进程时,除非它们完全微不足道(这些不是),否则您会需要 Systemd 这样的初始化系统。

在容器内运行 Systemd 会使事情复杂化——主要是由于额外的服务及其非平凡的配置。然而,在此用例中,它是必需的,因为我们希望容器执行多项任务。具体来说,Systemd 需要正确处理(可能)大量 Podman 子进程的出现和消失,以及其子树的信号处理。我甚至还没有提到将使用 Podman 套接字的应用程序,它也必然需要 Systemd 处理。

换句话说,如果没有 PID 1 进程管理器,我们的容器将很快变成一个不协调的混乱局面,不断地考验自身的崩溃边缘。

Leaning tower of Jenga
摇摇欲坠的叠叠乐(Jenga)

回到 Containerfile,更新 dnf install 行以包含 systemd 很容易。尽管需要一些额外的魔术才能使 Podman 套接字服务生效,并在容器启动时保持操作可观察。假设您的主机是基于 systemd 的,并且已安装 Podman,Podman systemd 文件可以简单地复制到容器构建上下文中,例如

$ cp /lib/systemd/system/podman.s* ./

首先需要更改的是对 podman.service 文件的小幅更新——使其以警告级别记录到控制台(Fedora 上的默认级别是 info 级别)

[Unit]
Description=Podman API Service
Requires=podman.socket
After=podman.socket
Documentation=man:podman-system-service(1)
StartLimitIntervalSec=0

[Service]
Delegate=true
Type=exec
KillMode=process

Environment=LOGGING="--log-level=warn"
ExecStart=/usr/bin/podman $LOGGING system service
StandardOutput=journal+console
StandardError=inherit

[Install]
WantedBy=default.target

其次,让 systemd 管理 Fred 主目录中监听的 podman.sock 文件,这样更容易交互,并节省一些输入

[Unit]
Description=Podman API Socket
Documentation=man:podman-system-service(1)

[Socket]
ListenStream=%h/podman.sock
SocketMode=0660

[Install]
WantedBy=sockets.target

套接字、服务的安装以及 Fred 的 systemd-slice 设置都在 Containerfile 中进行,到目前为止,它看起来像这样

FROM registry.fedoraproject.org/fedora:latest
RUN dnf upgrade -y && \
    dnf install -y podman systemd && \
    rpm --setcaps shadow-utils 2>/dev/null && \
    dnf clean all
RUN useradd -u 1000 fred && \
    echo -e "fred:1:999\npodman:1001:64535" | tee /etc/subuid > /etc/subgid
VOLUME /home/fred/.local/share/containers

ADD /podman.service /podman.socket /home/fred/.config/systemd/user/
RUN cd /home/fred/.config/systemd/user/ && \
    mkdir sockets.target.wants && \
    ln -s ../podman.socket ./sockets.target.wants/ && \
    mkdir -p /var/lib/systemd/linger && \
    touch /var/lib/systemd/linger/fred && \
    chown -R 1000:1000 /home/fred
ENTRYPOINT /sbin/init

让用户切片服务随容器启动的关键是创建文件 /var/lib/systemd/linger/fred,这等同于运行:loginctl enable-linger fred。然而,在容器镜像构建环境中,systemd 和 dbus 都不可用,因此只是简单地创建了该文件。


最后的 Containerfile 步骤修复了 Fred 的文件所有权,并指示容器应将 init (systemd) 作为 PID 1 启动。然而,由于容器现在将作为(命名空间)root 用户启动,我们需要预先创建容器存储卷,如下所示;否则,所有权将不正确——无疑会产生大量“权限拒绝”错误。

$ podman volume create -o o=uid=1000,gid=1000 freds-containers

如果您想查看或操作该卷的内容,请务必在命令前加上 podman unshare 以进入您的用户命名空间。否则,就是这样,所有技术细节都已揭示,魔术也已公开。剩下要做的就是启动容器并证明嵌套的、远程的、无根的 Podman 连接是功能性的

$ podman build -t piptest .
...cut...
$ podman run -dt --rm --privileged --hostname outer \
    -v freds-containers:/home/fred/.local/share/containers \
    --systemd true piptest
0af2a31de3467d0ddb2b540072c0864f7738d5de5745aa5b3596d70f4a5f7a04
$ podman exec -itl bash
[root@outer /]# ls -la /home/fred/podman.sock
srw-rw----. 1 fred fred 0 Feb  9 17:06 /home/fred/podman.sock
[root@outer /]# export CONTAINER_HOST=unix:///home/fred/podman.sock
[root@outer /]# podman --remote info --format={{.Store.GraphRoot}}
/home/fred/.local/share/containers/storage
[root@outer /]# podman --remote run -it --rm --hostname inner fedora:latest
...cut...
Writing manifest to image destination
Storing signatures
[root@inner /]# echo "Hello from $HOSTNAME"
Hello from inner
[root@inner /]# exit
exit
[fred@outer /]$ exit
exit
$ podman stop -l
0af2a31de3467d0ddb2b540072c0864f7738d5de5745aa5b3596d70f4a5f7a04

在这里,您可以看到容器已构建,然后以 --privileged 模式运行(嵌套的无根容器必需),并带有预先创建的容器存储卷。然后作为命名空间中的 root 用户执行进入,在那里我们连接到 Fred 的 Podman 套接字并打印出容器存储位置。尽管使用 root 只是为了方便,但您可以作为任何配置/命名空间用户连接到套接字,并具有适当的权限。如上所示,位置和权限可以在 podman.socket 文件中配置。

Robotic Jenga Stacking
机器人叠叠乐堆叠

此时,任何连接到 Podman 套接字的服务开发都是可能的,并且将与主机的 Podman 设置隔离。添加更多用户、systemd-slice 并使其持久化应该相当简单。不过,您需要记住将其他命名空间的 UID/GID 从嵌套的 /etc/sub{uid,gid} 中排除,以防止冲突。

尽管需要在 --privileged 模式下运行顶级容器,但容器化的 systemd-slice 提供了一定程度的额外隔离。这种设置当然不如没有 systemd 或额外权限那么安全。然而,privileged 选项远不如容器以 root 身份运行那么糟糕。在这种情况下,由于无根用户命名空间,额外启用的功能影响有限。因此,对于测试、开发或非关键目的,这是一个完全可以接受的安排。

总的来说,这些容器成功地将其软件、依赖项和运行时环境与主机操作系统隔离。此外,由于它们已经启用了 systemd,因此应该很容易添加额外的应用程序和服务文件——将它们指向嵌套的 Podman 套接字位置。然而,由于本文已经有点长,这些都留给读者作为练习。

发表评论

订阅

输入您的电子邮件地址以接收来自本网站的电子邮件更新。

返回

您的消息已发送

警告
警告
警告。

分类


搜索