Podman-实战-全-
Podman 实战(全)
原文:Podman in Action
译者:飞龙
前置材料
前言
我从事计算机安全工作近 40 年,过去 20 年专注于容器技术。大约 10 年前,当 Docker 出现时,它引发了人们在网上分发和运行应用程序方式的革命。在我为 Docker 工作时,我觉得它本可以设计得更好。与运行在 root 权限下的守护进程一起工作,然后添加越来越多的守护进程,感觉这是一种错误的方法。相反,我觉得我们可以使用低级操作系统概念来创建一个工具,以相同的方式运行相同的容器化应用程序,但具有更高的安全性和更少的权限。带着这个想法,我在红帽公司的团队着手构建一系列工具,以帮助开发人员和管理员以最安全的方式运行容器。Podman 就是从这个努力中诞生的。
我在 2000 年代初开始写关于 SELinux 等主题的博客,并且从那时起一直在写文章。多年来,我写了数百篇关于容器和安全的文章,但我希望将想法整合并描述 Podman 技术,以便我可以向用户和客户推荐一本书。
本书介绍了 Podman 及其使用方法。它还深入探讨了我们所利用的 Linux 操作系统的技术和不同部分。由于我是一个安全工程师,我还用几章的篇幅描述了容器安全的工作原理。阅读这本书应该能让你更好地理解容器是什么,它们是如何工作的,以及如何使用 Podman 的不同功能。你甚至还会学到更多关于 Docker 的知识。随着 Podman 越来越受欢迎并渗透到你的基础设施中,这本书将成为一个实用的参考,引导你的道路。
致谢
我感谢所有帮助我写这本书的人。这包括 Podman 团队成员,他们写的文章帮助我理解了一些我不完全理解的技术,并帮助构建了一个伟大的产品。感谢 Brent Baude、Matt Heon、Valentin Rothberg、Giuseppe Scrivano、Urvashi Mohnani、Nalin Dahyabhai、Lokesh Mandvekar、Miloslav Trmac、Jason Greene、Jhon Honce、Scott McCarty、Tom Sweeney、Ashley Cui、Ed Santiago、Chris Evich、Aditya Rajan、Paul Holzinger、Preethi Thomas 和 Charlie Doern。我还想感谢那些使 Linux 容器和 Podman 成为可能的无数开源贡献者。
我感谢 Manning 出版社的整个团队,但特别感谢 Toni Arritola。Toni 教会了我如何更好地集中思想,并在这次旅程中成为了一位伟大的合作伙伴。她不得不应对我这样一个老数学专业毕业生,我从未擅长写作,但她帮助使这本书成为可能。
向所有审稿人——Alain Lompo、Alessandro Campeis、Allan Makura、Amanda Debler、Anders Björklund、Andrea Monacchi、Camal Cakar、Clifford Thurber、Conor Redmond、David Paccoud、Deepak Sharma、Federico Kircheis、Frans Oilinki、Gowtham Sadasivam、Ibrahim Akkulak、James Liu、James Nyika、Jeremy Chen、Kent Spillner、Kevin Etienne、Kirill Shirinkin、Kosmas Chatzimichalis、Krzysztof Kamyczek、Larry Cai、Michael Bright、Mladen Knežić、Oliver Korten、Richard Meinsen、Roman Zhuzha、Rui Liu、Satadru Roy、Seung-jin Kim、Simeon Leyzerzon、Simone Sguazza、Syed Ahmed、Thomas Peklak 和 Vivek Veerappan——表示感谢,你们的建议帮助使这本书变得更好。
关于本书
《Podman 实战》描述了用户如何构建、管理和运行容器。我写这本书的目标是解释如何轻松地将你在 Docker 中学到的技能转移到 Podman 上,以及如果你之前从未使用过容器引擎,使用 Podman 是多么容易。此外,《Podman 实战》还教你如何使用高级功能,如 pods,并指导你走向构建可在 Kubernetes 边缘或内部运行的应用程序的道路。最后,《Podman 实战》解释了 Linux 内核中用于隔离容器与系统以及与其他容器之间的所有安全功能。
谁应该阅读这本书?
《Podman 实战》是为那些希望理解、开发和与容器一起工作的软件开发者以及需要在生产环境中运行容器的系统管理员所写的。阅读这本书将使你对容器是什么有更深入的了解。了解 Linux 进程以及熟悉使用 Linux shell 是充分利用本书的必要条件。
本书应该为每个人在他们的容器使用之旅中提供一些内容。对 Docker 有深入了解的用户将了解 Podman 的 Docker 中不可用的高级功能,并将对 Docker 的工作原理有更深入的理解。新手用户将学习容器和 pods 的基础知识。
本书如何组织:路线图
《Podman 实战》分为四部分和六个附录:
-
第一部分,“基础”,包含四章,为读者提供了 Podman 的介绍。第一章解释了 Podman 的功能、为什么它被创建以及为什么它很重要。接下来的两章介绍了命令行界面以及如何在容器中使用卷。最后,第四章介绍了 pods 的概念以及 Podman 如何与它们一起工作。这些章节中应该有适合每个人的内容,但如果你有丰富的 Docker 经验,你应该能够快速浏览第二章的大部分内容。
-
第二部分,“设计”,包含两个章节,其中我深入探讨了 Podman 的设计。您将了解无根容器及其工作原理,并在这些章节结束时对用户命名空间和无根容器的安全性有更深入的理解。您还将学习如何自定义 Podman 环境的配置。
-
第三部分,“高级主题”,包含三个章节,并超越了 Podman 的基础知识。在第七章中,您将了解 Podman 如何通过与其与 systemd 的集成在生产环境中工作。它涵盖了在容器内运行 systemd 以及如何将其用作容器管理器。您将学习如何使用 Podman 容器设置边缘服务器,其中 systemd 管理容器的生命周期。Podman 使您能够轻松生成 systemd 单元文件,以帮助您将容器化应用程序投入生产。在第八章中,您将了解如何使用 Podman 将容器移动到 Kubernetes。Podman 支持使用与 Kubernetes 相同的 YAML 文件启动容器,以及从当前容器生成 Kubernetes YAML 的能力。在第九章中,您将看到 Podman 作为服务运行,允许远程访问 Podman 容器。将 Podman 作为服务使用允许您使用其他编程语言和工具来管理 Podman 容器。您将了解
docker-compose如何与 Podman 容器协同工作。您还将学习如何使用 Python 库(如 podman-py 和 docker-py)与 Podman 服务通信以管理容器。 -
第四部分,“容器安全”,包含两个章节,其中我讨论了重要的安全考虑因素。第十章涵盖了用于确保容器隔离的功能。本章涵盖了 Linux 的安全子系统,如 SELinux、seccomp、Linux 能力、内核文件系统和命名空间。第十一章随后检查了我在尽可能安全地运行容器时认为的最佳实践。
此外,还有六个附录涵盖了与 Podman 相关的主题:
-
附录 A 涵盖了所有与 Podman 相关的工具,包括 Buildah、Skopeo 和 CRI-O。
-
附录 B 深入探讨了 Podman 以及 Docker 可用的不同 OCI 运行时,包括
runc、crun、Kata 和 gVisor。 -
附录 C 描述了如何将 Podman 安装到您的本地系统,无论该系统是 Linux、Mac 还是 Windows。
-
附录 D 描述了 Podman 开源社区以及如何加入。
-
附录 E 和 F 深入探讨了在 Mac 和 Windows 系统上运行 Podman。
liveBook 讨论论坛
每购买一本《Podman in Action》都包括免费访问 liveBook,曼宁的在线阅读平台。使用 liveBook 的独特讨论功能,您可以在全球范围内或特定章节或段落中附加评论。为自己做笔记、提问和回答技术问题,以及从作者和其他用户那里获得帮助都非常简单。要访问论坛,请访问livebook.manning.com/book/podman-in-action/discussion。您还可以在livebook.manning.com/discussion了解更多关于曼宁论坛和行为准则的信息。
曼宁对我们读者的承诺是提供一个平台,在这里个人读者之间以及读者与作者之间可以进行有意义的对话。这并不是对作者参与特定数量活动的承诺,作者对论坛的贡献仍然是自愿的(且未付费)。我们建议您尝试向他提出一些挑战性的问题,以免他的兴趣转移!只要这本书还在印刷中,论坛和先前讨论的存档将可通过出版社的网站访问。
作者在线
您可以在 Twitter 和 GitHub 上关注丹尼尔·沃尔什 @rhatdan。他经常在www.redhat.com/sysadmin/users/dwalsh以及几个其他网站上写博客。YouTube 上有许多丹尼尔·沃尔什发表的演讲视频。
关于作者

丹尼尔·沃尔什领导了创建 Podman、Buildah、Skopeo、CRI-O 及其相关工具的团队。丹自 2001 年 8 月加入红帽公司以来,担任高级杰出工程师。他在计算机安全领域工作了 40 多年。丹在红帽公司领导容器团队之前,曾领导开发 SELinux,因此有时被称为 SELinux 先生。丹在圣十字学院获得数学学士学位,在伍斯特理工学院获得计算机科学硕士学位。您可以在 Twitter 和 GitHub 上找到他 @rhatdan。您可以通过 dwalsh@redhat.com 给他发邮件。
关于封面插图
《Podman in Action》封面上的插图标题为“La vandale”,或“破坏者”,取自雅克·格拉塞·德·圣索沃尔于 1797 年出版的作品集。每一幅插图都是手工精心绘制和着色的。
在那个时代,人们通过他们的服饰就能轻易地识别出他们的居住地以及他们的职业或社会地位。曼宁通过基于几个世纪前丰富多样的地域文化的书封面来庆祝计算机行业的创新精神和主动性,这些文化通过如这一系列图片般的作品得以重现。
第一部分:基础
在本书的第一部分,我向您介绍了您可以从命令行使用 Podman 的几种方法。在第二章中,您将学习如何创建和使用容器,以及容器如何与镜像协同工作。您还将了解容器和镜像之间的区别,如何将容器保存为镜像,然后将镜像推送到注册表,以便与其他用户共享。
在第三章中,我介绍了 卷 的概念。卷是您的容器化应用程序用户用来存储数据和将其与应用程序隔离的主要机制。前两章主要集中讨论了容器和镜像的使用,这与 Docker 中的容器工作方式非常相似。
第四章增加了 Pod 的概念,类似于 Kubernetes 的 Pod,这是 Docker 不支持的功能。Pod 允许您在同一个资源、命名空间和安全约束内共享一个或多个容器。Pod 可以让您编写更复杂的应用程序,并将它们作为一个单一实体来管理。
1 Podman:下一代容器引擎
本章涵盖
-
Podman 是什么
-
使用 Podman 而不是 Docker 的优势
-
Podman 的使用示例
开始这本书是困难的,因为许多人带着不同的期望和经验来到这里。您可能对容器、Docker 或 Kubernetes 有一些经验,或者至少是因为您听说过 Podman 而对学习 Podman 感兴趣。如果您使用过或评估过 Docker,您会发现 Podman 在大多数情况下与 Docker 的工作方式相同,但它解决了 Docker 中固有的某些问题;最显著的是,Podman 提供了增强的安全性和使用非 root 权限运行命令的能力。这意味着您可以使用 Podman 来管理容器,而无需 root 访问或权限。由于 Podman 的设计,它默认情况下可以比 Docker 运行得更加安全。
除了是开源的(因此是免费的)之外,Podman 的命令,从命令行界面(CLI)运行,与 Docker 的命令非常相似。这本书展示了您如何使用 Podman 作为本地容器引擎在单个节点上启动容器,无论是本地还是通过远程 REST API。您还将学习如何使用 Podman 以及 Buildah 和 Skopeo 等开源工具来查找、运行和构建容器。
1.1 关于所有这些术语
在您继续之前,我认为定义本书中将使用的术语非常重要。在容器世界中,诸如 容器编排器、容器引擎 和 容器运行时 这样的术语经常被互换使用,这通常会导致混淆。以下列表是每个术语在本文中的含义的总结:
-
容器编排器——将容器编排到多个不同机器或节点上的软件项目和产品。这些编排器与容器引擎通信以运行容器。主要的容器编排器是 Kubernetes,它最初是为了与 Docker 守护进程容器引擎通信而设计的,但使用 Docker 正在变得过时,因为 Kubernetes 主要使用 CRI-O 或 containerd 作为其容器引擎。CRI-O 和 containerd 是专门为运行编排的 Kubernetes 容器而构建的(CRI-O 在附录 A 中介绍)。Docker Swarm 和 Apache Mesos 是其他容器编排器的例子。
-
容器引擎——主要用于配置容器化应用程序在单个本地节点上运行。它们可以直接由用户、管理员和开发者启动。它们也可以在启动时从 systemd 单元文件中启动,以及由容器编排器如 Kubernetes 启动。如前所述,CRI-O 和 containerd 是 Kubernetes 用于本地管理容器的容器引擎。它们实际上并不打算直接由用户使用。Docker 和 Podman 是用户用于在单个机器上开发、管理和运行容器化应用程序的主要容器引擎。Podman 很少用于启动 Kubernetes 的容器;因此,本书通常不涉及 Kubernetes。Buildah 是另一个容器引擎,尽管它仅用于构建容器镜像。
-
开放容器倡议(OCI)容器运行时——配置 Linux 内核的不同部分,然后最终启动容器化应用程序。最常用的两个容器运行时是
runc和crun。Kata 和 gVisor 是其他容器运行时的例子。请参阅附录 B 了解 OCI 容器运行时的区别。
图 1.1 展示了这些开源容器项目属于哪些类别。

图 1.1 不同开源项目在编排器、引擎和运行时类别中处理容器。
Podman 是 Pod Manager 的简称。Pod,一个由 Kubernetes 项目普及的概念,是指一个或多个共享相同命名空间和 cgroups(资源限制)的容器。Pods 在第四章中有更深入的介绍。Podman 可以运行单个容器以及 pods。图 1.2 中的 Podman 标志是一群塞尔基(Selkies),这是爱尔兰关于美人鱼的概念。塞尔基群被称为 pods。

图 1.2 Podman 的标志
Podman 项目将 Podman 描述为“一个无守护进程的容器引擎,用于在您的 Linux 系统上开发、管理和运行 OCI 容器。容器可以以 root 用户身份运行或在无 root 模式下运行” (podman.io)。Podman 常常被简单地概括为 alias Docker = Podman,因为 Podman 几乎可以执行 Docker 可以用相同命令行完成的几乎所有操作。但正如您将在本书中学到的,Podman 可以做更多的事情。理解 Docker 对于理解 Podman 不是必需的,但却是很有帮助的。
注意:开放容器倡议(Open Container Initiative,OCI)是一个标准机构,其主要目标是创建关于容器格式和运行时的开放行业标准。更多信息,请参阅 opencontainers.org。
Podman 上游项目位于 github.com 的容器项目中,如图 1.3 所示,(github.com/containers/podman),与其他容器库和容器管理工具如 Buildah 和 Skopeo 一起。 (有关这些工具的描述,请参阅附录 A。)

图 1.3 容器是 Podman 和其他相关容器工具的开发者网站(见github.com/containers)。
Podman 使用新的 OCI 格式运行镜像,如第 1.1.2 节所述,以及传统的 Docker(v2 和 v1)格式镜像。Podman 可以在容器注册库中运行任何可用的镜像,如 docker.io 和 quay.io,以及数百个其他容器注册库。Podman 将这些镜像拉取到 Linux 主机上,并以与 Docker 和 Kubernetes 相同的方式启动它们。Podman 支持所有 OCI 运行时,包括runc、crun、kata和gvisord(附录 B),就像 Docker 一样。
本书旨在帮助 Linux 管理员了解使用 Podman 作为其主要容器引擎的优势。您将学习如何以尽可能安全的方式配置您的系统,同时仍然允许您的用户与容器一起工作。Podman 的主要用例之一是在单节点环境中运行容器化应用程序,例如边缘设备。Podman 和 systemd 允许您在不进行人工干预的情况下管理节点上应用程序的整个生命周期。Podman 的目标是在 Linux 盒子上自然运行容器,利用 Linux 平台的所有功能。
注意:Podman 适用于许多不同的 Linux 发行版,以及 Mac 和 Windows 平台。请参阅附录 C 了解如何在您的平台上获取 Podman。
应用程序开发人员也是本书的目标读者。Podman 是开发者寻求以安全方式容器化其应用程序的绝佳工具。Podman 允许开发者在所有 Linux 发行版上创建 Linux 容器。此外,Podman 还可在 Mac 和 Windows 平台上使用,它可以在 VM 内或网络上可用的 Linux 盒子上与运行的 Podman 服务进行通信。“Podman in Action”向您展示如何与容器一起工作,构建容器镜像,然后将它们的容器化应用程序转换为在边缘设备上运行的单一节点服务或基于 Kubernetes 的微服务。
Podman 和容器工具是开源项目,来自许多不同的公司、大学和组织。贡献者来自世界各地。项目始终在寻找新的贡献者以改进它们;请参阅附录 D 了解您如何加入这一努力。在本章中,我首先简要概述容器,然后解释一些使 Podman 成为处理容器优秀工具的关键特性。
1.2 容器简要概述
容器是运行在 Linux 系统上的进程组,彼此之间是隔离的。容器确保一个进程组不会干扰系统上的其他进程。恶意进程无法控制系统资源,这可能会阻止其他进程执行其任务。容器的最终目标之一是允许应用程序安装具有自己版本的共享库,这些库不会与需要不同版本相同库的应用程序冲突。相反,它们允许应用程序生活在虚拟化环境中,给人一种它们拥有整个系统的印象。
容器通过以下方式隔离:
-
资源约束(cgroups**)—cgroup 手册页(
man7.org/linux/man-pages/man7/cgroups.7.xhtml)将 cgroups 定义为以下内容:“控制组,通常称为 cgroups,是 Linux 内核的一个特性,它允许将进程组织成层次结构分组,然后可以限制和监控这些分组对各种类型资源的使用。”cgroups 控制的资源示例包括以下内容:
-
一组进程可以使用的内存量
-
进程可以使用的 CPU 量
-
进程可以使用的网络资源量
cgroups 的基本思想是防止一个进程组以某种方式控制某些系统资源,以至于另一个进程组无法在系统上取得进展。
-
-
安全约束—容器使用内核中可用的许多安全工具彼此隔离。目标是阻止权限提升,防止恶意进程组对系统进行敌对行为,以下是一些示例:
-
丢弃 Linux 能力限制了 root 的权限。
-
SELinux 控制对文件系统的访问。
-
对内核文件系统只有只读访问。
-
Seccomp 限制了内核中可用的系统调用。
-
用户命名空间将主机中的一组 UID 映射到另一组,允许访问有限的 root 环境。
表 1.1 提供了更多信息和有关这些安全特性的更多详细信息的链接。
-
表 1.1 高级 Linux 安全特性
| 组件 | 描述 | 参考 |
|---|---|---|
| Linux 能力 | Linux 能力将 root 权限细分为不同的能力。 | 能力手册页是一个关于可用能力的良好概述(bit.ly/3A3Ppeg)。 |
| SELinux | 安全增强型 Linux (SELinux) 是一种 Linux 内核机制,它为系统上的每个进程和每个文件系统对象打上标签。SELinux 策略定义了标记进程如何与标签对象交互的规则。Linux 内核强制执行这些规则。 | 我编写了 SELinux 彩色图书,这是一种有趣的方式来帮助您理解 SELinux (bit.ly/33plEbD)。如果您真的想研究这个主题,请查看 SELinux 笔记本 (bit.ly/3GxGhkm)。 |
| Seccomp | seccomp 是 Linux 内核的一种机制,它限制了系统上进程组可以调用的系统调用数量。您可以移除可能危险的系统调用,防止进程调用它们。 | Seccomp 的 man 页面是获取关于 seccomp 的额外信息的良好来源 (bit.ly/3rnnim1)。 |
| 用户命名空间 | 用户命名空间允许您在命名空间分配的 UIDs 和 GIDs 组内拥有 Linux 能力,而无需在主机上具有 root 能力。 | 用户命名空间在第三章中得到了全面解释。 |
-
虚拟化技术(命名空间**)—Linux 内核采用了一种称为 命名空间 的概念,它创建了虚拟化环境,其中一组进程看到一组资源,而另一组进程看到不同的资源集。这些虚拟化环境消除了进程对系统其他部分的视图,使它们感觉像虚拟机(VM)而没有开销。命名空间的例子包括以下内容:
-
网络命名空间—消除对主机网络的访问,但允许访问虚拟网络设备
-
挂载命名空间—消除对除容器文件系统之外的所有文件系统的视图
-
PID 命名空间—消除对系统上其他进程的视图;容器进程只能看到容器内的进程
-
这些容器技术在 Linux 内核中已经存在了许多年。用于隔离进程的安全工具始于 Unix 时代的 1970 年代,而 SELinux 则始于 2001 年。命名空间大约在 2004 年引入,cgroups 大约在 2006 年引入。
注意 Windows 容器镜像确实存在,但本书专注于基于 Linux 的容器。即使运行 Windows 上的 Podman,您仍在处理 Linux 容器。Podman 在 Mac 上的内容包含在附录 E 中。Podman 在 Windows 上的内容包含在附录 F 中。
1.2.1 容器镜像:一种新的软件分发方式
容器技术直到 Docker 项目引入了容器镜像和容器注册的概念才真正兴起。基本上,它们创造了一种新的软件分发方式。
传统上,在 Linux 系统上安装多个软件应用程序导致了依赖管理问题。在容器出现之前,开发者使用 RPM 和 Debian 包管理等包管理器打包软件。这些包安装在主机上,并在主机上共享内容,包括共享库。当开发者测试他们的代码时,在主机机器上运行可能一切正常。然后,质量工程团队可能在具有不同包的不同机器上测试软件,并看到失败。两个团队都需要共同努力来生成正确的要求。最后,软件被发送给客户,他们有许多不同的配置和安装的软件,导致应用程序进一步损坏。
容器镜像通过将运行应用程序所需的所有软件捆绑成一个单元来解决依赖管理问题。你将所有库、可执行文件和配置文件一起发送。软件通过容器技术从主机隔离。通常,应用程序与主机系统交互的唯一部分是主机内核。
开发者、质量工程师和客户都运行与应用程序相同的精确容器化环境。这有助于保证一致性,并限制由配置错误引起的错误数量。
容器通常与虚拟机(VMs)相提并论,因为它们都可以在单个节点上运行多个隔离的应用程序。当使用虚拟机时,你需要管理整个虚拟机操作系统以及隔离的应用程序。你需要管理不同内核、init 系统、日志、安全更新、备份等不同组件的生命周期。系统还必须处理整个运行操作系统的开销,而不仅仅是应用程序。在容器世界中,你运行的是容器化的应用程序——没有开销,也没有额外的操作系统管理。图 1.4 展示了在三个不同的虚拟机上运行三个应用程序。

图 1.4 物理机在三个虚拟机上运行三个应用程序
使用虚拟机,你最终需要管理四个操作系统,而使用容器,三个应用程序只需运行它们所需的用户空间。如图 1.5 所示,你最终只需管理一个操作系统。

图 1.5 物理机在三个容器化应用程序中运行三个应用程序
1.2.2 容器镜像导致微服务
将应用程序打包到容器镜像中允许在同一主机上安装具有冲突要求的多应用程序。例如,一个应用程序可能需要与另一个应用程序不同的 C 库版本,这阻止了它们同时安装。图 1.6 展示了在没有使用容器的情况下,在操作系统中运行的传统应用程序。

图 1.6 传统 LAMP 栈(Linux、Apache、MariaDB 和 PHP/PERL 应用程序)在服务器上运行
容器可以在其镜像中包含正确的 C 库,每个镜像可能具有针对容器应用程序的不同版本的库。您可以从完全不同的发行版中运行应用程序。
容器使得运行同一应用程序的多个实例变得容易,如图 1.7 所示。容器镜像鼓励将单个服务或应用程序打包到单个容器中。容器允许您通过网络轻松地将多个应用程序连接在一起。

图 1.7 将 LAMP 栈(Linux、Apache、MariaDB 和 PHP/PERL 应用程序)分别打包到微服务容器中。由于容器通过网络进行通信,它们可以轻松地移动到其他虚拟机中,使得重用变得更加容易。
您可以不设计单体应用程序,其中包含一个网络前端、负载均衡器和数据库,而是构建三个不同的容器镜像,然后将它们连接起来以构建微服务。微服务允许您和其他用户尝试运行多个数据库和网络前端,然后一起编排它们。容器化的微服务使得软件的共享和重用成为可能。
1.2.3 容器镜像格式
容器镜像由三个组件组成:
-
包含运行您应用程序所需所有软件的目录树
-
描述根文件系统内容的 JSON 文件
-
另一个名为清单列表的 JSON 文件,它将多个镜像链接在一起以支持不同的架构
目录树被称为 rootfs(根文件系统)。软件布局就像它是 Linux 系统的根 (/) 一样。
在根文件系统中运行的可执行文件、工作目录、要使用的环境变量、可执行文件的维护者以及其他帮助识别镜像内容的标签定义在第一个 JSON 文件中。您可以使用 podman 的 inspect 命令查看此 JSON 文件:
$ podman inspect docker:/ /registry.access.redhat.com/ubi8
{
...
"created": "2022-01-27T16:00:30.397689Z", ❶
"architecture": "amd64", ❷
"os": "linux", ❸
"config": {
"Env": [ ❹
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
"container=oci"
],
"Cmd": [ ❺
"/bin/bash"
],
"Labels": { ❻
"architecture": "x86_64",
"build-date": "2022-01-27T15:59:52.415605",
...
}
❶ 镜像创建的日期
❷ 此镜像的架构
❸ 此镜像的操作系统
❹ 图像开发者希望在容器内设置的环境变量
❺ 容器启动时默认要执行的命令
❻ 帮助描述镜像内容的标签。这些字段可以是自由形式的,不会影响镜像的运行方式,但可以用于搜索和描述镜像。
第二个 JSON 文件,即清单列表,允许 arm64 机器上的用户拉取与他们在 arm64 机器上相同的名称的镜像。Podman 根据机器的默认架构使用此清单列表来拉取镜像。Skopeo 是一个使用与 Podman 相同的底层库的工具,可在 github.com/containers/skopeo 上找到(见附录 A)。Skopeo 提供了检查容器镜像结构的底层输出。在以下示例中,使用带有 --raw 选项的 skopeo 命令来检查 registry.access.redhat.com/ ubi8 镜像清单规范:
$ skopeo inspect --raw docker:/ /registry.access.redhat.com/ubi8
{
"manifests":
{
"digest": "sha256:cbc1e8cea
➥ 8c78cfa1490c4f01b2be59d43ddbb
➥ ad6987d938def1960f64bcd02c", ❶
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",❷
"platform": {
"architecture": "amd64", ❸
"os": "linux" ❹
},
"size": 737
},
{
"digest": ❺
➥ "sha256:f52d79a9d0a3c23e6ac4c3c8f2ed8d6337ea47f4e2dfd46201756160ca193308",
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
"platform": {
"architecture": "arm64",
"os": "linux"
},
"size": 737
},
...
}
❶ 当架构和操作系统匹配时,提取的确切镜像摘要
❷ mediaType 描述了镜像的类型,如 OCI、Docker 等。
❸ 此镜像摘要的架构:amd64
❹ 此镜像摘要的操作系统:Linux
❺ 此节指向不同架构的另一个镜像:arm64。
图片使用 Linux tar 工具将 rootfs 和 JSON 文件打包在一起。这些镜像随后存储在称为容器注册库的 Web 服务器上(例如,docker.io, quay.io 和 Artifactory)。Podman 等容器引擎可以将这些镜像复制到主机上,并在文件系统上解包。然后,引擎将镜像的 JSON 文件、引擎的内置默认值和用户的输入合并,以创建一个新的容器 OCI 运行时规范 JSON 文件。该 JSON 文件描述了如何运行容器化应用程序。
在最后一步,容器引擎启动一个名为容器运行时的程序(例如,runc、crun、kata 或 givisord)。容器运行时在最终启动容器的首要进程之前,读取容器的 JSON 文件,并配置内核 cgroups、安全约束和命名空间。
1.2.4 容器标准
OCI 标准机构定义了存储和定义容器镜像的标准格式。他们还定义了运行容器的容器引擎的标准。OCI 创建了 OCI 镜像格式,该格式标准化了容器镜像和镜像 JSON 文件的格式。他们还创建了 OCI 运行时规范,该规范标准化了用于 OCI 运行时的容器 JSON 文件。OCI 标准允许其他容器引擎,如 Podman[¹,遵循标准,能够与容器注册库中存储的所有镜像一起工作,并以与其他容器引擎(包括 Docker)完全相同的方式运行它们(见图 1.7)。
1.3 为什么在有 Docker 的情况下还要使用 Podman?
我经常被问到这样的问题,“为什么你已经有了 Docker,还需要 Podman?”好吧,一个原因就是 开源就是关于选择。操作系统有不止一个编辑器,不止一个 shell,不止一个文件系统,还有不止一个互联网网页浏览器。我相信 Podman 的设计在本质上优于 Docker,并提供了提高容器安全和使用的功能。
1.3.1 为什么只有一种运行容器的方式?
Podman 的一个优点是它是在 Docker 存在之后创建的。Podman 的开发者从完全不同的角度审视了改进 Docker 设计的方法。因为 Docker 是以开源的形式编写的,Podman 共享了一些代码并利用了新的标准,如开放容器倡议。Podman 与开源社区合作,专注于开发新功能。
在本节的其余部分,我将介绍一些这些改进。表 1.2 描述并比较了 Podman 和 Docker 中可用的功能。
表 1.2 Podman 和 Docker 功能比较
| 功能 | Podman | Docker | 描述 |
|---|---|---|---|
| 支持所有 OCI 和 Docker 镜像 | ✔ | ✔ | 从容器注册库(即,quay.io 和 docker.io)拉取和运行容器镜像。请参阅第二章。 |
| 启动 OCI 容器引擎 | ✔ | ✔ | 启动runc、crun、Kata、gVisor 和 OCI 容器引擎。请参阅附录 B。 |
| 简单的命令行界面 | ✔ | ✔ | Podman 和 Docker 共享相同的 CLI。请参阅第二章。 |
| 与 systemd 集成 | ✔ | ✘ | Podman 支持在容器内运行 systemd 以及许多 systemd 功能。请参阅第七章。 |
| Fork/exec 模型 | ✔ | ✘ | 容器是命令的子进程。 |
| 完全支持用户命名空间 | ✔ | ✘ | 只有 Podman 支持在单独的用户命名空间中运行容器。请参阅第六章。 |
| 客户端-服务器模型 | ✔ | ✔ | Docker 是一个 REST API 守护进程。Podman 通过 systemd 套接字激活服务支持 REST API。请参阅第九章。 |
支持docker-compose |
✔ | ✔ | Compose 脚本针对 REST API 工作。Podman 以 rootless 模式运行。请参阅第九章。 |
| 支持 docker-py | ✔ | ✔ | Docker-py Python 绑定针对 REST API 工作。Podman 以 rootless 模式运行。Podman 还支持 podman-py 以运行高级功能。请参阅第九章。 |
| 无守护进程模式 | ✔ | ✘ | Podman 命令的运行方式类似于传统的命令行工具,而 Docker 需要多个在 root 模式下运行的守护进程。 |
| 支持类似 Kubernetes 的 Pod | ✔ | ✘ | Podman 支持在同一个 Pod 中运行多个容器。请参阅第四章。 |
| 支持 Kubernetes YAML | ✔ | ✘ | Podman 可以根据 Kubernetes YAML 启动容器和 Pod。它还可以从运行中的容器生成 Kubernetes YAML。请参阅第八章。 |
| 支持 Docker Swarm | ✘ | ✔ | Podman 认为容器编排的多节点容器的未来是 Kubernetes,并且不打算实现 Swarm。 |
| 可定制的注册库 | ✔ | ✘ | Podman 允许您配置用于短名展开的注册库。当您指定短名时,Docker 被硬编码为 docker.io。请参阅第五章。 |
| 可定制的默认设置 | ✔ | ✘ | Podman 支持完全自定义其所有默认设置,包括安全、命名空间和卷。请参阅第五章。 |
| macOS 支持 | ✔ | ✔ | Podman 和 Docker 支持通过运行 Linux 的 VM 在 Mac 上运行容器。请参阅附录 E。 |
| Windows 支持 | ✔ | ✔ | Podman 和 Docker 支持在 Windows WSL 2 或运行 Linux 的虚拟机上运行容器。请参阅附录 F。 |
| Linux 支持 | ✔ | ✔ | Podman 和 Docker 支持所有主要的 Linux 发行版。请参阅附录 C。 |
| 软件升级时容器不会停止。 | ✔ | ✘ | Podman 不需要在容器运行时保持运行。由于 Docker 守护进程正在监控容器,默认情况下,当它停止时,所有容器都会停止。 |
1.3.2 Rootless 容器
Podman 最显著的功能可能是其能够在 rootless 模式下运行。在许多情况下,你不想给你的用户完全的 root 访问权限,但用户和开发者仍然需要运行容器和构建容器镜像。需要 root 访问权限阻止了许多注重安全的公司广泛采用 Docker。另一方面,Podman 可以在 Linux 上运行容器,无需额外的安全功能,只需一个标准的登录账户。
你可以通过将用户添加到 Docker 用户组(/etc/group)来以普通用户身份运行 Docker 客户端,但我认为授予这种访问权限是在 Linux 机器上你能做的最危险的事情之一。对 docker.sock 的访问允许你通过运行以下命令在主机上获得完整的 root 访问权限。在命令中,你将整个主机操作系统/挂载到容器内的/host 目录。--privileged标志关闭了所有容器安全特性,然后你chroot到/host。chroot之后,你就在操作系统的/目录下的 root shell 中,拥有完整的 root 权限:
$ docker run -ti --name hacker --privileged -v /:/host ubi8 chroot /host
#
在这一点上,你在机器上拥有完整的 root 权限,你可以做任何你想做的事情。当你完成对机器的破解后,你可以简单地执行docker rm命令来删除容器和你所做的一切记录:
$ docker rm hacker
当 Docker 配置为默认文件日志记录时,你启动容器的所有记录都会被删除。我相信这比没有 root 设置sudo还要糟糕,因为至少在日志文件中,你有机会看到sudo被运行。
使用 Podman 时,系统上运行的进程始终属于用户,并且没有比普通用户更大的能力。即使你从容器中逃逸出来,进程仍然以你的 UID 运行,系统上的所有操作都会记录在审计日志中。Podman 的用户不能简单地删除容器并掩盖他们的踪迹。更多信息请参阅第六章。
注意,Docker 现在可以像 Podman 一样以 rootless 模式运行,但几乎没有人那样使用。仅仅为了启动一个容器而在家目录中启动多个服务并没有流行起来。
1.3.3 Fork/exec 模型
Docker 被构建为一个 REST API 服务器。本质上,Docker 是一个包含多个守护进程的客户端-服务器架构。当用户执行 Docker 客户端时,他们执行一个连接到 Docker 守护进程的命令行工具。然后 Docker 守护进程将其存储中的镜像拉取到其存储中,然后连接到 containerd 守护进程,该守护进程最终执行一个 OCI 运行时来创建容器。然后,Docker 守护进程是一个通信平台,它从容器中创建的初始进程(PID1)读取和写入stdin、stdout和stderr。守护进程将所有输出回传给 Docker 客户端。用户想象容器的进程只是当前会话的子进程,但幕后有很多通信在进行。图 1.8 展示了 Docker 客户端-服务器架构。

图 1.8 Docker 客户端-服务器架构。容器是 containerd 的直接后裔,而不是 Docker 客户端。内核看不到客户端程序和容器之间的任何关系。
核心是 Docker 客户端与 Docker 守护进程通信,然后 Docker 守护进程与 containerd 守护进程通信,最终启动一个如runc这样的 OCI 运行时来启动容器的 PID1。以这种方式运行容器涉及很多复杂性。多年来,任何守护进程的故障都可能导致所有容器关闭,而且通常很难诊断发生了什么。Podman 的核心工程团队来自一个基于 Unix 哲学的操作系统背景。
Unix 和 C 都是基于计算中的 fork/exec 模型设计的。基本上,当你执行一个新程序时,像 Bash shell 这样的父程序会创建一个新的进程,然后作为旧程序的子程序执行新程序。Podman 工程团队认为,通过构建一个从容器注册库拉取容器镜像、配置容器存储并最终启动 OCI 运行时的工具,可以使容器更简单。这个运行时作为你的容器引擎的子程序启动容器。
在 Unix 操作系统中,进程可以通过文件系统和进程间通信(IPC)机制共享内容。这些操作系统特性使得多个容器引擎可以共享存储,而无需运行守护进程来控制访问和共享内容。除了使用操作系统文件系统提供的锁定机制外,引擎之间不需要相互通信。未来的章节将探讨这种机制的优势和劣势。图 1.9 展示了 Podman 架构和通信流程。

图 1.9 Podman fork/exec 架构。用户启动 Podman,它执行 OCI 运行时,然后启动容器。容器是 Podman 的直接后裔。
1.3.4 Podman 是无守护进程的
Podman 与 Docker 基本上不同,因为它是无守护进程的。Podman 可以运行与 Docker 相同的所有容器镜像,并使用相同的容器运行时启动容器。然而,Podman 在没有多个持续以 root 权限运行的守护进程的情况下完成这些操作。
想象一下,你有一个希望在启动时运行的 Web 服务。这个 Web 服务被封装在一个容器中,因此你需要一个容器引擎。在 Docker 的情况下,你需要将其设置在您的机器上运行,并且每个守护进程都在运行并接受连接。接下来,启动 Docker 客户端以启动 Web 服务。现在,你的容器化应用程序以及所有的 Docker 守护进程都在运行。在 Podman 的情况下,使用 Podman 命令来启动你的容器,Podman 将会消失。你的容器将继续运行,而无需运行多个守护进程的开销。在低端机器上,如 IOT 设备和边缘服务器,更少的开销非常受欢迎。
1.3.5 用户体验友好的命令行
Docker 的一个伟大特性是简单的命令行界面。曾经有过其他容器命令行,如 RKT、lxc 和 lxcd,但它们都有自己的命令行界面。Podman 团队很早就意识到,如果 Podman 有自己的命令行界面,那么它将无法获得市场份额。Docker 是主导工具,几乎每个玩过容器的人都使用过它的 CLI。此外,如果你在网上搜索如何使用容器,不可避免地你会得到一个使用 Docker 命令行的示例。从一开始,Podman 就必须与 Docker 命令行相匹配。一个用 Podman 替换 Docker 的座右铭很快就被开发出来了:alias Docker = Podman。
使用这个命令,你可以继续输入你的 Docker 命令,但 Podman 会运行你的容器。如果 Podman 命令行与 Docker 不同,则被认为是 Podman 的一个错误,并且用户要求 Podman 修复以使工具匹配。有一些命令,如 Docker Swarm,Podman 不支持,但就大部分而言,Podman 是 Docker CLI 的完整替代品。
许多发行版提供了一个名为 podman-docker 的软件包,它将别名从 docker 更改为 podman 并链接到 man 页面。别名意味着当你输入 docker ps 时,会运行 podman ps 命令。如果你执行 man docker ps,Podman 的 ps man 页面就会出现。图 1.10 是一位 Podman 用户发布的推文,他将 docker 命令别名为 podman,并惊讶地记得他已经使用 Podman 两个月了,而当时他以为自己在使用 Docker。

图 1.10 关于“alias docker=’podman’”的推文
回到 2018 年,Alan Moran 推文说:“我完全忘记了大约两个月前我设置了‘alias docker=“podman”’,这真是个梦。#nobigfatdaemons...”。Joe Thomson 回应道:“那么,是什么提醒你的?”Alan Moran 回答说:“docker help。”然后出现了 Podman 的帮助信息。
1.3.6 支持 REST API
Podman 可以作为 socket 激活的 REST API 服务运行。这允许远程客户端管理和启动 Podman 容器。Podman 支持 Docker API 以及用于高级 Podman 功能的 Podman API。通过使用 Docker API,Podman 支持docker-compose和其他使用 docker-py Python 绑定的用户。这意味着即使你围绕使用 Docker 套接字来启动容器构建了你的基础设施,你也可以简单地用 Podman 服务替换 Docker,并继续使用你现有的脚本和工具。第九章涵盖了 Podman 服务。
Podman REST API 还允许远程的 Mac、Windows 和 Linux 系统上的 Podman 客户端与 Linux 机器上的 Podman 容器交互。附录 E 和 F 涵盖了在 Mac 和 Windows 机器上使用 Podman。
1.3.7 与 systemd 的集成
Systemd 是操作系统中的基本初始化系统。Linux 系统上的初始化进程是内核在启动时启动的第一个进程。因此,初始化系统是所有进程的祖先,可以监控它们所有。Podman 希望将容器的运行与初始化系统完全集成。用户希望使用 systemd 在启动时启动和停止容器。容器应该执行以下操作:
-
在容器内支持 systemd
-
支持套接字激活
-
支持容器化应用程序完全激活的 systemd 通知
-
允许 systemd 完全管理容器化应用程序的 cgroups 和生命周期
基本上,容器在 systemd 单元文件中充当服务的作用。许多开发者希望在一个容器内运行 systemd,以便在容器内运行多个系统定义的服务。
然而,上游的 Docker 社区对此表示不同意,并拒绝了所有尝试将 systemd 集成到 Docker 中的拉取请求。他们认为 Docker 应该管理容器的生命周期,他们不希望满足那些希望在容器中运行 systemd 的用户的需求。
上游的 Docker 社区认为,与 systemd 相比,Docker 守护进程应该是进程的控制者,它应该管理容器的生命周期,并在启动时启动和停止它们。问题是 systemd 比 Docker 有更多功能,包括启动顺序、套接字激活、服务就绪通知等。图 1.11 是 DockerCon EU 上一位 Docker 员工的实际徽章,展示了他们对 systemd 的敌意。

图 1.11 DockerCon EU 上 Docker 员工的徽章
当设计 Podman 时,开发者想要确保它与 systemd 完全集成。当你在一个容器内运行 systemd 时,Podman 会按照 systemd 期望的方式设置容器,并允许它以有限的权限简单地作为容器的 PID1 运行。Podman 允许你以与系统或虚拟机中相同的方式在容器内运行服务:通过 systemd 单元文件。Podman 支持套接字激活、服务通知以及许多其他 systemd 单元文件功能。Podman 使得生成适用于在 systemd 服务中运行容器的最佳实践 systemd 单元文件变得简单。有关更多信息,请参阅第七章关于 systemd 集成的部分。
容器项目(github.com/containers),其中包含 Podman、容器库和其他容器管理工具,希望拥抱操作系统的所有功能并完全集成。第七章解释了 Podman 与 systemd 的集成。
1.3.8 Pods
Podman 的一个优点可以从其名称中看出。如前所述,Podman实际上是Pod Manager的缩写。正如官方 Kubernetes 文档所述,“Pod(就像海豹群,因此有该标志,或豌豆荚)是一组一个或多个容器,具有共享的存储/网络资源,以及如何运行容器的规范。”Podman 可以一次与单个容器一起工作,就像 Docker 一样,或者它可以一起管理 Pod 中的容器组。容器的设计目标之一是将服务分离到单个容器中:微服务。然后你将容器组合起来构建更大的服务。Pods 允许你将多个服务组合在一起形成一个更大的服务,该服务作为一个单一实体进行管理。Podman 的一个目标之一是允许你尝试使用 Pods。图 1.12 显示了在系统上运行的两个 Pod,每个 Pod 包含三个容器。

图 1.12 在主机上运行的两个 Pod。每个 Pod 运行两个不同的容器以及基础设施容器。
Podman 有一个podman generate kube命令,允许你从运行中的容器和 Pod 生成 Kubernetes YAML 文件,正如第七章中所示。同样,它还有一个podman play kube命令,允许你播放 Kubernetes YAML 文件并在你的主机上生成 Pod 和容器。我建议在单个主机上使用 Podman 运行 Pod 和容器,并使用 Kubernetes 将你的 Pod 和容器运行在多台机器上,并且在整个基础设施中运行。其他项目,如 kind (kind.sigs.k8s.io/docs/user/rootless),正在尝试在 Kubernetes 的指导下使用 Podman 运行 Pod。
1.3.9 可定制注册表
类似于 Podman 这样的容器引擎支持使用短名称拉取镜像的概念,例如 ubi8,而不必指定它们所在的注册库:registry.access.redhat.com。完整的镜像名称包括它们从中拉取的容器注册库的名称:registry.access.redhat.com/library/ubi8:latest。表 1.3 显示了镜像名称的组成部分。
表 1.3 短名称到容器镜像名称表
| Name | Registry | Repo | Name | Tag |
|---|---|---|---|---|
| 短名称 | ubi8 | |||
| 完整名称 | registry.access.redhat.com | library | ubi8 | latest |
Docker 在默认情况下,使用短名称时总是从docker.io拉取。如果您想从不同的容器注册库拉取镜像,您必须完全指定镜像。在以下示例中,我尝试拉取 ubi8/httpd-24,但失败了,因为容器镜像不在 docker.io 上。该镜像在registry.access.redhat.com:
# docker pull ubi8/httpd-24
Using default tag: latest
Error response from daemon: pull access denied for ubi8/httpd-24,
repository does not exist or may require 'docker login': denied: requested
access to the resource is denied
因此,如果我想使用 ubi8/httpd-24,我被迫输入整个名称,包括注册库:
# docker pull registry.access.redhat.com/ubi8/httpd-24
Docker 引擎给 docker.io 带来了优势,使其成为首选的注册库。Podman 被设计成允许您指定多个注册库,就像使用dnf、yum和apt工具安装软件包时一样。您甚至可以删除 docker.io。如果您尝试使用 Podman 拉取 ubi8/httpd-24,Podman 会向您提供一个注册库列表以供选择:
$ podman pull ubi8/httpd-24
? Please select an image:
registry.fedoraproject.org/ubi8/httpd-24:latest
▸ registry.access.redhat.com/ubi8/httpd-24:latest
docker.io/ubi8/httpd-24:latest
quay.io/ubi8/httpd-24:latest
一旦您做出决定,Podman 会记录短名称别名,并且不再提示并使用之前选择的注册库。Podman 支持许多其他功能,如阻止注册库、仅拉取签名镜像、设置镜像镜像以及指定硬编码的短名称,以便特定的短名称直接映射到长名称(见第五章)。
1.3.10 多种传输
Podman 支持许多不同的容器镜像源和目标,这些被称为传输(见表 1.4)。Podman 可以从容器注册库和本地容器存储中拉取镜像,同时也支持存储在 OCI 格式、OCI TAR 格式、传统 Docker TAR 格式、目录格式以及直接从 Docker 守护进程中的镜像。Podman 命令可以轻松运行来自每种格式的镜像。
表 1.4 Podman 支持的传输
| 传输 | 描述 |
|---|---|
容器注册库(docker) |
引用存储在远程容器镜像注册网站中的容器镜像。注册库存储和共享容器镜像(例如,docker.io 和 quay.io)。 |
oci |
引用符合 OCI 布局规范的容器镜像。manifest 和层 tar 包位于本地目录中作为单独的文件。 |
dir |
引用符合 Docker 镜像布局的容器镜像,类似于oci传输,但使用传统的docker格式存储文件。 |
docker-archive |
指向打包成 TAR 归档的 Docker 镜像布局中的容器镜像。 |
oci-archive |
指向符合 OCI 布局规范的容器镜像,该镜像被打包成 TAR 归档。 |
docker-daemon |
指向存储在 Docker 守护进程内部存储中的镜像。 |
container-storage |
指向存储在本地存储中的容器镜像。Podman 默认使用容器存储来处理本地镜像。 |
1.3.11 完全可定制性
容器引擎通常有很多内置的常量,例如它们运行的命名空间、SELinux 是否启用以及容器运行时使用的权限。在 Docker 中,这些值大多数是硬编码的,默认情况下无法更改。而 Podman 则具有非常可定制的配置。
Podman 有其内置的默认值,但定义了三个位置来存储其配置文件:
-
/usr/share/containers/containers.conf—在这里,发行版可以定义它希望使用的更改
-
/etc/containers/containers.conf—在这里可以设置系统覆盖项
-
$HOME/.config/containers/containers.conf—只能在无根模式下指定
配置文件允许您通过默认方式配置 Podman 以满足您的需求。您甚至可以选择以更高的安全性默认运行。
1.3.12 用户命名空间支持
Podman 完全集成了用户命名空间。无根模式依赖于用户命名空间,这允许为用户分配多个 UID。用户命名空间在系统上的用户之间提供隔离,因此您可以拥有多个无根用户,他们使用多个用户 ID 运行容器,所有这些用户都是相互隔离的。
用户命名空间可以用来隔离容器。Podman 使启动具有唯一用户命名空间的多个容器变得简单。然后内核根据 UID 分隔将进程从主机用户以及彼此之间隔离。
Docker 只支持在单个、独立的用户命名空间中运行容器,这意味着所有容器都在同一个用户命名空间内运行。一个容器中的 root 与另一个容器中的 root 相同。它不支持在不同的用户命名空间中运行每个容器,这意味着容器从用户命名空间的角度相互攻击。尽管 Docker 支持这种模式,但几乎没有人使用 Docker 在单独的用户命名空间中运行容器。
1.4 何时不使用 Podman
与 Docker 类似,Podman 不是一个容器编排器。Podman 是一个用于在单个主机上以无根或根模式运行容器工作负载的工具。如果您想在多台机器上编排运行中的容器,则需要更高层次的工具。
我认为目前做这件事最好的工具是 Kubernetes。在市场份额方面,Kubernetes 赢得了容器编排器的战争。Docker 有一个名为 Swarm 的编排器,曾经相当受欢迎,但现在似乎已经不再流行。因为 Podman 团队认为 Kubernetes 是多机容器化的正确选择,所以 Podman 不支持 Swarm 功能。Podman 已被用于不同的编排器,并用于网格/HPC 计算,开源开发者甚至将其添加到了 Kubernetes 前端。
摘要
-
容器技术已经存在很多年了,但容器镜像和容器注册表的引入为开发者提供了更好的软件分发方式。
-
Podman 是一个出色的容器引擎,适用于几乎所有的单节点容器项目。它对于开发、构建和运行容器化应用程序非常有用。
-
Podman 的使用与 Docker 一样简单,具有完全相同的命令行界面。
-
Podman 支持 REST API,允许远程工具和语言,包括
docker-compose,与 Podman 容器一起工作。 -
与 Docker 不同,Podman 包含诸如用户命名空间支持、多种传输方式、可定制的注册表、系统集成、fork/exec 模型以及开箱即用的无根模式等显著特性。
-
Podman 是运行容器的一种更安全的方式。
¹ 其他容器引擎包括 Buildah、CRI-O、containerd 以及许多其他引擎。
2 命令行
本章涵盖
-
Podman 命令行
-
运行 OCI 应用程序
-
比较容器和镜像
-
构建 OCI 基础镜像
Podman 是运行和构建容器化应用程序的优秀工具。在本章中,你将通过构建一个简单的 Web 应用程序来开始,以展示 Podman 命令行中常用的功能。
如果你的机器上没有安装 Podman,你可以跳转到附录 C,然后再返回这里。本章假设 Podman 4.1 或更高版本已经安装。Podman 的旧版本可能运行良好,但所有示例都是使用 Podman 4.1 进行测试的。我使用的示例基础镜像来自 registry.access.redhat.com/ubi8/httpd-24。
注意:通用基础镜像(UBI)可以在任何地方使用,但由 Red Hat 维护和审核的容器软件以及运行在 Red Hat 操作系统上的软件完全受支持。有数百个 Apache 镜像与此镜像类似,你也可以尝试使用。
第二章展示了 Podman 是一个处理容器的优秀工具。在本章中,我将带你通过运行可能用于构建容器化应用程序的场景。你启动一个容器,修改其内容,创建一个镜像,并将其发送到注册库。然后我解释了如何以自动化的方式执行这些操作以维护容器镜像的安全性。在这个过程中,你将接触到许多 Podman 命令行界面,并深入了解如何使用 Podman。
如果你是一个经验丰富的 Docker 用户,你可能只想快速浏览本章。你会知道很多内容,但 Podman 有许多独特的功能,例如挂载容器镜像(第 2.2.10 节)和不同的传输方式(第 2.2.4 节)。让我们先运行我们的第一个容器。
注意:Podman 是一个处于高度开发中的开源项目。Podman 被打包并提供在许多不同的 Linux 发行版上,以及 Mac 和 Windows 上。这些发行版可能正在运输 Podman 的旧版本,其中一些当前书籍中涵盖的功能可能不存在。本书中的一些示例假设你正在使用 Podman 4.1 或更高版本。如果示例无法正常工作,请更新你的 Podman 版本到最新版本。有关安装 Podman 的更多信息,请参阅附录 C。
2.1 与容器一起工作
在容器注册库中坐落着成千上万的不同的容器镜像。开发者、管理员、质量工程师和普通用户主要使用 podman run 命令来拉取和运行、测试或探索这些容器镜像。要开始构建容器化应用程序,你需要做的第一件事是与基础镜像开始工作。在我们的示例中,你将 registry.access.redhat.com/ubi8/httpd-24 镜像拉取并运行到你的家目录中的容器存储,并开始探索容器内部。
2.1.1 探索容器
在本节中,你将逐步检查一个典型的 Podman 命令。你将执行 podman 的 run 命令,该命令连接到 registry.access.redhat.com 容器注册库,并开始拉取镜像并将其存储在本地主目录中:
$ podman run -ti --rm registry.access.redhat.com/ubi8/httpd-24 bash
现在,我将分解你刚才执行的命令。默认情况下,podman 的 run 命令在容器退出前在前台执行容器化的命令。在这种情况下,你最终会在容器内看到一个 Bash 提示符,显示 bash-4.4$ 提示符。当你退出这个 Bash 提示符时,Podman 会停止容器。
在这个例子中,你使用了两个选项:-t 和 -i,作为 -ti,这告诉 Podman 连接到终端。这会将容器的 bash 进程的输入、输出和错误流连接到你的屏幕,允许你在容器内进行交互:
$ podman run -ti --rm registry.access.redhat.com/ubi8/httpd-24 bash
--rm 选项告诉 Podman 在容器退出时立即删除容器,从而释放容器占用的所有存储空间:
$ podman run -ti --rm registry.access.redhat.com/ubi8/httpd-24 bash
接下来,指定你正在使用的容器镜像,即 registry.access.redhat.com/ubi8/httpd-24。podman 命令连接到 registry.access.redhat.com 容器注册库,并开始下载 ubi8/httpd-24:latest 镜像。Podman 会复制多个层(也称为 blob),如下所示,并将它们存储在本地容器存储中。你可以看到随着镜像层的拉取,进度条的变化。一些镜像相当大,拉取时可能需要很长时间。如果你稍后要在同一镜像上运行不同的容器,Podman 会跳过镜像拉取步骤,因为你已经拥有正确的镜像在本地容器存储中。
列表 2.1 从注册库拉取并运行容器镜像
$ podman run -ti --rm registry.access.redhat.com/ubi8/httpd-24 bash
Trying to pull registry.access.redhat.com/
➥ ubi8/httpd-24:latest... ❶
Getting image source signatures
Checking if image destination supports signatures
Copying blob 296e14ee2414 skipped: already exists ❷
Copying blob 356f18f3a935 skipped: already exists ❷
Copying blob 359fed170a21 ❷
➥ [========================>---------] 11.8MiB / 16.2MiB ❷
Copying blob 226cafc3a0c6 ❷
➥ [=====>----------------------------] ❷
➥ 10.1MiB / 61.1MiB ❷
❶ 与注册库建立联系
❷ 跳过层拉取。
最后,指定容器内要运行的可执行文件,在本例中是 bash:
$ podman run -ti --rm registry.access.redhat.com/ubi8/httpd-24 bash
...
bash-4.4$
注意:镜像几乎总是有默认要执行的命令。只有当你想要覆盖镜像运行的默认应用程序时,才需要指定命令。在 registry.access.redhat.com/ubi8/httpd-24 镜像的情况下,它运行 Apache 网络服务器。
当你在 bash shell 容器内部时,运行 cat /etc/os-release,并注意它可能是一个不同的操作系统或版本,与容器外部的 /etc/os-release 不同。在容器内四处探索,并注意它与主机环境的不同之处:
bash-4.4$ grep PRETTY_NAME /etc/os-release
PRETTY_NAME="Red Hat Enterprise Linux 8.4 (Ootpa)"
在我的主机上的另一个终端中,相同的命令输出
$ grep PRETTY_NAME /etc/os-release
PRETTY_NAME="Fedora Linux 35 (Workstation Edition Prerelease)"
回到容器内部,你会注意到可用的命令要少得多:
bash-4.4$ ls /usr/bin | wc -l
525
然而,在主机上你看到
$ ls -l /usr/bin | wc -l
3303
执行 ps 命令以查看容器内正在运行哪些进程:
$ ps
PID TTY TIME CMD
1 pts/0 00:00:00 bash
2 pts/0 00:00:00 ps
你只能看到两个进程:bash 脚本和 ps 命令。不用说,在我的主机机器上,有成百上千个进程正在运行(包括这两个进程)。你可以进一步探索容器内部,以了解容器内正在发生的事情。
完成后,你退出bash脚本,容器将关闭。由于你使用了--rm选项,Podman 将删除所有容器存储并删除容器。容器镜像仍然保留在container/storage中。现在你已经探索了容器的内部工作原理,是时候开始使用容器中的默认应用程序了。
2.1.2 运行容器化应用程序
在上一个示例中,你在一个容器化的应用程序中拉取并运行了bash,但你没有运行开发者希望你运行的应用程序。在这个下一个示例中,你将通过移除命令并使用几个新选项来运行实际的应用程序。
首先,移除-ti和--rm选项,因为你希望在podman命令退出时容器保持运行。你不是一个在容器内交互式运行的 shell,因为它只是运行容器化的网络服务:
$ podman run -d -p 8080:8080 --name myapp registry.access.redhat.com/ubi8/httpd-24
37a1d2e31dbf4fa311a5ca6453f53106eaae2d8b9b9da264015cc3f8864fac22
首先要注意的选项是-d(--detach)选项,它告诉 Podman 启动容器然后从它断开连接。基本上,在后台运行容器。Podman 命令实际上会退出并留下容器运行。第六章将更深入地探讨幕后发生的事情:
$ podman run -d -p 8080:8080 --name myapp registry.access.redhat.com/ubi8/httpd-24
-p(--publish)选项告诉 Podman 在容器运行时将容器端口8080发布或绑定到主机端口8080。使用-p选项时,冒号之前的部分指的是主机端口,而冒号之后的部分指的是容器端口。在这种情况下,你可以看到端口是相同的。如果你只指定一个端口,Podman 认为这个端口是容器端口,并随机选择一个主机端口来绑定容器端口。你可以使用podman port命令来发现哪些端口绑定到了容器。
列表 2.2 podman port命令的示例
$ podman port myapp
8080/tcp -> 0.0.0.0:8080 ❶
❶ 显示容器内部的 8080/tcp 端口绑定到主机网络的所有网络(0.0.0.0)的 8080 端口
默认情况下,容器在其自己的网络命名空间内创建,这意味着它们绑定到虚拟化网络而不是主机网络。假设我没有使用-p选项来执行容器。在这种情况下,容器内的 Apache 服务器绑定到容器网络命名空间内的网络接口,但 Apache 没有绑定到主机网络。
只有容器内的进程能够连接到端口8080以与网络服务器通信。通过带有-p选项的命令执行,Podman 将容器内的端口连接到指定端口的主机网络。这个连接允许外部进程,如网络浏览器,从网络服务中读取。
注意:如果您以无根模式运行容器,如第三章所述,Podman 用户默认不允许内核绑定端口 < 1024。一些容器想要绑定到较低的端口,如端口 80,这在容器内部是允许的,但 -p 80:80 会失败,因为 80 小于 1024。使用 -p 8080:80 会导致 Podman 将主机的端口 8080 绑定到容器内的端口 80。上游 Podman 仓库包含有关绑定端口小于 1024 以及许多其他问题的故障排除信息(见 mng.bz/69ry)。
-p 选项可以将容器内的端口号映射到容器外的不同端口号:
$ podman run -d -p 8080:8080 --name myapp registry.access.redhat.com/ubi8/httpd-24
在示例名称中,容器 myapp 使用了 --name myapp 选项。指定名称可以更容易地找到容器,并允许您指定一个名称,然后可以用于其他命令(例如,podman stop myapp)。如果您不指定名称,Podman 会自动生成一个唯一的容器名称以及容器 ID。所有与容器交互的 Podman 命令都可以使用名称或 ID:
$ podman run -d --name myapp -p 8080:8080 registry.access.redhat.com/ubi8/httpd-24
当 podman run 命令完成后,容器正在运行。由于此容器以分离模式运行,Podman 打印出容器 ID 并退出,但容器仍然在运行:
$ podman run -d -p 8080:8080 --name myapp registry.access.redhat.com/ubi8/httpd-24
37a1d2e31dbf4fa311a5ca6453f53106eaae2d8b9b9da264015cc3f8864fac22
现在容器正在运行,您可以通过启动一个网络浏览器来与容器内部的 Web 服务器进行通信,该服务器在 localhost 端口 8080 上运行(见图 2.1):
$ web-browser localhost:8080

图 2.1 使用 Podman 运行的 ubi8/httpd-24 容器连接到 Web 浏览器窗口
恭喜!您已成功启动了您的第一个容器化应用程序。
现在假设您想要启动另一个容器。您可以通过进行一些更改来执行类似的命令:
$ podman run -d -p 8081:8080 --name myapp1 \
➥ registry.access.redhat.com/ubi8/httpd-24
fa41173e4568a8fa588690d3177150a454c63b53bdfa52865b5f8f7e4d7de1e1
注意,您需要将容器的名称更改为 myapp1;否则,由于容器之前已存在,使用 myapp 名称的 podman run 命令会失败。您还需要将 -p 选项更改为使用 8081 作为主机端口,因为之前的容器 myapp 目前正在运行并绑定到端口 8080。第二个容器不允许绑定到端口 8080,直到第一个容器退出:
$ podman run -d -p 8081:8080 --name myapp1
registry.access.redhat.com/ubi8/httpd-24
podman 的 create 命令几乎与 podman 的 run 命令相同。如果镜像不在容器存储中,create 命令会拉取镜像并配置容器信息以便运行,但不会执行容器。它通常与第 2.1.4 节中描述的 podman start 命令一起使用。您可能想要创建一个容器,然后稍后使用 systemd 单元文件来启动和停止容器。
一些值得注意的 podman run 选项包括以下内容:
-
--
userUSERNAME—这告诉 Podman 以在镜像中定义的特定用户运行容器。默认情况下,Podman 将以 root 用户身份运行容器,除非容器镜像指定了默认用户。 -
--rm— 这会在容器退出时自动删除容器。 -
--tty-(t)— 这分配一个伪-tty并将其附加到容器的标准输入。 -
--interactive(-i)— 这将stdin连接到容器的首要进程。这些选项在容器内为你提供了一个交互式 shell。
注意:有数十个 podman run 选项可供使用,允许你更改安全功能、命名空间、卷等。我在本书中使用了其中一些并进行了解释。请参阅 podman-run 的 man 页面以了解所有选项的描述。表 2.1 中定义的大多数 podman create 选项也适用于 podman run。
使用 man podman-run 命令获取所有选项的信息。现在容器已经启动并运行,是时候停止容器并进入下一步了。
2.1.3 停止容器
你有两个正在运行的容器,并且通过运行网页浏览器对它们进行了测试。为了通过实际向网页添加内容来继续开发,你可以使用 podman stop 命令停止容器:
$ podman stop myapp
stop 命令会停止之前使用 podman run 命令启动的容器。
当停止容器时,Podman 会检查正在运行的容器,并向容器的首要进程(PID1)发送停止信号,通常是 SIGTERM,然后默认等待 10 秒以等待容器停止。停止信号告诉容器内的首要进程优雅地退出。如果容器在 10 秒内没有停止,Podman 会向进程发送 SIGKILL 信号,强制容器停止。这 10 秒的等待时间给容器内的进程提供了清理和提交更改的时间。
可以使用 podman run --stop-signal 选项更改容器的默认停止信号。有时容器的首要或初始化进程会忽略 SIGTERM(例如,在容器内部使用 systemd 作为首要进程的容器)。systemd 忽略 SIGTERM 并指定使用 SIGRTMIN+3(信号 #37)信号来关闭。停止信号可以嵌入到容器镜像中,正如我在第 2.3 节中描述的那样。
一些容器会忽略 SIGTERM 停止信号,这意味着你必须等待 10 秒才能让容器退出。如果你知道容器忽略了默认的停止信号,并且你不在乎容器进行清理,你只需将 -t 0 选项添加到 podman stop 中,就可以立即发送 SIGKILL 信号:
$ podman stop -t 0 myapp1
myapp1
Podman 有一个类似的命令 podman kill,它发送指定的终止信号。当你想要向容器发送信号而不实际停止容器时,podman kill 命令非常有用。
一些值得注意的 Podman 停止选项包括以下内容:
-
--timeout(-t)— 这设置超时时间;-t 0在等待容器停止之前发送SIGKILL信号。 -
--latest(-l``)—这是一个有用的选项,允许您停止最后一个创建的容器,而不是必须使用容器名称或容器 ID。大多数需要您指定容器名称或 ID 的 Podman 命令也接受--latest选项。此选项仅在 Linux 机器上可用。 -
--all—这告诉 Podman 停止所有正在运行的容器。类似于--latest,需要容器名称或容器 ID 参数的 Podman 命令也接受--all选项。
使用 man podman-stop 命令获取所有选项的信息。
最终,您的系统将会有很多停止的容器,有时您需要重新启动它们(例如,如果系统重启)。另一个常见用例是首先创建一个容器,然后稍后启动它。下一节将解释如何启动容器。
2.1.4 启动容器
您创建的容器现在已经停止。接下来,您可能想要使用以下列表中的命令重新启动它。
列表 2.3 启动容器的示例
$ podman start myapp
myapp ❶
❶ 启动命令打印已启动容器的名称。
podman start 命令启动一个或多个容器。此命令将输出容器 ID,表示您的容器正在运行。您现在可以使用网络浏览器重新连接到它。podman start 的一个常见用例是在重启后启动所有在关机期间停止的容器。
一些喜欢的 Podman 启动选项包括以下内容:
-
--all—这会启动容器存储中所有停止的容器。 -
--attach—这会将您的终端连接到容器的输出。 -
--interactive(-i``)—这会将终端输入连接到容器。
使用 man podman-start 命令获取所有选项的信息。
在您使用 Podman 一段时间并拉取并运行了许多不同的容器镜像之后,您可能想要找出哪些容器正在运行或您在本地存储中有哪些容器。您将需要能够列出这些容器。
2.1.5 列出容器
您可以列出正在运行的容器和之前创建的所有容器。使用 podman ps 命令列出容器:
$ podman ps
CONTAINER ID IMAGE COMMAND CREATED \
➥ STATUS PORTS NAMES
b1255e94d084 registry.access.redhat.com/ubi8/httpd-24:latest /usr/bin/run-\
➥ http... 6 minutes ago Up 4 minutes ago 0.0.0.0:8080->8080/tcp myapp
注意,默认情况下 podman ps 命令列出正在运行的容器。使用 --all 选项查看所有容器:
$ podman ps --all
CONTAINER ID IMAGE COMMAND CREATED \
➥ STATUS PORTS NAMES
b1255e94d084 registry.access.redhat.com/ubi8/httpd-24:latest /usr/bin/run-\
➥ http... 9 minutes ago Up 8 minutes ago 0.0.0.0:8080->8080/tcp myapp
3efee4d39965 registry.access.redhat.com/ubi8/httpd-24:latest /usr/bin/run-\
➥ http... 7 minutes ago Exited (0) 3 minutes ago 0.0.0.0:8081->8080/tcp myapp1
一些值得注意的 podman ps 选项包括以下内容:
-
--all—这告诉 Podman 列出所有容器而不是仅列出正在运行的容器。 -
--quiet—这告诉 Podman 只打印容器 ID。 -
--size—这告诉 Podman 返回每个容器(除了基于它们的镜像)当前使用的磁盘空间量。
使用 man podman-ps 命令获取所有选项的信息。现在您已经知道了系统上所有的容器,您可能想要检查它们的内部结构。
2.1.6 检查容器
为了完全了解一个容器,有时您想知道容器基于哪个镜像,容器默认获取哪些环境变量,或者容器使用的安全设置是什么。podman ps命令为我们提供了一些关于容器的数据,但如果您想真正检查有关容器的信息,您可以使用podman inspect命令。
podman inspect命令也可以用来检查镜像、网络、卷和 Pod。podman container inspect命令也是可用的,并且是针对容器的。但大多数用户只是输入较短的podman inspect命令:
$ podman inspect myapp
[
{
"Id": "240271ae90480d3836b1477e5c0b49fbd3883846ca474e3f6effdfb271f4ff54",
"Created": "2021-09-27T05:27:47.163828842-04:00",
"Path": "container-entrypoint",
"Args": [
"/usr/bin/run-httpd"
],
...
]
如您所见,podman inspect命令输出一个大的 JSON 文件——在我的机器上有 307 行。所有这些信息最终都会传递给 OCI 运行时以启动容器。当使用inspect命令时,通常最好将其输出通过less或grep管道,以找到您感兴趣的字段。或者,您可以使用格式选项。如果您想检查启动容器时执行的命令,请执行以下操作。
列表 2.4 检查要执行的指定容器命令
$ podman inspect --format '{{ .Config.Cmd }}' myapp ❶
[/usr/bin/run-httpd]
❶ 检查正在显示来自 OCI 镜像规范的数据。
或者,如果您想查看停止信号,请执行以下操作。
列表 2.5 检查停止容器时使用的停止信号
$ podman inspect --format '{{ .Config.StopSignal }}' myapp
15 ❶
❶ 所有容器的默认停止信号为 15(SIGTERM)。
一些显著的podman inspect选项包括以下内容:
-
--latest(-l``)—这很方便,因为它允许您快速检查最新创建的容器,而不是指定容器名称或容器 ID。 -
--format—正如之前所示,这很有用,可以从中提取 JSON 中的特定字段。 -
--size—这会增加容器使用的磁盘空间量。收集这些信息需要很长时间,因此默认情况下不会执行。
使用man podman-inspect命令获取有关所有选项的信息。在检查完容器后,您可能会意识到您不再需要占用存储空间的该容器,因此您需要删除它。
2.1.7 删除容器
如果您已经使用完一个容器,您可能想删除该容器以释放磁盘空间或重用容器名称。记得您启动了第二个名为myapp1的容器吗?您不再需要它,因此可以删除它。在删除之前,请确保停止容器(第 2.1.3 节)。然后使用podman rm命令删除容器:
$ podman rm myapp1
3efee4d3996532769356ffea23e1f50710019d4efc704d39026c5bffd6aa18be
一些显著的podman rm选项包括以下内容:
-
--all—如果您想删除所有容器,此选项很有用。 -
--force—此选项告诉 Podman 在删除时停止所有正在运行的容器。
使用man podman-rm命令获取有关所有选项的信息。现在您已经了解了一些命令,是时候开始修改正在运行的容器了。
2.1.8 在容器中执行
通常,当容器正在运行时,你可能会想在容器内启动另一个进程以进行调试或检查正在发生的事情。在某些情况下,你可能想要修改容器使用的一些内容。
假设你想进入你的容器并修改它显示的网页。你可以使用podman exec命令进入容器。使用--interactive(-i)选项允许你在容器内执行命令。你需要指定容器的名称myapp并在容器内执行 Bash 脚本。如果你停止了myapp容器,你需要重新启动它,因为podman exec只对正在运行的容器有效。
在以下示例中,你将在容器中exec一个bash进程来创建/var/www/html/index.xhtml文件。你将写入 HTML 内容,使容器化的网站显示Hello World:
$ podman exec -i myapp bash -c 'cat > /var/www/html/index.xhtml' << _EOF
<html>
<head>
</head>
<body>
<h1>Hello World</h1>
</body>
</html>
_EOF
第二次exec回到容器中,你可以看到文件已被成功修改。这表明通过exec对容器的修改是永久性的,即使你停止并重新启动了容器,这些修改也会保留。podman run和podman exec之间的一个关键区别是run从一个带有内部运行进程的图像创建一个新的容器,而exec在现有容器内部启动进程:
$ podman exec myapp cat /var/www/html/index.xhtml
<html>
<head>
</head>
<body>
<h1>Hello World</h1>
</body>
</html>
现在,让我们将一个网络浏览器连接到容器以查看内容是否已更改(见图 2.2):
$ web-browser localhost:8080

图 2.2 Web 浏览器窗口连接到在 Podman 中运行的更新后的 Hello World HTML 的 ubi8/httpd-24 容器
一些值得注意的podman exec选项包括以下内容:
-
--tty—这会将一个-tty连接到exec会话。 -
--interactive—-i选项告诉 Podman 以交互模式运行,这意味着你可以与一个exec执行的程序交互,比如一个 shell。
使用man podman-exec命令获取所有选项的信息。
现在你已经创建了一个应用程序,你可能想要与他人分享它。首先,你需要将容器提交为一个图像。
2.1.9 从容器创建图像
开发者通常从基础镜像运行容器以创建一个新的容器环境。一旦完成,他们将这个环境打包成一个容器图像以便与其他用户分享。然后,这些用户可以使用 Podman 启动容器化应用程序。你可以通过将容器提交到 OCI 图像来实现这一点。
首先,停止或暂停容器以确保在提交过程中没有任何内容被修改:
$ podman stop myapp
现在你可以执行podman commit命令,将你的应用程序容器myapp提交,创建一个名为myimage的新图像:
$ podman commit myapp myimage
Getting image source signatures
Copying blob e39c3abf0df9 skipped: already exists
Copying blob 8f26704f753c skipped: already exists
Copying blob 83310c7c677c skipped: already exists
Copying blob 654b3bf1361e skipped: already exists
Copying blob 9e816183404c done Copying config e38084bb8a done
Writing manifest to image destination
Storing signatures
e38084bb8a76104a7cac22b919f67646119aff235bb1cfcba5478cc1fbf1c9eb
现在你可以通过调用podman start继续运行现有的myapp容器,或者你可以基于myimage创建一个新的容器:
$ podman run -d --name myapp1 -p 8080:8080 myimage
0052cb32c8e63b845ac5dfd5ba176b8204535c2c6cafa3277453424de601263f
注意:使用 podman commit 命令来创建镜像不是一种常见的方法。构建容器镜像的整个过程可以使用 podman build 脚本化和自动化。有关此过程的更多信息,请参阅第 2.3 节。
一些显著的 podman commit 选项包括以下内容:
-
--pause—在提交过程中暂停正在运行的容器。注意我在提交之前停止了容器,尽管我可以简单地暂停它。podmanpause和podmanunpause命令允许您直接暂停和恢复容器。 -
--change—此选项允许您提交关于使用镜像的说明。说明包括CMD、ENTRYPOINT、ENV、EXPOSE、LABEL、ONBUILD、STOPSIGNAL、USER、VOLUME和WORKDIR。这些说明与 Containerfile 或 Dockerfile 中的指令相对应。
使用 man podman-commit 命令获取所有选项的信息。表 2.1 列出了所有 Podman 容器命令。
现在您已经将容器提交为镜像,是时候展示 Podman 如何与镜像一起工作了。
注意:您已经检查了一些 Podman 容器命令,但还有很多。使用 podman-container(1) 手册页来探索所有这些命令,以及本节中指定命令的完整描述。
表 2.1 Podman 容器命令
| 命令 | 手册页 | 描述 |
|---|---|---|
attach |
podman-container-attach(1) |
连接到正在运行的容器。 |
checkpoint |
podman-container-checkpoint(1) |
创建容器的检查点。 |
cleanup |
podman-container-cleanup(1) |
清理容器的网络和挂载点。 |
commit |
podman-container-commit(1) |
将容器提交到镜像中。 |
cp |
podman-container-cp(1) |
将文件或文件夹复制到容器中或从容器中复制出来。 |
create |
podman-container-create(1) |
创建一个新的容器。 |
diff |
podman-container-diff(1) |
检查容器文件系统中的更改。 |
exec |
podman-container-exec(1) |
在容器中运行进程。 |
exists |
podman-container-exists(1) |
检查容器是否存在。 |
export |
podman-container-export(1) |
将容器的文件系统导出为 TAR 归档。 |
init |
podman-container-init(1) |
初始化容器。 |
inspect |
podman-container-inspect(1) |
显示容器的详细信息。 |
kill |
podman-container-kill(1) |
向容器中的主进程发送信号。 |
List (ps``) |
podman-container-list(1) |
列出所有容器。 |
logs |
podman-container-logs(1) |
获取容器的日志。 |
mount |
podman-container-mount(1) |
挂载容器的根文件系统。 |
pause |
podman-container-pause(1) |
暂停容器。 |
port |
podman-container-port(1) |
列出容器的端口映射。 |
prune |
podman-container-prune(1) |
删除所有非运行中的容器。 |
rename |
podman-container-rename(1) |
重命名现有的容器。 |
restart |
podman-container-restart(1) |
重新启动一个容器。 |
restore |
podman-container-restore(1) |
恢复已检查点的容器。 |
rm |
podman-container-rm(1) |
删除一个容器。 |
run |
podman-container-run(1) |
在新容器中运行命令。 |
runlabel |
podman-container-runlabel(1) |
执行由镜像标签描述的命令。 |
start |
podman-container-start(1) |
启动一个容器。 |
stats |
podman-container-stats(1) |
显示容器的统计信息。 |
stop |
podman-container-stop(1) |
停止一个容器。 |
top |
podman-container-top(1) |
显示容器中的运行进程。 |
unmount |
podman-container-unmount(1) |
卸载容器的根文件系统。 |
unpause |
podman-container-unpause(1) |
恢复 pod 中所有容器的暂停状态。 |
wait |
podman-container-wait(1) |
等待容器退出。 |
2.2 与容器镜像一起工作
在上一节中,你尝试了与容器的基本操作,包括检查和提交到容器镜像。在本节中,你将尝试与容器镜像一起工作,了解它们与容器之间的区别,以及如何通过容器注册库来共享它们。
2.2.1 容器和镜像之间的区别
计算机编程的一个问题是,相同的名称被不断地用于不同的目的。在容器世界中,没有比 container 更被过度使用的术语了。通常 container 指的是 Podman 启动的运行进程。但 container 也可以指作为非运行对象坐在容器存储中的容器数据。正如你在上一节中看到的,podman ps --all 显示了运行和非运行的容器。
另一个例子是术语 namespace,它在许多不同的方式中被使用。当人们谈论 Kubernetes 中的命名空间时,我经常感到困惑。有些人听到这个术语就会想到 虚拟集群,但当我听到它时,我会想到与 Pods 和容器一起使用的 Linux 命名空间。同样,image 可以指 VM 镜像、容器镜像、OCI 镜像或存储在容器注册库中的 Docker 镜像。
我认为容器是在环境中执行进程或在准备运行的东西。相比之下,镜像是被 提交 的 容器,准备与他人共享。其他用户或系统可以使用这些镜像来创建新的容器。
容器镜像只是提交的容器。OCI 定义了镜像的格式。Podman 使用容器/镜像库 (github.com/containers/image) 与镜像的所有交互。容器镜像可以存储在不同的存储或传输类型中,因为 container/image 就是指这些。这些传输可以是容器注册库、Docker 存档、OCI 存档、docker-daemon,以及 containers/storage。有关传输的更多信息,请参阅 2.2.4 节。
在 Podman 的上下文中,我通常将镜像称为存储在容器存储或容器注册库(如 docker.io 和 quay.io)中的本地内容。Podman 使用 GitHub container/storage 库(github.com/containers/storage)来处理本地存储的镜像。让我们更详细地看看它。
容器/存储库提供了存储容器的概念。基本上,存储容器是尚未提交的中间存储内容。把它们想象成磁盘上的文件和一些描述内容的 JSON。Podman 有自己的与 Podman 容器相关的数据存储库,Podman 需要同时处理其容器的多个用户。它依赖于 containers/storage 提供的文件系统锁定,以确保数百个 Podman 可执行文件可以可靠地共享相同的数据存储库。
当你将容器提交到存储时,Podman 将容器存储复制到镜像存储。镜像存储在一系列层中,每次提交都会创建一个新的层。
我喜欢将镜像想象成一个婚礼蛋糕(图 2.3)。在我们之前的例子中,你使用了 ubi8/httpd-24 镜像,它由两层组成:基础层是 ubi8,然后镜像提供的添加了 httpd 包和其他一些内容,创建了 ubi8/httpd-24。现在当你提交上一节中的容器时,Podman 在 ubi8/httpd-24 镜像之上添加了另一个层,称为 myimage。

图 2.3 展示了构成我们的 Hello World 应用程序的镜像的婚礼蛋糕展示。
一个方便的 Podman 命令,用于显示镜像的层,是 podman image tree 命令:
$ podman image tree myimage
Image ID: 2c7e43d88038
Tags: [localhost/myimage:latest]
Size: 461.7MB
Image Layers
├── ID: e39c3abf0df9 Size: 233.6MB
├── ID: 42c81bd2b468 Size: 20.48kB Top Layer of: [registry.access.redhat.com/ubi8:latest]
├── ID: 51a7beaa0b88 Size: 57.43MB
├── ID: 519e681b5702 Size: 170.6MB Top Layer of: [registry.access.redhat.com/ubi8/httpd-24:latest]
└── ID: bc3dcdefdac3 Size: 69.63kB Top Layer of: [localhost/myimage:latest localhost/myapp:latest]
你可以看到,镜像 myimage 由五个层组成。
另一个有用的 Podman 命令,podman image diff,允许你看到与另一个镜像或底层相比实际已更改(C)、已添加(A)或已删除(D)的文件和目录:
$ podman image diff myimage ubi8/httpd-24
C /etc/group
C /etc/httpd/conf
C /etc/httpd/conf/httpd.conf
C /etc/httpd/conf.d
C /etc/httpd/conf.d/ssl.conf
C /etc/httpd/tls
C /etc
C /etc/httpd
A /etc/httpd/tls/localhost.crt
A /etc/httpd/tls/localhost.key
...
镜像只是应用于底层镜像的软件的 TAR 差分,容器内容是软件的一个未提交层。一旦容器被提交,你就可以在你的镜像之上创建其他容器。你还可以与他人共享镜像,这样他们就可以在你的镜像上创建其他容器。现在让我们看看你的容器存储中的所有镜像。
2.2.2 列出镜像
在容器部分,你正在处理镜像,并使用 podman images 命令列出本地存储中的镜像:
$ podman images
REPOSITORY TAG IMAGE ID CREATED SIZE
localhost/myimage latest 2c7e43d88038 46 hours ago 462 MB
registry.access.redhat
➥.com/ubi8/httpd-24 latest 8594be0a0b57 5 weeks ago 462 MB
registry.access.redhat
➥.com/ubi8 latest ad42391b9b46 5 weeks ago 234 MB
让我们看看默认输出中的不同字段。表 2.2 描述了 podman images 命令可用的不同字段和数据。你将在本节中使用 podman images 命令。
表 2.2 由 podman images 命令列出的默认字段
| 标题 | 描述 |
|---|---|
Repository |
镜像的完整名称。 |
TAG |
镜像的版本(标签)。镜像标记将在 2.2.6 节中介绍。 |
IMAGE ID |
镜像的唯一标识符。它由 Podman 通过镜像的 JSON 配置对象的 SHA256 哈希生成。 |
CREATED |
自镜像创建以来经过的时间。默认情况下,镜像按此字段排序。 |
SIZE |
镜像使用的存储量。 |
注意:随着时间的推移,你拉取的所有镜像使用的存储量会增加。用户耗尽磁盘空间相对常见,因此你应该监控镜像和容器的大小,在你不再使用它们时删除它们。使用 man podman-system-prune 命令获取更多关于清理的信息。
一个值得注意的 podman 镜像选项如下:
--all——此选项对于列出所有镜像很有用。默认情况下,podman-images只列出当前正在使用的镜像。当一个镜像被具有相同标签的新镜像替换时,之前的镜像会被标记为<none><none>;这些镜像被称为悬挂镜像。我在第 2.3.1 节中介绍了悬挂镜像。
使用 man podman-images 命令来获取所有选项的信息。类似于容器,你可能会想通过检查来查看与镜像关联的配置信息。
2.2.3 检查镜像
在前面的章节中,我提到了几个检查镜像的命令。我使用了 podman image diff 来检查镜像之间创建或删除的文件和目录。我还展示了使用 podman image tree 命令查看镜像层次结构或婚礼蛋糕层的办法。
有时你可能想检查镜像的配置;使用 podman image inspect 命令来做这件事。podman inspect 命令也可以用来检查镜像,但名称可能与容器冲突,所以我更喜欢使用特定的镜像命令:
$ podman image inspect myimage
[
{
"Id": "3b8fcf9081b4c4e6c16d763b8d02684df0737f3557a1e03ebfe4cc7cd6562135",
"Digest":
"sha256:ff49aa6253ae47569d5aadbd73d70e7d0431bcf3a2f57b1b56feecdb531029a3",
"RepoTags": [
"localhost/myimage:latest"
],
"RepoDigests": [ "localhost/myimage@sha256:ff49aa6253ae47569d5aadbd73d70e7d0431bcf3a2f57b1b\
➥ 56feecdb531029a3"
],
...
]
如你所见,这个命令输出了一个大的 JSON 数组——在先前的例子中有 153 行——它包括了用于 OCI 镜像格式规范的所需数据。当你从一个镜像创建容器时,这些信息被用作创建容器的输入之一。
当使用 inspect 命令时,通常最好将其输出通过 less 或 grep 管道,以找到你感兴趣的特定字段。或者,你也可以使用 --format 选项。
如果你想要检查从这个镜像执行默认命令的情况,请执行以下操作:
$ podman image inspect --format '{{ .Config.Cmd }}' myimage
[/usr/bin/run-httpd]
或者,如果你想查看停止信号,执行以下命令:
$ podman image inspect --format '{{ .Config.StopSignal }}' myimage
如你所见,没有输出任何内容,这意味着应用程序的开发者没有指定 STOPSIGNAL。当你从这个镜像构建容器时,STOPSIGNAL 是默认的 15,除非你通过命令行覆盖它。
一个值得注意的 podman image inspect 选项如下:
--format——如上所示,这很有用,可以提取 json 中的特定字段。
使用 man podman-image-inspect 命令来获取关于该命令的信息。
一旦你对一个容器满意并将其提交为镜像,下一步就是与他人分享或者可能在另一个系统上运行它。你需要将镜像推送到其他类型的容器存储,通常是容器注册库。
2.2.4 推送镜像
在 Podman 中,你使用 podman 的 push 命令将镜像及其所有层从容器存储复制出来,并将其推送到其他形式的容器镜像存储,如容器注册库。Podman 支持几种不同类型的容器存储,它称之为传输。
容器传输
Podman 使用 containers/image 库(github.com/containers/image)来拉取和推送镜像。我把 containers/image 项目描述为在不同类型的容器存储之间复制镜像的库。正如你所看到的,其中一种存储是 containers/storage。
当推送一个镜像时,使用 transport:ImageName 格式指定 [destination]。如果没有指定传输方式,默认使用 docker(容器注册库)传输方式。
如我之前解释的,Docker 做的一件新颖的事情是发明了容器注册库的概念——基本上是一个包含容器镜像的网页服务器。docker.io、quay.io 和 Artifactory 网页服务器都是容器注册库的例子。Docker 工程团队定义了一个从容器注册库拉取和推送这些镜像的协议,我将其称为容器注册库或 docker 传输。
当我想运行一个镜像的容器时,我可以完全指定镜像名称,包括传输方式,如下面的命令所示:
$ podman run docker://registry.access.redhat.com/ubi8/httpd-24:latest echo hello
hello
对于 Podman,docker:// 传输是默认的;为了方便可以省略:
$ podman run registry.access.redhat.com/ubi8/httpd-24:latest echo hello
hello
在上一节中创建的 myimage 镜像是本地创建的,这意味着它没有与之关联的注册库。默认情况下,本地创建的镜像与 localhost 注册库相关联。你可以使用 podman 的 images 命令在 containers/storage 中查看镜像:
$ podman images
REPOSITORY TAG IMAGE ID CREATED SIZE
localhost/myimage latest 2c7e43d88038 46 hours ago 462 MB
registry.access.redhat
➥.com/ubi8/httpd-24 latest 8594be0a0b57 5 weeks ago 462 MB
registry.access.redhat
➥.com/ubi8 latest ad42391b9b46 5 weeks ago 234 MB
如果镜像与远程注册库相关联(例如,registry.access.redhat.com/ubi8),则可以不指定 [destination] 字段进行推送。相反,由于 localhost/myimage 没有与之关联的注册库,需要指定远程注册库(例如,quay.io/rhatdan):
$ podman push myimage quay.io/rhatdan/myimage
Getting image source signatures
Copying blob 164d51196137 done
Copying blob 8f26704f753c done
Copying blob 83310c7c677c done
Copying blob 654b3bf1361e [==================>-------------------] 82.0MiB / 162.7MiB
Copying blob e39c3abf0df9 [================>---------------------] 100.0MiB / 222.8MiB
注意:在执行 podman 的 push 命令之前,我使用 podman 的 login 登录到 quay.io/rhatdan 账户,这将在下一节中介绍。
在 push 命令完成后,如果其他用户有权访问这个容器注册库,镜像将可供他们拉取。表 2.3 描述了支持不同类型容器存储的传输方式。
表 2.3 Podman 支持的传输
| 传输 | 描述 |
|---|---|
| 容器注册库(Docker) | 默认传输。它引用存储在远程容器图像注册库中的容器图像。容器注册库是存储和共享容器图像的地方(例如,docker.io 或 quay.io)。 |
oci |
指向符合 Open Container Image Layout 规范的容器图像。清单和层 tarball 作为单独的文件位于本地目录中。 |
dir |
指向符合 Docker 图像布局的容器图像。它与 oci 传输非常相似,但使用传统的 Docker 格式存储文件。它是一种非标准化格式,主要用于调试或非侵入式容器检查。 |
docker-archive |
指向 Docker 图像布局中的容器图像,它被打包成一个 TAR 归档。 |
oci-archive |
指向符合 Open Container Image Layout 规范的图像,它被打包成一个 TAR 归档。它与 docker-archive 传输非常相似,但它以 OCI 格式存储图像。 |
docker-daemon |
指向存储在 Docker 守护进程内部存储中的图像。由于 Docker 守护进程需要 root 权限,Podman 必须由 root 用户运行。 |
container-storage |
指向位于本地容器存储中的图像。它不是一个传输,而更像是一种存储图像的机制。它可以用来将其他传输转换为 container-storage。Podman 默认使用 container-storage 来存储本地图像。 |
您想将图像推送到容器注册库,但如果您尝试推送,容器注册库会拒绝您的推送,因为您没有提供登录授权信息。您需要执行 podman login 来创建授权。
2.2.5 podman login:登录到容器注册库
在上一节中,我通过执行以下操作将图像推送到我的容器注册库:
$ podman push myimage quay.io/rhatdan/myimage
然而,我遗漏了一个关键步骤:使用正确的凭证登录到容器注册库。这是推送容器图像的必要步骤。它也是从私有注册库拉取容器图像所必需的。
要在本节中跟随操作,您需要在容器注册库中设置一个账户;有多个容器注册库可供选择。quay.io 和 docker.io 注册库都提供免费账户和存储。您的公司可能有一个私有注册库,在那里您也可以获得一个账户。
对于示例,我将继续使用我在 quay.io 的 rhatdan 账户。登录以获取您的凭证:
$ podman login quay.io
Username: rhatdan
Password:
Login Succeeded!
注意 Podman 命令会在注册库中提示您输入用户名和密码。podman login 命令有选项可以在命令行上传递用户名/密码信息,以避免提示,让您能够自动化登录过程。
要为用户存储认证信息,podman login 命令会创建一个 auth.json 文件。默认情况下,它存储在 /run/user/$UID/containers/auth.json 文件中:
cat /run/user/3267/containers/auth.json
{
"auths": {
"quay.io": {
"auth": "OBSCURED-BASE64-PASSWORD"
}
}
}
auth.json 文件包含你的注册表密码,以 Base64 编码的字符串形式;其中不涉及加密。因此,auth.json 文件需要受到保护。Podman 默认将文件存储在 /run 中,因为它是一个临时文件系统,当你注销或系统重启时会被销毁。/run/user/$UID/containers 目录对系统上的其他用户不可访问。
可以通过指定 --auth-file 选项来覆盖位置。或者,你可以使用 REGISTRY_AUTH_FILE 环境变量来修改其位置。如果两者都指定了,则使用 --auth-file 选项。所有容器工具都使用此文件来访问容器注册表。
可以多次运行 podman login 命令以登录多个注册表,并将登录信息存储在同一个授权文件的不同部分中。
注意 Podman 支持其他存储密码信息的机制。这些被称为 凭证助手。
使用完注册表后,你可以通过执行 podman logout 来注销。此命令会删除存储在 auth.json 文件中的缓存凭证:
$ podman logout quay.io
Removed login credentials for quay.io
一些值得注意的 podman login 和 logout 选项包括以下内容:
-
--username,(-u)—这提供了在登录注册表时使用的 Podman 用户名。 -
--authfile—这告诉 Podman 将授权文件存储在不同的位置。你也可以使用REGISTRY_AUTH_FILE环境变量来更改位置。 -
--all—这允许你注销所有注册表。
使用 man podman-login 和 man podman-logout 命令获取所有选项的信息。
注意当你将镜像推送到容器注册表时,你将 myimage 重命名为 quay.io/rhatdan/myimage:
$ podman push myimage quay.io/rhatdan/myimage
很好,如果只是有一个名为 quay.io/rhatdan/myimage 的本地镜像,那么你就可以直接执行
$ podman push quay.io/rhatdan/myimage
在下一节中,你将学习如何为镜像添加名称。
2.2.6 镜像标记
在本章前面,我指出本地创建的镜像使用 localhost 注册表创建。当你将容器提交为镜像或使用 podman build 命令构建镜像时,镜像会使用 localhost 注册表创建。Podman 有一个机制来为镜像添加额外的名称;它将这些名称称为标签,命令是 podman tag。
使用 podman images 命令,在 container/storage 中列出镜像(们):
$ podman images
REPOSITORY TAG IMAGE ID CREATED SIZE
localhost/myimage latest 2c7e43d88038 46 \hours ago 462 MB
registry.access.redhat
➥.com/ubi8/httpd-24 latest 8594be0a0b57 5 weeks ago 462 MB
registry.access.redhat
➥.com/ubi8 latest ad42391b9b46 5 weeks ago 234 MB
你希望最终计划发布的镜像被称为 quay.io/rhatdan/myimage。为了实现这一点,使用以下 podman tag 命令添加该名称:
$ podman tag myimage quay.io/rhatdan/myimage
现在再次运行 podman images 来检查镜像。你会看到名称现在是 quay.io/rhatdan/myimage。注意 localhost/myimage 和 quay.io/rhatdan/myimage 有相同的镜像 ID 2c7e43d88038:
$ podman images
REPOSITORY TAG IMAGE ID CREATED SIZE
localhost/myimage latest 2c7e43d88038 46 hours ago 462 MB
quay.io/rhatdan/myimage latest 2c7e43d88038 46 hours ago 462 MB
registry.access.redhat
➥.com/ubi8/httpd-24 latest 8594be0a0b57 5 weeks ago 462 MB
registry.access.redhat
➥.com/ubi8 latest ad42391b9b46 5 weeks ago 234 MB
由于这些图像具有相同的图像 ID,它们是具有多个名称的同一图像。现在您可以直接与 quay.io/rhatdan/myimage 交互。首先,您需要重新登录到 quay.io:
$ podman login --username rhatdan quay.io
Password:
Login Succeeded!
现在无需指定目标名称即可推送:
$ podman push quay.io/rhatdan/myimage
Getting image source signatures
...
Storing signatures
这非常简单。
让我们给之前使用的镜像添加一个版本号,1.0:
$ podman tag quay.io/rhatdan/myimage quay.io/rhatdan/myimage:
再次检查图像;注意myimage现在有三个不同的名称/标签。所有三个都具有相同的图像 ID 2c7e43d88038:
$ podman images
REPOSITORY TAG IMAGE ID CREATED SIZE
localhost/myimage latest 2c7e43d88038 46 hours ago 462 MB
quay.io/rhatdan/myimage 1.0 2c7e43d88038 46 hours ago 462 MB
quay.io/rhatdan/myimage latest 2c7e43d88038 46 hours ago 462 MB
registry.access.redhat
➥.com/ubi8/httpd-24 latest 8594be0a0b57 5 weeks ago 462 MB
registry.access.redhat
➥.com/ubi8 latest ad42391b9b46 5 weeks ago 234 MB
现在,您可以将myimage(应用程序)的 1.0 版本推送到注册库:
$ podman push quay.io/rhatdan/myimage:1.0
Getting image source signatures
Copying blob 8f26704f753c skipped: already exists
Copying blob e39c3abf0df9 skipped: already exists
Copying blob 654b3bf1361e skipped: already exists
Copying blob 83310c7c677c skipped: already exists
Copying blob 164d51196137 [--------------------------------------] 0.0b / 0.0b
Copying config 2c7e43d880 [--------------------------------------] 0.0b / 4.0KiB
Writing manifest to image destination
Storing signatures
用户可以拉取最新版本或 1.0 版本。稍后,当您构建应用程序的 2.0 版本时,您可以在注册库中存储这两个镜像。您可以在主机上同时运行应用程序的 1.0 和 2.0 版本。
使用网络浏览器(例如,Firefox、Chrome、Safari、Internet Explorer 或 Microsoft Edge)查看 quay.io 上的镜像。您可以在图 2.4 中看到 1.0 和最新版本:
$ web-browser quay.io/repository/rhatdan/myimage?tab=tags

图 2.4 quay.io 上myimage标签列表(quay.io/repository/rhatdan/myimage/?tab=tags)
现在您已经将镜像推送到容器注册库,您可能想通过删除镜像来释放您主目录中的存储空间。
注意:与常识相反,标签latest并不指向存储库中最新的镜像。它只是一个没有魔力的普通标签。更糟糕的是,因为它被用作未标记推送的镜像的默认标签,它可能指向任何随机的镜像版本。使用此标签,容器注册库中可能有比您本地容器存储空间中更新的镜像。因此,始终最好指定您想要使用的特定图像版本,而不是依赖latest。
2.2.7 删除镜像
随着时间的推移,镜像可能会占用大量的磁盘空间。因此,删除不再使用的镜像将是一个好主意。让我们首先列出本地镜像:
$ podman images
REPOSITORY TAG IMAGE ID CREATED SIZE
localhost/myimage 1.0 2c7e43d88038 46 hours ago 462 MB
quay.io/rhatdan/myimage 1.0 2c7e43d88038 46 hours ago 462 MB
quay.io/rhatdan/myimage latest 2c7e43d88038 46 hours ago 462 MB
registry.access.redhat
➥.com/ubi8/httpd-24 latest 8594be0a0b57 5 weeks ago 462 MB
registry.access.redhat
➥.com/ubi8 latest ad42391b9b46 5 weeks ago 234 MB
使用podman的rmi命令删除本地镜像:
$ podman rmi localhost/myimage
Untagged: localhost/myimage:latest
再次列出本地镜像,您会看到命令实际上并没有删除图像,只是从图像中删除了localhost标签。Podman 仍然有两个对同一图像 ID 的引用:图像的实际内容尚未删除。没有释放任何磁盘空间:
$ podman images
REPOSITORY TAG IMAGE ID CREATED SIZE
quay.io/rhatdan/myimage 1.0 2c7e43d88038 46 hours ago 462 MB
quay.io/rhatdan/myimage latest 2c7e43d88038 46 hours ago 462 MB
registry.access.redhat
➥.com/ubi8/httpd-24 latest 8594be0a0b57 5 weeks ago 462 MB
registry.access.redhat
➥.com/ubi8 latest ad42391b9b46 5 weeks ago 234 MB
您可以使用简短名称删除其他标签(参见第 2.2.8 节)。Podman 使用简短名称,并在本地存储中找到与简短名称匹配的第一个名称(无注册表),然后将其删除,这就是为什么我需要删除两次才能去除两个图像。除了latest之外的标签需要明确指定:
$ podman rmi myimage
Untagged: quay.io/rhatdan/myimage:latest
$ podman rmi myimage:1.0
Untagged: quay.io/rhatdan/myimage:1.0
Deleted: 2c7e43d88038669e8cdbdff324a9f9605d99697215a0d21c360fe8dfa8471bab
只有当最后一个标签被删除时,实际的磁盘空间才会被回收:
$ podman images
REPOSITORY TAG IMAGE ID CREATED SIZE
registry.access.redhat
➥.com/ubi8/httpd-24 latest 8594be0a0b57 5 weeks ago 462 MB
registry.access.redhat
➥.com/ubi8 latest ad42391b9b46 5 weeks ago 234 MB
或者,您可以通过指定图像 ID 来尝试删除图像:
$ podman rmi 14119a10abf4
Error: unable to delete image\
➥ "2c7e43d88038669e8cdbdff324a9f9605d99697215a0d21c360fe8dfa8471bab" by\
➥ ID with more than one tag ([quay.io/rhatdan/myimage:1.0\
➥ quay.io/rhatdan/myimage:latest]): please force removal
但这失败了,因为同一图像有多个标签。添加--force选项将删除图像及其所有标签:
$ podman rmi 14119a10abf4 --force
Untagged: quay.io/rhatdan/myimage:1.0
Untagged: quay.io/rhatdan/myimage:latest
Deleted: 2c7e43d88038669e8cdbdff324a9f9605d99697215a0d21c360fe8dfa8471bab
随着你的镜像大小和数量增长以及创建更多容器,确定哪些镜像不再需要变得更加困难。Podman 有另一个有用的命令—podman image prune—用于移除所有悬挂镜像。悬挂镜像是指不再与任何标签相关联或被任何容器使用的镜像。prune 命令也有 --all 选项,它移除所有当前未被任何容器使用的镜像,包括悬挂镜像:
$ podman image prune -a
WARNING! This command removes all images without at least one container \
➥ associated with them.
Are you sure you want to continue? [y/N] y
6d633c2626113fb4e5aa75babb2af39268948497893f7bb5b4c2043d7a986ba0
B9097177b416944cabdcfcab0e74a319223ad1acaed38ac57a262b2421732355
注意:没有运行容器时,使用 podman image prune 命令会移除所有本地镜像。这会释放主目录中的所有磁盘空间。你可以使用 podman system df 命令来显示 Podman 在主目录中使用的所有存储。
$ podman images
REPOSITORY TAG IMAGE ID CREATED SIZE
一些显著的 podman image prune 选项包括以下内容:
-
--all—这告诉 Podman 移除所有镜像,释放所有存储空间。运行在镜像上的容器不会被移除。 -
--force—这告诉 Podman 停止并移除任何正在运行的容器,并移除任何依赖于你试图删除的镜像的镜像。
使用 man podman-image-prune 命令获取有关所有选项的信息。
推送到注册库的镜像也可能因各种原因而被拉取,包括但不限于与他人共享应用程序、测试其他版本、恢复已删除的本地版本以及开发镜像的新版本。
2.2.8 拉取镜像
尽管你之前已经移除了所有本地镜像,但你仍然可以拉取之前推送到 quay.io/rhatdan/myimage 的镜像。Podman 有 podman pull 命令,可以从容器注册库(传输)拉取镜像到本地容器存储:
$ podman pull quay.io/rhatdan/myimage
Trying to pull quay.io/rhatdan/myimage:latest...
Getting image source signatures
Copying blob dfd8c625d022 done
Copying blob e21480a19686 done
Copying blob 68e8857e6dcb done
Copying blob 3f412c5136dd done
Copying blob fbfcc23454c6 done
Copying config 2c7e43d880 done
Writing manifest to image destination
Storing signatures
2c7e43d880382561ebae3fa06c7a1442d0da2912786d09ea9baaef87f73c29ae
输出看起来熟悉吗?你可能记得 2.1.2 节中 podman run 命令的类似输出:
$ podman run -d -p 8080:8080 --name myapp\
➥ registry.access.redhat.com/ubi8/httpd-24
Trying to pull registry.access.redhat.com/ubi8/httpd-24:latest...
Getting image source signatures
Checking if image destination supports signatures
Copying blob 296e14ee2414 skipped: already exists
Copying blob 356f18f3a935 skipped: already exists
Copying blob 359fed170a21 done
Copying blob 226cafc3a0c6 done
Writing manifest to image destination
Storing signatures
37a1d2e31dbf4fa311a5ca6453f53106eaae2d8b9b9da264015cc3f8864fac22
许多 Podman 命令在本地没有所需镜像时会隐式执行 podman pull 命令。
执行 podman images 命令会显示容器存储中的镜像,准备用于容器:
$ podman images
REPOSITORY TAG IMAGE ID CREATED SIZE
quay.io/rhatdan/myimage latest 2c7e43d88038 2 days ago 462 MB
到目前为止,你一直使用完整的名称作为镜像,例如 registry.access.redhat.com/ubi8/httpd-24 或 quay.io/rhatdan/myimage,但如果你像我一样不是打字高手,这可能会很痛苦。你真的需要一个方法通过短名称来引用镜像。
短名称和容器注册库
当 Docker 首次出现时,它们定义了一个镜像引用为存储镜像的容器注册库、仓库、镜像名称以及镜像的标签或版本的组合。在我们的示例中,我们一直在使用 quay.io/rhatdan/myimage。在表 2.4 中,你可以看到这个镜像名称的分解;注意,latest 标签是隐式使用的,因为未指定镜像版本。
表 2.4 容器镜像名称表
| 注册库 | 仓库 | 名称 | 标签 |
|---|---|---|---|
| quay.io | rhatdan | myimage | latest |
Docker 命令行内部已将 docker.io 注册表设置为唯一注册表,因此使得每个短镜像名称都指向 docker.io 上的镜像。还有一个特殊的仓库库,用于存储认证镜像。
所以,您不必输入
# docker pull docker.io/library/alpine:latest
您只需执行
# docker pull alpine
相反,如果您想从不同的注册表拉取镜像,您需要指定镜像的完整名称:
# docker pull registry.access.redhat.com/ubi8/httpd-24:latest
表 2.5 显示了短名使用的镜像名称与完全指定的镜像名称之间的区别。请注意,当使用短名时,注册表、仓库和标签都没有指定。
表 2.5 短名到容器镜像名称表
| 注册表 | 仓库 | 名称 | 标签 |
|---|---|---|---|
| alpine | |||
| docker.io | library | alpine | latest |
由于我懒惰且不喜欢输入额外的字符,我几乎总是使用短名。在 Podman 中,开发者不想将一个注册表,docker.io,硬编码到工具中。Podman 允许发行版、公司和您控制要使用的注册表,并能够配置多个注册表。同时,Podman 提供了对易于使用的短名的支持。
Podman 通常包含多个注册表定义,由打包 Podman 的发行版控制。您可以使用podman info命令查看为您的 Podman 安装定义了哪些注册表:
$ podman info
...
registries:
search:
- registry.fedoraproject.org
- registry.access.redhat.com
- docker.io
- quay.io
注册表列表可以在registries.conf文件中修改,该文件在 5.2.1 节中有所描述。
让我们使用这些命令来讨论安全方面的问题:
$ podman pull rhatdan/myimage
$ podman pull quay.io/rhatdan/myimage
从安全角度考虑,在从注册表中拉取镜像时始终指定完整镜像名称总是更好的。这样,Podman 可以保证它从指定的注册表拉取。想象一下,您正在尝试拉取 rhatdan/myimage。使用之前的搜索顺序,有人可能在 docker.io/rhatdan 上设置了一个账户,并诱使您错误地拉取 docker.io/rhatdan/myimage。
为了帮助防止这种情况,在第一次拉取镜像时,Podman 会提示您从配置的注册表中找到的镜像列表中选择一个确切的镜像:
$ podman create -p 8080:8080 ubi8/httpd-24
? Please select an image:
registry.fedoraproject.org/ubi8/httpd-24:latest
▸ registry.access.redhat.com/ubi8/httpd-24:latest
docker.io/ubi8/httpd-24:latest
quay.io/ubi8/httpd-24:latest
一旦您成功选择并拉取了镜像,Podman 会记录短名映射。在未来,当您使用此短名运行容器时,Podman 会使用短名映射来选择正确的注册表,而不会提示。
Linux 发行版还提供了最常用短名的映射,因为它们希望您从它们支持的注册表中拉取。您可以在 Linux 主机上的/etc/containers/registries.conf.d目录中找到这些短名配置文件。公司也可以将短名别名文件放入此目录:
$ cat /etc/containers/registries.conf.d/000-shortnames.conf
[aliases]
# centos
"centos" = "quay.io/centos/centos"
# containers
"skopeo" = "quay.io/skopeo/stable"
"buildah" = "quay.io/buildah/stable"
"podman" = "quay.io/podman/stable"
...
一些显著的podman pull选项包括以下内容:
-
--arch——这告诉 Podman 为不同的架构拉取镜像。例如,在我的 x86_64 机器上,我可以拉取 arm64 镜像。默认情况下,podmanpull命令会拉取本地架构的镜像。 -
--quiet(-q)— 这告诉 Podman 完成时不要打印所有进度信息。它只打印镜像 ID。
使用 man podman-pull 命令获取所有选项的信息。
我在这本书中提到了一些镜像,但可供选择的有成千上万。您需要一个机制来搜索这些镜像以找到完美的匹配。
2.2.9 搜索镜像
您可能不知道要运行或用作自己镜像基础的特定镜像的名称。Podman 提供了 podman search 命令,允许您在容器注册库中搜索匹配的名称:
$ podman search registry.access.redhat.com/httpd
INDEX NAME
➥ DESCRIPTION redhat.com
➥ registry.access.redhat.com/rhscl/httpd-24-rhel7
➥ Apache HTTP 2.4\ Server
redhat.com registry.access.redhat.com/ubi8/httpd-24\
➥ Platform for running Apache httpd 2.4 or bui...
redhat.com registry.access.redhat.com/rhscl/varnish-6-rhel7 Varnish\
➥ available as container is a base pla...
...
在这个例子中,我们正在搜索在 repository.registry.access.redhat.com 仓库中名称包含字符串 httpd 的镜像。
一些显著的 podman search 选项包括以下内容:
-
--no-trunc— 这告诉 Podman 显示镜像的完整描述。 -
--format— 这允许您自定义 Podman 显示的字段。
使用 man podman-search 命令获取所有选项的信息。
到目前为止,您已经看到了几种管理和操作容器镜像的方法,包括检查、推送、拉取和搜索它们。但您只能通过将其作为容器运行来查看镜像的内容。简化此过程的一种方法是将容器镜像挂载。
2.2.10 挂载镜像
通常,您可能想要检查容器镜像的内容,一种方法是启动一个运行中的容器内的 shell。但问题是,您用于检查容器镜像的工具可能不在容器内可用。此外,还存在一个安全风险,即容器中的应用程序可能是恶意的,这使得使用此容器不受欢迎。
为了帮助解决这些问题,Podman 提供了 podman image mount 命令,可以在不创建容器的情况下以只读模式挂载镜像的根文件系统。挂载的镜像将立即在主机系统上可用,允许您检查其内容。
现在尝试挂载您之前拉取的镜像:
$ podman mount quay.io/rhatdan/myimage
Error: cannot run command "podman mount" in rootless mode, must execute `podman unshare` first
出现此错误的原因是,无根模式不允许挂载镜像。您需要进入用户命名空间和单独的挂载命名空间。第五章解释了大多数无根 Podman 命令在执行时如何进入用户命名空间和挂载命名空间。现在,只需知道 podman unshare 命令进入用户和挂载命名空间,并在您执行 shell 的 exit 命令时关闭即可。
注意:名称 unshare 来自 Linux 系统调用 unshare (man 2 unshare)。Linux 还包括一个名为 unshare 的工具 (man 1 unshare),它允许你手动创建命名空间。另一个名为 nsenter 的低级工具,或称为命名空间进入 (man 1 nsenter),允许你将进程加入到不同的命名空间中。Podman unshare 使用相同的内核功能。它简化了创建和配置命名空间以及将进程插入命名空间的过程。
podman 的 unshare 命令将你置于一个 # 提示符,在那里你可以实际挂载一个镜像:
$ podman unshare
#
挂载镜像,并将挂载的文件系统位置保存在一个环境变量中:
# mnt=$(podman image mount quay.io/rhatdan/myimage)
现在,你实际上可以检查镜像的内容。让我们在终端上打印一个文件的目录:
# cat $mnt/var/www/html/index.xhtml
<html>
<head>
</head>
<body>
<h1>Hello World</h1>
</body>
</html>
当你完成时,卸载镜像,并退出 unshare 会话:
# podman image unmount quay.io/rhatdan/myimage
# exit
注意:你已经检查了大约一半的 podman image 子命令,可以说是最常用的。请参考 Podman 的 man 页面以获取这些和其他 podman image 命令子命令的完整解释:$ man podman-image。
现在你对容器和镜像有了更好的理解,下一步重要的步骤是更新你的镜像。主要原因包括需要更新你的应用程序以及你使用的基镜像的新版本可用。你可以编写脚本来手动运行构建镜像的命令,但幸运的是,Podman 优化了这一体验。
2.3 构建镜像
到目前为止,你一直在处理已经创建并上传到容器注册库的镜像。创建容器镜像的过程称为 构建。
在构建容器镜像时,你不仅要管理你的应用程序,还要管理该应用程序使用的镜像内容。在容器出现之前,你将应用程序作为 RPM 或 DEB 软件包分发,然后由发行版负责确保操作系统的其他部分保持最新和安全。但在容器世界中,容器镜像包括了应用程序以及操作系统的子集。保持镜像内容最新和安全是开发者的责任。
我的同事 Scott McCarty(smccarty@redhat.com,@fatherlinux)有一句话,“容器镜像不像葡萄酒那样越陈越香,更像奶酪。随着镜像变老,它变得越来越臭。”
这意味着如果开发者没有跟上安全更新,镜像中的漏洞数量将以惊人的速度增长。幸运的是,对于开发者来说,Podman 有一个特殊的机制来帮助你为应用程序构建镜像。podman 的 build 命令使用 Buildah 工具 (github.com/containers/buildah) 作为库来构建容器镜像;Buildah 的内容在附录 A 中介绍。
podman build 使用一种特殊的文本文档,称为 Containerfile 或 Dockerfile,来自动化容器镜像的构建。该文档列出了构建容器镜像所使用的命令。
注意:Dockerfile 及其语法的概念最初是为 Docker 工具创建的,该工具由 Docker, Inc. 开发。Podman 默认使用 Containerfile 作为名称,它使用完全相同的语法。Dockerfile 也支持作为向后兼容。Docker 构建命令默认不支持 Containerfile,但可以使用 Containerfile。你可以指定 -f 选项:# docker build -f Containerfile.
2.3.1 Containerfile 或 Dockerfile 的格式
Containerfiles 包含许多指令。我将这些指令分为两类,一类是向容器镜像添加内容,另一类是描述和记录如何使用该镜像。
向镜像添加内容
回想一下,在第 1.1.2 节中,我描述了容器镜像为一个类似于 Linux 系统上的根目录的磁盘上的目录。这个目录被称为 rootfs。容器作业中的许多指令都是向这个 rootfs 添加内容。这个 rootfs 最终包含创建你的容器镜像所使用的所有内容。
每个 Containerfile 都必须包含一个FROM行。FROM行指定了新镜像基于的镜像,通常称为基础镜像。podman build 命令支持一个名为scratch的特殊镜像,这意味着以无内容的方式开始你的镜像。当 Podman 看到指定的FROM scratch指令时,它只是在容器存储中为空根文件系统分配空间,然后可以使用COPY来填充根文件系统。更常见的是,FROM指令使用现有的镜像。例如,FROM registry.access.redhat.com/ubi8会导致 Podman 从 registry.access.redhat.com 容器注册库中拉取 ubi8 镜像并将其复制到容器存储中。podman build 拉取与你在第 2.2.8 节中学习的podman pull命令相同的镜像。当镜像被拉取时,Podman 使用容器存储在根文件系统目录上挂载镜像,使用类似于 OverlayFS 的写时复制文件系统,这样其他指令就可以开始添加内容。这个镜像成为根文件系统的底层。
COPY 指令通常用于将文件、目录或 tarball 从本地主机复制到新创建的 rootfs 中。RUN 指令是 Containerfile 中最常用的指令之一。RUN 指令告诉 Podman 在镜像上实际运行一个容器。像 DNF/YUM 和 apt-get 这样的包管理工具被用来将发行版的软件包安装到你的新镜像中。RUN 指令在容器镜像中运行任何命令作为容器。podman build 命令使用与 podman run 命令相同的权限约束来运行命令。
例如,假设您想向容器镜像添加ps命令;您可以创建如下所示的指令。RUN命令执行容器,更新基础镜像中的所有包,然后安装包含ps命令的procps-ns包。最后,容器化命令执行yum来自动清理,从而从容器镜像中移除冗余:
RUN yum -y update; yum -y install procps-ng; yum -y clean all
在创建容器镜像时,向容器镜像添加内容只是您需要完成的一半工作。您还需要描述和记录其他用户下载并运行您的镜像时如何使用该镜像。
记录如何使用镜像
回想一下,在 1.1.2 节中,我也描述了包含镜像规范的 JSON 文件。该规范描述了容器镜像的运行方式、命令、运行它的用户以及其他镜像要求。Containerfile 也支持许多指令,这些指令告诉 Podman 如何运行容器。以下是一些指令:
-
ENTRYPOINT和CMD指令——这些指令为用户使用 Podmanrun执行镜像时默认要执行的命令配置了镜像。CMD是实际要运行的命令。ENTRYPOINT可以使整个镜像作为一个单独的命令执行。 -
ENV指令——此指令设置 Podman 在镜像上运行容器时默认的环境变量。 -
EXPOSE指令——此指令记录 Podman 在容器中基于镜像暴露的网络端口。如果您执行podman run --publish-all ...,Podman 会在镜像内部查找EXPOSE网络端口并将它们连接到主机。
表 2.6 解释了在 Containerfile 中用于向容器镜像添加内容的指令。
表 2.6 更新镜像的 Containerfile 指令
| 指令示例 | 说明 |
|---|---|
FROM quay.io/rhatdan/myimage |
设置后续指令的基础镜像。Containerfile 必须以FROM作为其第一条指令。FROM可以在单个 Containerfile 中多次出现以创建多个构建阶段。 |
ADD start.sh /usr/bin/start.sh |
将新文件、目录或远程文件 URL 复制到容器文件系统的指定路径。 |
COPY start.sh /usr/bin/start.sh |
将文件复制到容器文件系统的指定路径。 |
RUN dnf -y update |
在当前镜像之上执行新层的命令,并提交结果。提交的镜像用于 Containerfile 中的下一步。 |
VOLUME /var/lib/mydata |
创建具有指定名称的挂载点,并将其标记为包含来自本地主机或其他容器的外部挂载卷。有关卷的更多信息,请参阅第三章。 |
表 2.7 解释了在 Containerfile 中使用的指令,这些指令用于将信息填充到 OCI 运行时规范中,以便容器引擎(如 Podman)了解镜像以及如何运行镜像。你可以在 containerfile(5) 手册页中找到有关 Containerfile 的更多信息。
表 2.7 定义 OCI 运行时规范的 Containerfile 指令
| 指令示例 | 说明 |
|---|---|
CMD /usr/bin/start.sh |
指定在从该镜像启动容器时运行的默认命令。如果没有指定 CMD,则继承父镜像的 CMD。请注意,RUN 和 CMD 非常不同。RUN 在构建过程中运行命令,而 CMD 仅在用户启动镜像且未指定命令时使用。 |
ENTRYPOINT “/bin/sh -c” |
允许你配置容器以可执行文件的方式运行。当向 podman run 传递参数时,ENTRYPOINT 指令不会被覆盖。这允许将参数传递给入口点——例如,podman run <image> -d 将 -d 参数传递给 ENTRYPOINT。 |
ENV foo=”bar” |
在镜像构建和容器执行期间添加一个环境变量。 |
EXPOSE 8080 |
宣布容器化应用程序将要暴露的端口。这实际上不会映射或打开任何端口。 |
LABEL Description=”Web browser which displays Hello World” |
向镜像添加元数据。 |
MAINTAINER Daniel Walsh |
设置生成镜像的 Author 字段。 |
STOPSIGNAL SIGTERM |
设置发送给容器以退出时的默认停止信号。信号可以是有效的无符号数字或格式为 SIGNAME 的信号名称。 |
USER apache |
设置用于任何 RUN、CMD 和 ENTRYPOINT 指定的用户名(或 UID)和组名(或 GID)。 |
ONBUILD |
向镜像添加一个触发指令,在稍后使用该镜像作为其他构建的基础时执行。 |
WORKDIR /var/www/html |
设置 RUN、CMD、ENTRYPOINT 和 COPY 指令的工作目录。如果该目录不存在,将会创建它。 |
提交镜像
当 podman build 完成处理 Containerfile 后,它会提交镜像,使用与你在 2.1.9 节中学习的 podman commit 相同的代码。基本上,Podman 将根文件系统中新内容与通过 FROM 指令拉取的基本镜像之间的所有差异打包成 TAR 文件。Podman 还会提交 JSON 文件,并将其保存为容器存储中的镜像。现在你可以采取构建容器化应用程序的步骤,并使用 Containerfile 和 Podman 构建来自动化它们。
小贴士 使用 --tag 选项通过 podman build 命令命名你正在创建的新镜像。这告诉 Podman 以与 podman tag 命令相同的方式将指定的标签或名称添加到容器存储中的镜像。
2.3.2 自动化构建我们的应用程序
首先,创建一个目录来放置您的 Containerfile 以及任何其他用于容器镜像的内容。这个目录被称为上下文目录:
mkdir myapp
接下来,在 myapp 目录中创建您计划在容器化应用程序中使用的 index.xhtml 文件:
$ cat > myapp/index.xhtml << _EOF
<html>
<head>
</head>
<body>
<h1>Hello World</h1>
</body>
</html>
_EOF
接下来,在 myapp 目录中创建一个简单的 Containerfile,用于构建您的应用程序。Containerfile 的第一行是 FROM 指令,用于拉取您作为基础镜像的 ubi8/httpd-24 镜像。然后添加一个 COPY 命令,将 index.xhtml 文件复制到镜像中。COPY 指令告诉 Podman 将 index.xhtml 文件从上下文目录(./myapp)复制出来,并将其复制到镜像内的 /var/www/html/index.xhtml 文件中:
$ cat > myapp/Containerfile << _EOF
FROM ubi8/httpd-24
COPY index.xhtml /var/www/html/index.xhtml
_EOF
最后,使用 podman build 构建您的容器化应用程序。指定 --tag (-t) 来命名镜像为 quay.io/rhatdan/myimage。您还需要指定上下文目录 ./myapp:
$ podman build -t quay.io/rhatdan/myimage ./myapp
STEP 1/2: FROM ubi8/httpd-24
STEP 2/2: COPY index.xhtml /var/www/html/index.xhtml
COMMIT quay.io/rhatdan/myimage
--> f81b8ace4f1
Successfully tagged quay.io/rhatdan/myimage:latest
F81b8ace4f134d08cedb20a9156ae727444ae4d4ec1ceb3b12d3aff23d18128b
当 podman build 命令完成后,它会提交镜像并使用 quay.io/rhatdan/myimage 名称对其进行标记。现在它已准备好使用 podman push 命令将其推送到容器注册库。
现在,您可以设置 CI/CD 系统,甚至一个简单的 cron 作业,以定期构建和替换 myapplication:
$ cat > myapp/automate.sh << _EOF
#!/bin/bash
podman build -t quay.io/rhatdan/myimage ./myapp
podman push quay.io/rhatdan/myimage
_EOF
$ chmod +x myapp/automate.sh
添加一些测试脚本,以确保在替换上一个版本之前,您的应用程序按预期工作。让我们看看构建的镜像:
$ podman images
REPOSITORY TAG IMAGE ID CREATED SIZE
quay.io/rhatdan/myimage latest f81b8ace4f13 2 minutes ago 462 MB
<none> <none> 2c7e43d88038 2 days ago 462 MB
registry.access.redhat
➥.com/ubi8/httpd-24 latest 8594be0a0b57 5 weeks ago 462 MB
注意,quay.io/rhatdan/myimage 的旧版本仍然存在于容器存储中,其镜像 ID 为 2c7e43d88038,但现在其 REPOSITORY 和 TAG 都是 <none> <none>。这样的镜像被称为悬挂镜像。由于我已使用 podman build 命令创建了一个新的 quay.io/rhatdan/myimage 版本,因此旧镜像失去了那个名称。您仍然可以使用带有镜像 ID 的 Podman 命令,或者如果新镜像不起作用,只需使用 podman tag 命令将旧镜像重命名为 quay.io/ rhatdan/myimage。如果新镜像运行正确,您可以使用 podman rmi 命令删除旧镜像。这些 <none><none> 镜像往往会随着时间的推移而积累,浪费空间,但您可以使用 podman image prune 命令定期删除它们。
podman build 实际上需要一个章节,甚至一本书来专门介绍。人们使用这里简要描述的命令以数千种不同的方式构建镜像。
--tag 是一个显著的 podman build 选项,用于指定镜像的标签或名称。请记住,您可以在使用 2.2.6 节中使用的 podman tag 命令创建镜像后添加额外的名称。使用 man podman-build 命令获取有关所有选项的信息(见表 2.8)。
表 2.8 Podman 镜像命令
| 命令 | 手册页 | 描述 |
|---|---|---|
build |
podman-image-build(1) |
使用容器文件中的指令构建镜像 |
diff |
podman-image-diff(1) |
检查镜像文件系统的更改 |
exists |
podman-image-exists(1) |
检查镜像是否存在 |
history |
podman-image-history(1) |
显示指定镜像的历史记录 |
import |
podman-image-import(1) |
从 tarball 导入以创建文件系统镜像 |
inspect |
podman-image-inspect(1) |
显示镜像的配置 |
list |
podman-image-list(1) |
列出所有镜像 |
load |
podman-image-load(1) |
从 tarball 加载镜像 |
mount |
podman-image-mount(1) |
挂载镜像的根文件系统 |
prune |
podman-image-prune(1) |
删除未使用的镜像 |
pull |
podman-image-pull(1) |
从仓库拉取镜像 |
push |
podman-image-push(1) |
将镜像推送到仓库 |
rm |
podman-image-rm(1) |
删除镜像 |
save |
podman-image-save(1) |
将镜像保存到存档 |
scp |
podman-image-scp(1) |
安全地将镜像复制到其他容器/存储 |
search |
podman-image-search(1) |
在仓库中搜索镜像 |
sign |
podman-image-sign(1) |
签名镜像 |
tag |
podman-image-tag(1) |
向本地镜像添加额外的名称 |
tree |
podman-image-tree(1) |
以树形格式打印镜像的层层次结构 |
trust |
podman-image-trust(1) |
管理容器镜像信任策略 |
unmount |
podman-image-unmount(1) |
卸载镜像的根文件系统 |
untag |
podman-image-untag(1) |
从本地镜像中移除一个名称 |
摘要
-
Podman 简单的命令行界面使得与容器的工作变得容易。
-
Podman 的
run、stop、start、ps、inspect、rm和commit都是用于处理容器的命令。 -
Podman 的
pull、push、login和rmi是用于处理镜像并通过容器仓库共享它们的工具。 -
Podman 的
build命令是自动化构建容器镜像的一个强大工具。 -
Podman 的命令行界面基于 Docker CLI,并完全支持它,这使得我们可以告诉人们只需将 Docker 别名为 Podman。
-
Podman 有额外的命令和选项来支持更高级的概念,如
podmanimagemount。
3 卷
本章涵盖
-
使用卷将容器化应用程序的数据隔离开来
-
通过卷将主机内容共享到容器中
-
使用用户命名空间和 SELinux 的卷
-
将卷嵌入到容器镜像中
-
探索不同类型的卷和卷命令
到目前为止,您一直在使用的容器将所有内容都包含在容器镜像中。正如我在第一章中描述的,与传统的容器共享的唯一要求是 Linux 内核。您需要将应用程序数据与应用程序隔离开来的原因有很多,包括以下内容:
-
避免嵌入数据库等应用程序的实际数据。
-
使用相同的容器镜像运行多个环境。
-
通过卷直接写入文件系统,从而减少开销并提高存储读写性能,因为容器使用 overlay 或 fuse-overlayfs 文件系统来挂载它们的层。Overlay是一种分层文件系统,这意味着内核需要完全复制前一层来创建新层,而 fuse-overlayfs 将每个读写操作从内核空间切换到用户空间,然后再切换回来。所有这些都造成了相当大的开销。
-
通过网络存储共享内容。
注意bind挂载会在文件系统中的不同位置重新挂载文件层次结构的一部分。bind挂载中的文件和目录与原始文件相同(有关bind挂载的解释,请参阅mount命令的手册页)。bind挂载允许相同的内容在两个地方可访问,而无需任何额外的开销。重要的是要理解bind不会复制数据或创建新数据。
支持卷也增加了复杂性,尤其是在安全性方面。容器的大多数安全特性都阻止容器进程访问容器镜像之外的文件系统。在本章中,您将了解 Podman 允许您绕过这些障碍的方法。
3.1 使用容器卷
让我们回到您的容器化应用程序。到目前为止,您只是直接将 Web 应用程序数据嵌入到容器文件系统中。回想一下,在 2.1.8 节中,您使用了podman exec命令来修改容器内的 Hello World index.xhtml 数据:
$ podman exec -i myapp bash -c 'cat > /var/www/html/index.xhtml' << _EOF
<html>
<head>
</head>
<body>
<h1>Hello World</h1>
</body>
</html>
_EOF
您通过允许用户为网络服务提供自己的内容或可能是在线更新网络服务,使容器化镜像更加灵活。同时,虽然这种方法是可行的,但它容易出错且不可扩展;这就是卷派上用场的地方。
Podman 允许您通过podman run命令使用--volume (-v)选项将主机文件系统内容挂载到容器中。
--volume HOST-DIR:CONTAINER-DIR选项告诉 Podman 将主机上的HOST-DIR绑定挂载到容器中的CONTAINER-DIR。Podman 还支持其他类型的卷,但在这个部分,我将专注于bind挂载卷。
有可能在一个选项中挂载文件或目录。主机上内容的变化将在容器内看到。同样,如果容器进程更改容器内的内容,这些更改将在主机上看到。
让我们来看一个例子。在你的主目录中创建一个名为 html 的目录,然后在其中创建一个新的 html/index.xhtml 文件:
$ mkdir html
$ cat > html/index.xhtml << _EOF
<html>
<head>
</head>
<body>
<h1>Goodbye World</h1>
</body>
</html>
_EOF
现在,使用选项 -v ./html:/var/www/html 启动容器:
$ podman run -d -v ./html:/var/www/html:ro,z -p 8080:8080
quay.io/rhatdan/myimage
94c21a3d8fda740857abc571469aaaa181f4db27a464ceb6743c4a37fb875772
注意 --volume 选项中的额外 :ro,z 字段。ro 选项告诉 Podman 以只读模式挂载卷。只读挂载意味着容器内的进程不能修改 /var/www/html 下的任何内容,而主机上的进程仍然可以修改内容。Podman 默认所有卷挂载为读写模式。z 选项告诉 Podman 将内容重新标记为共享标签,以便 SELinux 使用(见 3.1.2 节)。
现在,你已经启动了容器,打开网页浏览器,导航到 localhost:8080 以确保已发生更改(见图 3.1)。
$ web-browser localhost:8080

图 3.1 网页浏览器窗口连接到带有挂载卷的 myimage Podman 容器
现在,你可以关闭并删除你刚刚创建的容器。删除容器不会影响内容。以下命令删除最新的(--latest)容器,即你的容器。--force 选项告诉 Podman 停止容器然后删除它:
$ podman rm --latest --force
最后,使用此命令删除内容:
$ rm -rf html
注意:--latest 选项在 Mac 和 Windows 上不可用。你必须指定容器名称或 ID。远程模式在第九章中解释,Mac 和 Windows 上的 Podman 在附录 E 和 F 中解释。
3.1.1 命名卷
在第一个卷示例中,你在磁盘上创建了一个目录,然后将其挂载到容器中。同样,只要你对该文件或目录有读取权限,你就可以将其挂载到容器中。
持久化 Podman 容器数据的另一种机制称为 volume。你可以使用 podman volume create 命令创建其中一个。在以下示例中,你将创建一个名为 webdata 的 volume:
$ podman volume create webdata
webdata
Podman 默认创建本地命名的卷,存储在容器存储目录中。你可以使用以下命令检查卷并查找其挂载点:
$ podman volume inspect webdata
[
{
"Name": "webdata",
"Driver": "local",
"Mountpoint":
➥ "/home/dwalsh/.local/share/containers/storage/volumes/webdata/_data",
"CreatedAt": "2021-10-11T14:10:48.741367132-04:00",
"Labels": {},
"Scope": "local",
"Options": {}
}
]
Podman 实际上在你的本地容器存储中创建一个目录,/home/dwalsh/ .local/share/containers/storage/volumes/webdata/_data,以存储卷的内容。你可以在该目录中创建来自主机的内容:
$ cat > /home/dwalsh/.local/share/containers/storage/volumes/webdata/_
data/index.xhtml << _EOL
<html>
<head>
</head>
<body>
<h1>Goodbye World</h1>
</body>
</html>
_EOL
现在,你可以使用这个卷来运行 myimage 应用程序:
$ podman run -d -v webdata :/var/www/html:ro,z -p 8080:8080 quay.io/rhatdan/myimage
0c8eb612831f8fe22438d73d801e5bb664ec3b1d524c5c10759ee0049061cb6b
现在刷新网页浏览器,以确保在主机目录中创建的文件显示“再见,世界”(见图 3.2)。

图 3.2 网页浏览器窗口连接到带有命名卷挂载的 myimage Podman 容器
命名卷可以同时用于多个容器,并且即使容器被移除后它们也会保留。如果你完成了命名卷和容器的使用,你可以首先停止容器,无需等待进程完成:
$ podman stop -t 0 0c8eb61283
然后使用podman volume rm命令移除卷。注意--force选项,它告诉 Podman 移除卷以及所有依赖该卷的容器:
$ podman volume rm --force webdata
现在,你可以通过执行volume list命令来确保卷已被移除:
$ podman volume list
如果在执行podman run命令之前不存在命名卷,它将被自动创建。在以下示例中,你将指定webdata1作为命名卷的名称,然后列出卷:
$ podman run -d -v webdata1:/var/www/html:ro,z -p 8080:8080\
➥ quay.io/rhatdan/myimage
58ccaf37958496322e34cd933cd4dd5a61ab06c5ba678beb28fdc29cfb81f407
$ podman volume list
DRIVER VOLUME NAME
local webdata1
当然,这个卷是空的。移除webdata1卷和容器:
$ podman volume rm --force webdata1
Podman 还支持其他类型的卷。它使用卷插件的概念,以便第三方可以提供卷;有关更多信息,请参阅podman-volume-create手册页。
Podman 还有其他有趣的卷功能。podman volume export命令将卷的所有内容导出到一个外部的 TAR 归档中。这个归档可以被复制到其他机器,使用podman volume import命令在另一台机器上重新创建卷。现在你了解了卷的处理方法,是时候深入了解卷选项了。
3.1.2 卷挂载选项
你在本章中一直在使用卷挂载选项。ro选项告诉 Podman 挂载只读卷,而小写z选项告诉 Podman 使用 SELinux 标签重新标记内容,这将允许多个容器在卷中读写:
$ podman run -d -v ./html:/var/www/html:ro,z -p 8080:8080 quay.io/rhatdan/myimage
Podman 支持一些其他有趣的卷选项。
U 卷选项
有时当你运行无根容器时,你需要一个由容器用户拥有的卷。想象一下,如果你的应用程序需要允许 Web 服务器向卷写入。在你的容器中,Apache Web 服务器进程(httpd)以apache(UID==60)用户身份运行。你的主目录中的 html 目录属于你的 UID,这意味着在容器内部属于 root。内核不允许以UID==60运行的容器进程更改属于 root 的目录。你必须将卷的所有权设置为UID==60。
在无根容器中,容器的 UID 通过用户命名空间进行偏移。我的用户命名空间映射如下所示:
$ podman unshare cat /proc/self/uid_map
0 3267 1
1 100000 65536
容器内的UID==0是我的UID 3267,UID 1==100000,UID 2==10000 ... UID60==100059,这意味着我需要将 html 目录的所有权设置为100059。
我可以相当简单地使用podman unshare命令来完成这项操作,如下所示:
$ podman unshare chown 60:60 ./html
现在一切正常。这个问题的一个问题是,我需要做一些心理体操来弄清楚容器将使用哪个 UID。
许多容器镜像都定义了默认的 UID。mariadb镜像就是这样一个例子;它以mysql用户运行,UID=999:
$ podman run docker.io/mariadb grep mysql /etc/passwd
mysql:x:999:999::/home/mysql:/bin/sh
如果你创建了一个用于数据库的卷,你需要弄清楚UID=999在用户命名空间中的映射。在我的系统中这是UID=100998。
Podman 为此提供了U命令选项。U选项告诉 Podman 递归地更改源卷的所有权(chown),使其与容器执行的默认 UID 相匹配。
通过首先创建数据库目录来尝试一下。注意家目录中的目录是由你的用户拥有的:
$ mkdir mariadb
$ ls -ld mariadb/
drwxrwxr-x. 1 dwalsh dwalsh 0 Oct 23 06:55 mariadb/
现在运行带有--user mysql的mariadb容器,并使用:U选项将./mariadb 目录绑定挂载到/var/lib/mariadb。注意,现在目录是由mysql用户拥有的:
$ podman run --user mysql -v ./mariadb:/var/lib/mariadb:U \
➥ docker.io/mariadb ls -ld /var/lib/mariadb
drwxrwxr-x. 1 mysql mysql 0 Oct 23 10:55 /var/lib/mariadb
如果你再次查看主机上的 mariadb 目录,你会看到它现在由UID 100998或UID 999映射到你的用户命名空间中的任何UID:
$ ls -ld mariadb/
drwxrwxr-x. 1 100998 100998 0 Oct 23 06:55 mariadb/
用户命名空间并不是你需要绕过的唯一安全机制,与无根容器一起使用时,SELinux 虽然对容器安全很有帮助,但在处理卷时可能会引起一些问题。
SELinux 卷选项
在我看来,SELinux 是保护文件系统免受恶意容器进程侵害的最佳机制。多年来,通过 SELinux 阻止了多次容器逃逸(有关 SELinux 的更多信息,请参阅第 10.8 节)。
正如我之前解释的那样,卷会将文件从操作系统泄露到容器中,从 SELinux 的角度来看,这些文件和目录必须正确标记,否则内核会阻止访问。
你在本章中一直使用的带下划线的z命令选项告诉 Podman 递归地使用可以由所有容器从 SELinux 角度读取和写入的标签重新标记源目录中的所有内容。如果卷不会由多个容器使用,使用带下划线的z选项并不是你想要的。如果另一个敌对的容器逃逸了限制,它可能能够访问这些数据并对其进行读写。Podman 提供了一个大写Z选项,告诉 Podman 以这种方式递归地重新标记内容,这样只有容器内的进程可以读取/写入内容。
在这两种情况下,你都重新标记了目录的内容。如果目录被指定用于容器使用,重新标记效果很好。有时你可能想使用容器来检查系统特定目录中的内容——例如,如果你想运行一个容器来检查/var/log中的所有日志或检查所有家目录(/home/dwalsh)。
注意:在主目录上使用此选项可能会对系统产生灾难性的影响,因为它会递归地重新标记目录中的所有内容,好像数据是容器私有的。其他受限域将无法使用错误标记的数据。
对于这些情况,您需要禁用 SELinux 强制执行以允许容器使用卷。Podman 提供了命令选项 --security-opt label=disable 来禁用单个容器的 SELinux 支持,基本上是以 SELinux 视角使用 未限制 标签运行容器:
$ podman run --security-opt label=disable -v /home/dwalsh:/home/dwalsh -p\
➥ 8080:8080 quay.io/rhatdan/myimage
表 3.1 列出并描述了 Podman 中所有可用的挂载选项。
表 3.1 卷挂载选项
| 卷选项 | 描述 |
|---|---|
nodev |
防止容器进程在卷上使用字符或块设备。 |
noexec |
防止容器进程直接在卷上的任何二进制文件上执行。 |
nosuid |
防止 SUID 应用程序在卷上更改它们的权限。 |
O |
使用 overlay 文件系统将主机上的目录作为临时存储挂载。当容器执行完成后,对挂载点的修改将被销毁。此选项对于将主机上的软件包缓存共享到容器中以加快构建速度非常有用。 |
| [r]shared|``[r]slave|``[r]private|``[r]unbindable | 指定挂载传播模式。挂载传播控制挂载更改如何在挂载边界之间传播:
-
private(默认)—在容器内完成的任何挂载在主机上不可见,反之亦然。 -
shared—在该卷内部容器中完成的挂载将在主机上可见,反之亦然。 -
slave—在主机上在该卷下完成的挂载将在容器内可见,但反之则不然。 -
unbindable—私有模式的非绑定版本。
前缀 r 代表 递归的,意味着挂载点下方的任何挂载也将以相同的方式处理。 |
rw|ro |
以只读 (ro) 或读-写 (rw) 模式挂载卷。默认情况下,读/写是隐含的。 |
|---|---|
U |
根据容器内的 UID 和 GID 使用正确的主机 UID 和 GID。使用时请谨慎,因为这将修改主机文件系统。 |
z|Z |
在共享卷上重新标记文件对象。选择 z 选项将卷内容标记为在多个容器之间共享。选择 Z 选项将内容标记为非共享且私有。 |
有关更多信息,请参阅 mount 和 mount_namespaces(7) 的手册页。
大多数时候,简单的 --volume 选项就足以将卷挂载到容器中。随着时间的推移,对新挂载选项的需求变得越来越复杂,因此添加了一个名为 --mount 的新选项。
3.1.3 podman run - -mount 命令选项
podman run --mount 选项与底层 Linux 挂载命令非常相似。它允许您指定挂载命令理解的所有挂载选项;Podman 直接将它们传递给内核。
当前支持的唯一挂载类型是 bind、volume、image、tmpfs 和 devpts。(有关更多信息,请参阅 podman-mount(1) 手册页。)
卷和挂载是保持数据与容器镜像分离的绝佳方式。在大多数情况下,容器镜像应该被视为只读的,任何需要写入或与应用程序不特定的数据都应该通过卷存储在容器镜像之外。在许多情况下,通过保持数据分离可以获得更好的性能,因为读写不会产生写时复制的文件系统开销。这些挂载还使得使用不同的数据(表 3.2)与相同的容器镜像变得更容易。
表 3.2 Podman 卷命令
| 命令 | 手册页 | 描述 |
|---|---|---|
create |
podman-volume-create(1) |
创建一个新的卷。 |
exists |
podman-volume-exists(1) |
检查卷是否存在。 |
export |
podman-volume-export(1) |
将卷的内容导出到一个 tar 包中。 |
import |
podman-volume-import(1) |
将 tar 包解压到卷中。 |
inspect |
podman-volume-inspect(1) |
显示卷的详细信息。 |
list |
podman-volume-list(1) |
列出所有卷。 |
prune |
podman-volume-prune(1) |
删除所有未使用的卷。 |
rm |
podman-volume-rm(1) |
删除一个或多个卷。 |
摘要
-
卷对于将容器使用的数据与应用程序镜像内的数据分离非常有用。
-
卷将文件系统的部分挂载到容器环境中,这意味着需要修改像 SELinux 和用户命名空间这样的安全相关设置,以允许访问。
4 个 Pod
本章涵盖
-
Pod 简介
-
在 pod 内管理多个容器
-
使用 Pod 与卷
Podman 是 Pod Manager 的简称。pod 是 Kubernetes 项目推广的一个概念;它是一组一个或多个容器,为了共同目的而协同工作,并共享相同的命名空间和 cgroups(资源限制)。此外,Podman 确保在 SELinux 机器上,pod 内的所有容器进程共享相同的 SELinux 标签。这意味着它们可以从 SELinux 角度协同工作。
4.1 运行 pod
Podman pod(见图 4.1),就像 Kubernetes Pods 一样,始终包含一个名为 infra 的容器——有时称为 pause 容器(不要与第 5.2 节中提到的无根暂停容器混淆)。infra 容器仅保留内核中的命名空间和 cgroups,允许容器在 pod 内来去。当 Podman 向 pod 添加容器时,它会将容器进程添加到 cgroups 和命名空间中。注意 infra 容器有一个容器监控进程 conmon 在监控它。pod 内的每个容器都有自己的 conmon。
Conmon 是一个轻量级的 C 程序,监控容器直到其退出,允许 Podman 可执行文件退出并重新连接到容器。当监控容器时,Conmon 执行以下操作:
-
Conmon 执行 OCI 运行时,将其指向 OCI 规范文件的路径以及指向 containers/storage 中的容器层挂载点。挂载点称为 rootfs。
-
Conmon 监控容器直到其退出,并将退出代码返回。
-
当用户连接到容器时,Conmon 处理这种情况,提供一个套接字以流式传输容器的
STDOUT和STDERR。 -
STDOUT和STDERR也被记录到 Podman 日志文件中。
注意:infra 容器(暂停容器;见图 4.1)类似于无根暂停容器;它的唯一目的是保持命名空间和 cgroups 打开,同时容器来去。然而,每个 pod 将有一个不同的 infra 容器。

图 4.1 Podman pod 使用 infra 容器启动 conmon,该容器将保留 cgroups 和 Linux 命名空间。
Podman pod 也支持 init 容器,如图 4.2 所示。这些容器在 pod 中的主要容器执行之前运行。init 容器的一个例子是在卷上进行的数据库初始化。这将允许主要容器使用数据库。Podman 支持以下两类 init 容器:
-
一次——仅在 pod 首次创建时运行
-
总是——每次启动 pod 时都会运行
主要容器运行应用程序。

图 4.2 Podman 接着使用 conmon 启动任何 init 容器。这些 init 容器检查 infra 容器并加入其 cgroups 和命名空间。
Pods 还支持额外的容器,这些容器通常被称为sidecar容器(见图 4.4)。Sidecar 容器通常监控主容器,如图 4.3 所示,或者主容器运行的环境。Kubernetes 文档(kubernetes.io/docs/concepts/workloads/pods)将带有 sidecar 容器的 pods 描述如下:

图 4.3 Podman 在将带有conmon的主容器启动到 pod 中之前,会等待 init 容器完成。
一个 Pod 可以封装由多个紧密耦合且需要共享资源的容器组成的应用程序。这些紧密耦合的容器形成一个单一的服务单元——例如,一个容器向公众提供存储在共享卷中的数据,而另一个 sidecar 容器刷新或更新这些文件。Pod 将这些容器、存储资源和短暂的网络标识符作为一个单一单元封装起来。
如果你想要深入了解 sidecar 容器,以下网站上有多篇优秀的文章:www.magalix.com/blog/the-sidecar-pattern。

图 4.4 Podman 可以启动额外的称为 sidecar 容器的容器。
注意:虽然 pods 可以支持多个 sidecar 容器,但我建议你只使用一个。人们滥用这种能力的诱惑是真实的,尤其是在 Kubernetes 中,但它可能会消耗更多资源并变得难以控制。
使用 pods 的一个大优点是你可以将它们作为独立的单元进行管理。启动 pod 将启动其中的所有容器,停止 pod 将停止所有容器。
4.2 创建 pod
在本节中,你将创建一个包含 myimage 应用程序作为 pod 中主容器的 pod。你还将向 pod 添加第二个容器,一个 sidecar 容器,该容器将更新应用程序使用的 web 内容,以显示在 pod 内一起工作的两个容器。
你可以使用podman pod create命令创建一个名为mypod的 pod,如下面的命令所示:
$ podman pod create -p 8080:8080 --name mypod --volume ./html:/var/www/html:z
790fefe97b280e5f67c526e3a421e9c9f958cf5a98f3709373ef1afd91965955
podman的pod create命令具有与podman的container create命令许多相同的选项。当你在一个 pod 中创建容器时,该容器会继承这些选项作为其默认设置(见图 4.5)。

图 4.5 Podman 创建一个网络命名空间,并在容器中将主机的/var/www/html目录绑定到容器的端口8080。Podman 在容器中创建基础设施容器,并加入 cgroups 和网络命名空间。
注意,与前面的例子一样,你正在将 pod 绑定到端口-p 8080:8080:
$ podman pod create -p 8080:8080 --name mypod --volume ./html:/var/www/html:z
因为 Pod 内的容器共享相同的网络命名空间,所以这个端口绑定被所有容器共享。内核只允许一个进程监听端口8080。最后,请注意,./html 目录被挂载到 Pod 中,--volume ./html:/var/www/html:z:
$ podman pod create -p 8080:8080 --name mypod --volume ./html:/var/www/html:z
:z参数导致 Podman 重新标记目录的内容。Podman 会自动将此目录挂载到加入 Pod 的每个容器中。Pod 内的容器共享相同的 SELinux 标签,这意味着它们可以共享相同的卷。
4.3 将容器添加到 Pod
您可以使用podman create命令在 Pod 内创建一个容器(见图 4.6)。使用--pod mypod选项将 quay.io/rhatdan/myimage 容器添加到 Pod 中:
$ podman create --pod mypod --name myapp quay.io/rhatdan/myimage
Cec045acb1c2be4a6e4e88e21275076fb1de5519a25fb5a55f192da70708a640

图 4.6 因为 Pod 没有初始化容器,所以第一个myapp容器被启动到 Pod 中。
当您将第一个容器添加到 Pod 时,Podman 读取与基础设施容器相关联的信息,并将卷挂载添加到myapp容器中,然后将其加入到基础设施容器持有的命名空间中。下一步是将侧边容器添加到 Pod 中。侧边容器将更新/var/www/html 卷中的 index.xhtml 文件,每秒添加一个新时间戳。
创建一个简单的 Bash 脚本来更新myapp容器使用的 index.xhtml,命名为 html/time.sh。您可以在./html 目录中创建它,这样它就会在 Pod 内的进程可用:
$ cat > html/time.sh << _EOL
#!/bin/sh
data() {
echo "<html><head></head><body><h1>"; date;echo "Hello World</h1></body></html>"
sleep 1
}
while true; do
data > index.xhtml
done
_EOL
确保它是可执行的。您可以在 Linux 上使用chmod命令来完成此操作:
$ chmod +x html/time.sh
现在创建第二个容器(--name time),这次使用不同的镜像:ubi8。Pod 内的容器可以使用完全不同的镜像,甚至是来自不同发行版的镜像。回想一下,容器镜像默认情况下只共享主机内核:
$ podman create --pod mypod --name time --workdir /var/www/html ubi8 ./time.sh
Resolved "ubi8" as an alias (/etc/containers/registries.conf.d/000-shortnames.conf)
Trying to pull registry.access.redhat.com/ubi8:latest...
...
1be0b2fae53029d518e75def71c0d6961b662d0e8b4a1082edea5589d1353af3
记住第二章中提到的短名概念。您可以输入长名,registry.access.redhat.com/ubi8,但这需要很多输入。幸运的是,短名,ubi8,已经映射到其长名,这意味着您不需要从注册表列表中选择它。Podman 在输出中显示了它找到长名别名的位置:
$ podman create --pod mypod --name time --workdir /var/www/html ubi8 ./time.sh
Resolved "ubi8" as an alias (/etc/containers/registries.conf.d/000-shortnames.conf)
您还使用了--workdir命令选项来设置容器的默认目录为/var/www/html。当容器启动时,./time.sh 将在workdir中运行,实际上是/var/www/html/time.sh(见图 4.7):
$ podman create --pod mypod --name time --workdir /var/www/html ubi8 ./time.sh
因为这个容器将在mypod Pod 内运行,所以它会继承 Pod 的-v ./html:/var/www/html 选项,这意味着主机目录中的./html/time.sh 命令对 Pod 内的每个容器都是可用的。

图 4.7 最后,Podman 启动了名为time的侧边容器。
Podman 检查基础设施容器,在启动侧边容器时挂载了/var/www/html 卷,并加入了命名空间。现在,是时候启动 Pod 并看看会发生什么了。
4.4 启动 pod
您可以使用podman pod start命令启动 pod:
$ podman pod start mypod
790fefe97b280e5f67c526e3a421e9c9f958cf5a98f3709373ef1afd91965955
使用podman ps命令查看 pod 启动了哪些容器:
$ podman ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
b9536ea4a8ab localhost/podman-pause:4.0.3-1648837314 14 minutes ago Up 5 seconds ago 0.0.0.0:8080->8080/tcp 8920b1ccd8b0-infra
a978e0005273 quay.io/rhatdan/myimage:latest /usr/bin/run-http... 14 minutes ago Up 5 seconds ago 0.0.0.0:8080->8080/tcp myapp
be86937986e9 registry.access.redhat.com/ubi8:latest ./time.sh 13 minutes ago Up 5 seconds ago 0.0.0.0:8080->8080/tcp time
注意现在已启动了三个容器。基础容器基于 k8s.gcr.io/pause 镜像,您的应用程序基于 quay.io/rhatdan/myimage:latest 镜像,更新容器基于 registry.access.redhat.com/ubi8:latest 镜像。
当 ubi8 侧边容器启动时,它开始通过 time.sh 脚本来修改 index.xhtml 索引文件。由于myapp容器共享卷挂载,/var/www/html,因此它可以看到/var/www/html/index.xhtml 文件中的更改。启动您喜欢的网页浏览器,导航到 http://localhost:8080 以验证应用程序是否正在运行,如图 4.8 所示。

图 4.8 网页浏览器与 pod 中运行的myapp进行通信。
几秒钟后,按刷新按钮。注意日期变化,表明侧边容器正在运行并更新主容器内运行的myapp网络服务器使用的数据,如图 4.9 所示。

图 4.9 网页浏览器显示myapp的内容已被 pod 中运行的第二个容器更改。
一些显著的podman pod start选项包括以下内容:
-
--all—这告诉 Podman 启动所有 pod。 -
--latest—-l告诉 Podman 启动最后创建的 pod。(在 Mac 和 Windows 上不可用。)
现在您已经在 pod 内运行了应用程序,您可能想要停止应用程序。
4.5 停止 pod
现在您看到应用程序运行成功,您可以使用podman pod stop命令停止 pod,如下所示:
$ podman pod stop mypod
790fefe97b280e5f67c526e3a421e9c9f958cf5a98f3709373ef1afd91965955
使用podman ps命令确保 Podman 已停止 pod 内的所有容器:
$ podman ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
一些显著的podman pod stop选项包括以下内容:
-
--all—这告诉 Podman 停止所有 pod。 -
--latest—-l告诉 Podman 停止最近启动的 pod。 -
--timeout—-t告诉 Podman 在尝试停止 pod 内的容器时设置超时。
现在您已经创建了、运行并停止了 pod,您可以开始检查它。首先,列出您系统上的所有 pod。
4.6 列出 pod
您可以使用podman pod list命令列出 pod:
$ podman pod list
POD ID NAME STATUS CREATED INFRA ID # OF CONTAINERS
790fefe97b28 mypod Exited 22 minutes ago b9536ea4a8ab 3
一些显著的podman pod list选项包括以下内容:
-
--ctr*—这告诉 Podman 列出 pod 内的容器信息。 -
--format—这告诉 Podman 更改 pod 的输出。
现在您已经完成了演示,是时候清理 pod 和容器了。
4.7 删除 pod
在第八章中,我讨论了您如何生成 Kubernetes YAML 文件,以便您可以使用 Podman 或其他系统或 Kubernetes 内部启动您的 pod。但到目前为止,您可以使用podman pod rm命令删除 pod。
在进行此操作之前,列出系统上的所有容器--all。使用--format选项仅显示 ID、镜像和 pod ID,您将看到组成您的 pod 的三个容器:
$ podman ps --all --format "{{.ID}} {{.Image}} {{.Pod}}"
b9536ea4a8ab k8s.gcr.io/pause:3.5 790fefe97b28
a978e0005273 quay.io/rhatdan/myimage:latest 790fefe97b28
be86937986e9 registry.access.redhat.com/ubi8:latest 790fefe97b28
现在,您可以使用以下命令删除 pod:
$ podman pod rm mypod
790fefe97b280e5f67c526e3a421e9c9f958cf5a98f3709373ef1afd91965955
确保它已消失:
$ podman pod ls
POD ID NAME STATUS CREATED INFRA ID # OF CONTAINERS
太好了!看起来您的 pod 已经消失了。通过运行以下命令验证 Podman 是否删除了所有容器:
$ podman ps -a --format "{{.ID}} {{.Image}}"
系统已完全清理。
一些显著的podman pod rm选项包括以下内容(也请参阅表 4.1):
-
--all—这告诉 Podman 删除所有 pod。 -
--force—这告诉 Podman 在尝试删除容器之前先停止所有正在运行的容器。否则,Podman 只会删除非运行的 pod。
表 4.1 podman pod命令
| 命令 | 手册页 | 描述 |
|---|---|---|
create |
podman-pod-create(1) |
创建一个新的 pod。 |
exists |
podman-pod-exists(1) |
检查 pod 是否存在。 |
inspect |
podman-pod-inspect(1) |
显示 pod 的详细信息。 |
kill |
podman-pod-kill(1) |
向 pod 中容器的首要进程发送信号。 |
list |
podman-pod-list(1) |
列出所有 pod。 |
logs |
podman-pod-logs(1) |
获取包含一个或多个容器的 pod 的日志。 |
pause |
podman-pod-pause(1) |
暂停 pod 中的所有容器。 |
prune |
podman-pod-prune(1) |
删除所有停止的 pod 及其容器。 |
restart |
podman-pod-restart(1) |
重新启动一个 pod。 |
rm |
podman-pod-rm(1) |
删除一个或多个 pod。 |
stats |
podman-pod-stats(1) |
显示 pod 中容器的统计信息。 |
start |
podman-pod-start(1) |
启动一个 pod。 |
stop |
podman-pod-stop(1) |
停止一个 pod。 |
top |
podman-pod-top(1) |
显示 pod 中的运行进程。 |
unpause |
podman-pod-unpause(1) |
取消暂停 pod 中的所有容器。 |
摘要
-
Pod 是将容器组合在一起形成更复杂应用的一种方式,共享命名空间和资源限制。
-
Pod 与容器使用的多数选项相同,当您将容器添加到 pod 中时,它会与 pod 中的所有容器共享这些选项。
第二部分. 设计
书的第二部分涵盖了 Podman 的底层设计。第五章解释了与 Podman 一起使用的所有不同配置文件。Podman 使用多个不同的容器库进行开发,每个库都有独特的配置方法。你将学习如何配置你的容器存储以及在哪里存储你的容器和镜像。你还将学习如何配置你用于拉取和推送容器镜像的容器注册库。最后,你将了解 containers.conf,它允许你完全自定义 Podman 的工作方式。基本上,你可以更改 Podman CLI 为你创建的每个容器使用的默认值。
第六章接着深入探讨了无根容器的工作原理。无根容器是 Podman 的一个关键特性,允许你作为普通用户完全使用容器和 pods,而不需要任何额外权限。这一章还介绍了用户命名空间的工作原理,并允许你在容器中使用多个 UID,而不需要是 root 用户。最后,你将了解一些无根容器的问题以及如何解决这些问题。
5 自定义和配置文件
本章涵盖
-
根据使用的库使用 Podman 配置文件
-
配置 storage.conf 文件
-
使用 registries.conf 和 policy.json 文件进行配置
-
使用 containers.conf 文件配置其他默认设置
-
使用系统配置文件允许非 root 用户命名空间访问
Podman 等容器引擎内置了数十个硬编码的默认值。这些默认值决定了 Podman 的功能性和非功能性行为的许多方面,例如网络和安全设置。Podman 开发者试图选择最大程度的安全,但仍然允许大多数容器成功运行。同样,我也希望尽可能多地从主机隔离。
安全默认设置包括要使用的 Linux 能力、要设置的 SELinux 标签以及容器可用的系统调用集合。对于资源约束,如内存使用量和容器内允许的最大进程数,也有默认值。其他默认设置包括存储镜像的本地路径、容器注册库列表,甚至允许无根模式工作的系统配置。Podman 开发者希望允许用户对这些默认值拥有最终控制权,因此容器引擎配置文件提供了一种自定义 Podman 和其他容器引擎运行方式的机制。
默认配置的问题在于它们是开发者基于最佳猜测得出的估计。虽然大多数用户都在默认配置下运行 Podman,但有时需要更改配置。并非每个环境都有相同的配置,你可能希望将某些机器的默认安全级别和注册表配置设置为不同级别。即使是无根用户也可能需要与有根用户不同的配置。在本章中,我将向您展示如何自定义 Podman 的不同部分,并解释您可以在哪里找到有关所有可用旋钮的更多信息。
如您在前面章节中学到的,Podman 使用多个库在处理容器时执行不同的任务。表 5.1 描述了 Podman 使用的不同库。
表 5.1 Podman 使用的容器库
| 库 | 描述 |
|---|---|
| containers/storage | 定义了容器镜像和其他基本存储的存储,这些存储由容器引擎使用 |
| containers/image | 定义了用于在不同类型存储之间移动容器镜像的机制;通常用于容器注册库和本地容器存储之间 |
| containers/common | 定义了在 containers/storage 或 containers/image 中未定义的所有容器引擎默认配置选项 |
| containers/buildah | 如第二章所述,它用于使用在 Containerfile 或 Dockerfile 中定义的规则将容器镜像构建到本地存储;有关 Buildah 的更多信息,请参阅附录 A。 |
这些库中的每一个都有单独的配置文件,用于设置特定库的默认设置,Buildah 除外。容器引擎 Podman 和 Buildah 共享 containers/common 配置文件 containers.conf,这在第 5.3 节中进行了描述。
注意:Podman 所使用的所有非系统配置文件都使用 TOML 格式。TOML 的语法由名称 = “值” 对、[部分名称] 和 # 注释组成。TOML 的格式可以简化为以下形式:
-
[表格] -
选项=值 -
[表格子表格 1] -
选项=值 -
[表格子表格 2] -
选项=值
请参阅 toml.io 以获取对 TOML 语言的更完整解释。当配置 Podman 时,通常第一个关注的问题之一是您打算在哪里存储您的容器和镜像。
5.1 存储配置文件
Podman 使用 github.com/containers/storage 库,该库提供了存储文件系统层、容器镜像和容器的相关方法。此库的配置是通过 storage.conf 配置文件完成的,该文件可以存储在多个不同的目录中。
Linux 发行版通常提供 /usr/share/containers/storage.conf 文件,可以通过创建 /etc/containers/storage.conf 文件来覆盖。无根用户可以将他们的配置存储在 $XDG_CONFIG_HOME/containers/storage.conf 文件中;如果 $XDG_CONFIG_HOME 环境变量未设置,则使用 $HOME/.config/ containers/storage.conf 文件。大多数用户永远不会更改 storage.conf 文件,但在少数情况下,高级用户需要进行一些自定义设置。最常见的更改原因是重新定位容器的存储。
注意:当在远程模式下使用 Podman,例如在 Mac 或 Windows 系统上时,Podman 服务会使用位于 Linux 系统上的 storage.conf 文件。要修改这些文件,您需要进入虚拟机。在使用 Podman 机器时,执行 podman machine ssh 命令进入虚拟机。有关更多信息,请参阅附录 E 和 F。
Podman 只读取一个 storage.conf 文件,并忽略所有后续的文件。Podman 首先尝试使用您主目录中的 storage.conf 文件;其次是 /etc/storage/ 目录下的 storage.conf 文件;最后,如果这两个文件都不存在,Podman 会读取 /usr/share/ containers/storage.conf 文件。您可以通过 podman info 命令查看 Podman 命令正在使用的 storage.conf 文件:
$ podman info --format '{{ .Store.ConfigFile }}'
/home/dwalsh/.config/containers/storage.conf
5.1.1 存储位置
默认情况下,无根 Podman 配置为将您的镜像存储在 $HOME/.local/ share/containers/storage 目录中。默认的带根存储位置是 /var/lib/ containers/storage。
有时您需要更改此默认位置。可能您在 /var 或用户主目录中没有足够的磁盘空间,因此您希望将镜像存储在不同的磁盘上。storage.conf 文件将存储位置称为 graphRoot,并且可以在 /etc/containers/storage.conf 中为带根容器覆盖。
在本节中,你将修改图驱动程序的位置到 /var/mystorage。首先,成为 root 并确保 /etc/containers/storage.conf 文件存在。如果不存在,只需将 /usr/share/containers/storage.conf 文件复制到其中:
$ sudo cp /usr/share/containers/storage.conf /etc/containers/storage.conf
注意:某些发行版仅提供 /etc/containers/storage.conf。
现在,创建一个备份,并打开 /etc/containers/storage.conf 文件进行编辑:
$ sudo cp /etc/containers/storage.conf /etc/containers/storage.conf.orig
$ sudo vi /etc/containers/storage.conf
将 graphdriver 变量的 graphroot 设置为 "/var/lib/containers/storage" 并保存文件。
你的 storage.conf 文件应包括以下内容:
$ grep -B 1 graph /etc/containers/storage.conf
# Primary Read/Write location of container storage
graphroot = "/var/mystorage"
执行 podman info 来查看更改是否发生:
$ sudo podman info
...
Store:
configFile: /etc/containers/storage.conf
...
graphDriverName: overlay
graphOptions:
overlay.mountopt: nodev,metacopy=on
graphRoot: /var/mystorage
...
volumePath: /var/mystorage/volumes
注意在存储部分,graphRoot 现在是 /var/mystorage。所有镜像和容器都将存储在这个目录中。
现在,在无根模式下运行 podman info 命令。存储位置不会改变;它仍然是 /home/dwalsh/.local/share/containers/storage:
$ podman info
store:
configFile: /home/dwalsh/.config/containers/storage.conf
containerStore:
number: 27
paused: 0
running: 0
stopped: 27
graphDriverName: overlay
graphOptions: {}
graphRoot: /home/dwalsh/.local/share/containers/storage
你可以在 $HOME/.config/containers/storage.conf 中创建一个并更改它,但这对于有多个用户的系统来说扩展性不好。键 rootless_storage_path 允许你更改系统中所有用户的位置。
这次,取消注释并修改 rootless_storage_path 行:
$ sudo vi /etc/containers/storage.conf
将 storage.conf 中的 rootless_storage_path 行修改为
# rootless_storage_path = "$HOME/.local/share/containers/storage"
将其更改为
rootless_storage_path = "/var/tmp/$UID/var/mystorage"
保存 storage.conf 文件。当你完成时,它应该看起来像这样:
$ grep -B 3 rootless_storage_path /etc/containers/storage.conf
# Storage path for rootless users
#
rootless_storage_path = "/var/tmp/$UID/var/mystorage"
现在,运行 podman info 来查看更改。注意,graphRoot 现在指向 /var/tmp/3267/var/mystorage 目录:
$ podman info
...
store:
configFile: /home/dwalsh/.config/containers/storage.conf
...
graphOptions: {}
graphRoot: /var/tmp/3267/var/mystorage
容器/存储支持为此路径扩展 $HOME 和 $UID 环境变量。要撤销更改,复制并恢复原始的 storage.conf 文件:
$ sudo cp /etc/containers/storage.conf.orig /etc/containers/storage.conf
注意:如果你在一个 SELinux 系统上运行,并更改存储的默认位置,你需要使用以下 semanage 命令通知 SELinux。这将告诉 SELinux 将新位置标记为如果它在旧位置一样。接下来,你需要使用 restorecon 命令更改磁盘上的标记。你可以使用以下命令来完成此操作:
sudo semanage fcontext -a -e /var/lib/containers/storage /var/mystorage
sudo restorecon -R -v /var/mystorage
在无根模式下,你需要执行以下操作:
sudo semanage fcontext -a -e $HOME/.local/share/containers/storage/
➥ var/tmp/3267/var/mystorage
sudo restorecon -R -v /var/tmp/3267/var/mystorage
有时你可能想更改存储驱动程序,或者更可能的是,更改存储驱动程序的配置。
5.1.2 存储驱动程序
回想第二章中的婚礼蛋糕插图。这个插图显示图像通常由多个层组成。这些层由容器/存储库存储在磁盘上,但当你在这上面运行容器时,每个层都需要挂载到前一个层(图 5.1)。

图 5.1 层叠在一起的分层图像通过容器/存储重新组装和挂载。
Container/storage 使用 Linux 内核文件系统概念,称为分层文件系统来完成这项工作。Podman 通过 container/storage 支持多种不同类型的分层文件系统。在 Linux 中,这些文件系统被称为写时复制(CoW)文件系统。在容器/storage 中,这些不同的文件系统类型被称为驱动程序。默认情况下,Podman 使用overlay存储驱动程序。
注意 Docker 支持两种类型的 overlay 驱动程序:overlay和overlay2。overlay2是overlay的改进版,原始的overlay驱动程序现在很少使用。相比之下,Podman 使用较新的overlay2驱动程序,并称之为overlay。您可以在 Podman 中选择overlay驱动程序,但这只是overlay2的别名。
表 5.2 列出了 Podman 和 container/storage 支持的存储驱动程序。我建议您只坚持使用overlay驱动程序,因为这是世界上绝大多数人使用的驱动程序。
表 5.2 容器存储驱动程序
| 存储驱动程序 | 描述 |
|---|---|
overlay (overlay2) |
这是默认驱动程序,我强烈推荐使用它。它基于 Linux 内核 overlay 文件系统。在 Podman 中,overlay和overlay2完全相同。这是最经过测试的驱动程序,绝大多数用户都在使用它。 |
vfs |
这是最简单的驱动程序;它为每一层创建完整的副本到下一层。它在任何地方都适用,但速度较慢且非常占用磁盘。 |
devmapper |
当 Docker 最初变得流行时,此驱动程序被广泛使用——在overlay驱动程序可用之前。它会重新分配每个层的最大大小。现在不再推荐使用。 |
aufs |
此驱动程序从未合并到上游内核中,因此它仅在少数 Linux 发行版上可用。 |
btrfs |
此驱动程序允许基于 Btrfs 文件系统的快照进行存储。一些用户在使用此文件系统时取得了成功。 |
zfs |
此驱动程序使用 ZFS 文件系统,这是一个专有文件系统,在大多数发行版上不可用。 |
overlay 存储选项
overlay驱动程序有一些有趣的定制选项。这些选项位于 storage.conf [storage.options.overlay]表中。
配置 overlay 驱动程序有多个高级选项可用。我将简要提及几个以描述用例。
mount_program选项允许您指定一个可执行文件来代替内核 overlay 驱动程序。Podman 通常附带fuse-overlayfs可执行文件,它提供了一个FUSE(用户空间)overlay 驱动程序。如果系统不支持无根原生 overlay,Podman 会自动切换到fuse-overlayfs的mount_program。大多数内核支持原生 overlay;然而,在某些情况下,您可能需要配置mount_program。fuse-overlayfs具有目前原生 overlay 不支持的高级功能。
Podman 正迅速被高性能计算 (HPC) 社区采用。HPC 社区不允许有 root 权限的容器,并且在许多情况下只允许工作负载以单个 UID 运行。这意味着一些 HPC 系统不允许具有多个 UIDs 的用户命名空间。由于许多镜像都带有多个 UIDs,Podman 为 containers/storage 添加了 ignore_chown_errors 选项,以便将具有不同 UIDs 的文件扁平化到单个 UID。表 5.3 列出了容器存储支持的所有当前存储选项。
注意:您已经检查了 storage.conf 的一些字段,但还有很多。使用 containers-storage.conf 手册页来探索所有这些选项:
https:/ /github.com/containers/storage/blob/main/docs/containers-storage.conf.5.md
$ man containers-storage.conf
表 5.3 容器存储驱动程序
| 存储驱动程序 | 描述 |
|---|---|
ignore_chown_errors |
忽略对无 root 权限容器中具有单个 UID 的文件进行 chown 的 UIDs。/etc/subuid 中没有条目。 |
mount_program |
用于挂载文件系统的辅助程序路径,而不是使用内核覆盖来挂载。较旧的内核不支持无 root 权限的覆盖。 |
mountopt |
要传递给内核的挂载选项的逗号分隔列表。默认为 "nodev,metacopy=on"。 |
skip_mount_home |
不要在存储主目录上创建 PRIVATE 绑定挂载。 |
inode |
容器镜像中 inode 的最大数量 |
size |
容器镜像的最大大小 |
| force_mask | 图像中新文件和目录的权限掩码。值如下:
-
private—这会将所有文件系统对象设置为0700。系统上的其他用户无法访问这些文件。 -
shared—这相当于0755。系统上的每个人都可以读取、访问和执行镜像中的文件。这对于与其他用户共享容器存储很有用。
图像中的所有文件都允许系统上的任何用户读取和执行。即使您镜像中的 /etc/shadow 现在也可以被任何用户读取。当 force_mask 设置时,原始权限掩码存储在 xattrs 中,而 mount_program(如 /usr/bin/fuse-overlayfs)向容器内的进程呈现 xattr 权限。
现在,您已经了解了如何配置容器存储!接下来,您将查看的是容器注册表访问的配置。
5.2 注册表配置文件
Podman 使用 github.com/containers/image 库来拉取和推送容器镜像,通常来自容器注册表。Podman 使用 registries.conf 配置文件来指定注册表,并使用 policy.json 文件对镜像进行签名验证。与容器存储的 storage.conf 一样,大多数用户永远不会修改这些文件,只是使用发行版的默认设置。
5.2.1 registries.conf
registries.conf 配置文件是容器镜像注册表的系统级配置文件。如果存在,Podman 使用 $HOME/.config/containers/registries.conf;否则,它使用 /etc/containers/registries.conf。
注意:当在远程模式下使用 Podman,例如在 Mac 或 Windows 机器上时,registries.conf 文件存储在服务器端的 Linux 机器上。你需要通过 ssh 登录到 Linux 机器以进行更改。使用 Podman 机器,你可以执行 podman machine ssh。有关更多信息,请参阅附录 E 和 F。
与 registries.conf 文件一起使用的主要键值是 unqualified-search-registries。该字段指定了一个 host[:port] 注册库数组,用于通过短名称拉取时尝试,按顺序。如果你在 unqualified-search-registries 选项中只指定一个注册库,Podman 将与 Docker 类似,并强制用户使用单个注册库。
在这个练习中,你需要修改 Podman 使用的默认搜索注册库。首先,你需要备份 /etc/containers/registries.conf 文件,然后删除 docker.io 并添加 example.com:
$ sudo cp /etc/containers/registries.conf /etc/containers/registries.conf.orig
$ sudo vi /etc/containers/registries.conf
修改以下行:
unqualified-search-registries = ["registry.fedoraproject.org", "registry.access.redhat.com", "docker.io", "quay.io"]
将行更改为
unqualified-search-registries = ["registry.fedoraproject.org", "registry.access.redhat.com", "example.com", "quay.io"]
保存文件后,执行 podman info 以验证更改:
$ podman info
registries:
search:
- registry.fedoraproject.org
- registry.access.redhat.com
- example.com
- quay.io
现在,如果你尝试通过一个未知的短名称进行拉取,你应该看到以下提示:
$ podman pull foobar
? Please select an image:
▸ registry.fedoraproject.org/foobar:latest
registry.access.redhat.com/foobar:latest
example.com/foobar:latest
quay.io/foobar:latest
将原始内容复制到 registries.conf 文件中:
$ sudo cp /etc/containers/registries.conf.orig /etc/containers/registries.conf
表 5.4 描述了 registries.conf 文件中所有可用的选项。
表 5.4 Container registries.conf 全局字段
| 字段 | 描述 |
|---|---|
unqualified-search-registries |
一个 host[:port] 注册库数组,用于按顺序尝试拉取未经验证的镜像。 |
| short-name-mode | 确定 Podman 如何处理短名称。值包括以下内容:
-
enforcing—如果有一个未经验证的搜索注册库,则使用它。如果有两个或更多注册库定义,并且你在终端中运行 Podman,则提示用户选择一个搜索注册库;否则,将出现错误。 -
permissive—行为类似于enforcing,但如果没有终端则不会导致错误:只需使用未经验证的搜索注册库中的每个条目,直到成功。 -
disabled—使用所有未经验证的搜索注册库而无需提示。
|
credential-helpers |
使用默认凭证助手数组作为外部凭证存储。请注意,containers-auth.json 是一个保留值,用于使用 containers-auth.json(5) 中指定的认证文件。如果没有指定任何凭证助手,则将凭证助手设置为 ["containers-auth.json"]。 |
|---|
阻止从容器注册库拉取
在 registries.conf 中可以配置的另一个有趣的功能是阻止用户从容器注册库拉取的能力。在以下示例中,你将配置 registries.conf 以阻止从 docker.io 拉取。registries.conf 文件有一个特定的 [[registry]] 表条目,可以指定如何处理单个容器注册库。你可以多次添加此表——每个注册库一次:
$ sudo vi /etc/containers/registries.conf
添加以下内容:
[[registry]]
Location = "docker.io"
blocked=true
保存文件。使用 podman info 检查设置:
$ podman info
...
registries:
Docker.io:
Blocked: true
Insecure: false
Location: docker.io
MirrorByDigestOnly: false
Mirrors: null
Prefix: docker.io
search:
- registry.fedoraproject.org
- registry.access.redhat.com
- docker.io
- quay.io
现在,尝试从 docker.io 拉取镜像:
$ podman pull docker.io/ubuntu
Trying to pull docker.io/library/ubuntu:latest...
Error: initializing source docker:/ /ubuntu:latest: registry docker.io is blocked in /etc/containers/registries.conf or /home/dwalsh/.config/containers/registries.conf.d
这表明管理员有阻止来自特定注册表内容的能力。表 5.5 描述了在 registries.conf 文件中的 [[registry]] 表中可用的子选项。
注意:将原始 registries.conf 复制到 docker.io 以从本书的其余部分拉取:
$ sudo cp /etc/containers/registries.conf.orig/
➥ etc/containers/registries.conf
表 5.5 [[registry]] 表字段
| 字段 | 描述 |
|---|---|
location |
要应用筛选器的注册表/存储库名称 |
prefix |
在尝试拉取与特定前缀匹配的镜像时选择指定的配置。 |
insecure |
如果为真,则允许未加密的 HTTP 以及与不受信任证书的 TLS 连接。 |
blocked |
如果为真,则禁止拉取具有匹配名称的镜像。 |
一些用户在完全与互联网隔离的系统上工作,但仍需要使用依赖于互联网镜像的应用程序。这种情况的一个例子是,如果您有一个期望使用 registry.access.redhat.com/ubi8/ httpd-24:latest 的应用程序,但没有访问 registry.access.redhat.com 的互联网。您可以下载镜像并将其放入内部注册表,然后配置 registries.conf 以使用镜像注册表。如果您在 registries.conf 中配置条目,它将看起来像这样:
[[registry]]
location="registry.access.redhat.com"
[[registry.mirror]]
location="mirror-1.com"
然后,您的用户可以使用 podman pull 命令:
$ podman pull registry.access.redhat.com/ubi8/httpd-24:latest
Podman 实际上拉取 mirror-1.com/ubi8/httpd-24:latest,但用户不会注意到差异。
注意:您已经检查了一些 registries.conf 字段,但还有很多。使用 containers-registries.conf(5) 手册页来探索所有这些字段:
$ man containers-registries.conf
https:/ /github.com/containers/image/blob/main/docs/containers-registries.conf.5.md
现在您已经知道了如何配置存储和注册表,是时候看看如何配置 Podman 的所有核心选项了。
5.3 引擎配置文件
Podman 和其他容器引擎使用 github.com/containers/common 库来处理与容器存储或容器注册表无关的默认设置。这些配置设置来自 containers.conf 文件。如果存在,Podman 会读取表 5.6 中的文件。
表 5.6 读取 rootful 和 rootless Podman 的 containers.conf 文件
| 文件 | 描述 |
|---|---|
| /usr/share/containers/containers.conf | 通常与发行版默认值一起提供 |
| /etc/containers/containers.conf | 系统管理员可以使用此文件来设置和修改不同的默认值。 |
| /etc/containers/containers.conf.d/*.conf | 一些包工具可能会将额外的默认文件放入此目录,按数字排序。 |
当以 rootless 模式运行时,如果存在,Podman 也会读取表 5.7 中的文件。
表 5.7 rootless Podman 读取的 containers.conf 文件
| 文件 | 描述 |
|---|---|
| $HOME/.config/containers/containers.conf | 用户可以创建此文件以覆盖系统默认值。 |
| $HOME/.config/containers/containers.conf.d/*.conf | 用户也可以将文件放在这里,如果他们愿意,它们将被按数字排序。 |
与 storage.conf 和 registries.conf 不同,containers.conf 文件是合并在一起的,并且它们不会完全覆盖之前的版本。单个字段可以覆盖高级 containers.conf 文件中的相同字段。由于 Podman 内置了默认设置,因此它不需要任何 containers.conf 文件存在。大多数系统只包含 /usr/share/containers/containers.conf 中的发行版默认覆盖设置。
注意:Podman 支持使用 CONTAINERS_CONF 环境变量,这会强制 Podman 使用 $CONTAINERS_CONF 的目标。所有其他 containers.conf 文件都将被忽略。这在测试环境或确保没有人自定义了 Podman 默认设置时非常有用。
containers.conf 当前支持五个不同的表格,如表 5.8 所示。当你修改选项时,请确保你处于正确的表格中。
表 5.8:containers.conf 表格
| 表格 | 描述 |
|---|---|
[containers] |
对运行单个容器的配置。例如,将容器粘附在命名空间中、是否启用 SELinux 以及容器的默认环境变量。 |
[engine] |
Podman 使用时的默认配置。例如,默认的日志系统、OCI 运行时使用的路径以及 conmon 的位置。 |
[service_destinations] |
与 podman --remote 一起使用的远程连接数据。远程服务在第九章中有介绍。 |
[secrets] |
关于用于容器的 secrets 插件驱动程序的信息 |
[network] |
网络配置的特殊配置,包括默认网络名称、CNI 插件的位置以及默认子网 |
许多 Podman 用户希望改变它在环境中启动容器的方式。我之前解释了高性能计算社区如何希望使用 Podman 来运行他们的工作负载,但他们对于添加到容器中的卷、添加的环境变量以及启用的命名空间有非常具体的要求。
可能你希望所有容器的环境变量都设置得相同。让我们尝试一个例子。运行 podman 来显示 ubi8 镜像中的默认环境。
$ podman run --rm ubi8 printenv
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
TERM=xterm
container=oci
HOME=/root
HOSTNAME=ba4acf180386
注意:当在远程模式下使用 Podman,例如在 Mac 或 Windows 机器上时,大多数 containers.conf 文件的设置都是从服务器端的 Linux 机器上使用的。用户主目录中的 containers.conf 文件用于存储连接数据,这部分内容在第九章中有详细说明。Mac 和 Windows 客户端在第 E 和 F 附录中有介绍。
现在在主目录中创建一个名为 env.conf 的文件,并设置 env="[foo=bar]":
$ mkdir -p $HOME/.config/containers/containers.conf.d
$ cat << _EOF > $HOME/.config/containers/containers.conf.d/env.conf
[containers]
env=[ "foo=bar" ]
_EOF
Run any container and you see the foo=bar environment set.
$ podman run --rm ubi8 printenv
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
TERM=xterm
container=oci
foo=bar
HOME=/root
HOSTNAME=406fc182d44b
我使用 containers.conf 来配置 Podman 在容器内运行。许多用户希望将 Podman 运行在容器内,用于 CI/CD 系统,或者只是测试比他们的发行版提供的 Podman 更新版本的 Podman。由于很多人在容器内运行 Podman 时遇到了困难,我决定尝试创建一个默认镜像 quay.io/podman/stable,以帮助他们。在创建该镜像时,我意识到 Podman 的几个默认设置在容器内运行时效果不佳,因此我使用了 containers.conf 来更改这些设置。您可以通过此链接查看我的 containers.conf 文件:mng.bz/o5DM。
你可以通过实际运行镜像来查看 contains.conf:
$ podman run quay.io/podman/stable cat /etc/containers/containers.conf
[containers]
netns="host"
userns="host"
ipcns="host"
utsns="host"
cgroupns="host"
cgroups="disabled"
log_driver = "k8s-file"
[engine]
cgroup_manager = "cgroupfs"
events_logger="file"
runtime="crun"
在编写此文件时,我考虑了以下几点。首先,由于 Podman 在容器内运行,我决定除了挂载和用户命名空间之外,禁用所有其他 cgroups 和命名空间。如果用户设置了 cgroups 或配置了命名空间,那么 Podman 在容器内运行的容器将遵循父 Podman 的规则:
[containers]
netns="host"
userns="host"
ipcns="host"
utsns="host"
cgroupns="host"
cgroups="disabled"
许多分布默认的 log_driver、事件记录器和 cgroup 管理器分别是 journald 和 system,但在容器内,systemd 和 journald 都没有运行,因此容器引擎需要使用文件系统:
[containers]
log_driver = "k8s-file"
[engine]
cgroup_manager = "cgroupfs"
events_logger="file"
最后,使用 OCI 运行时 crun 而不是 runc,主要是因为 crun 比较小:
[engine]
runtime="crun"
现在尝试在一个容器内运行一个容器。使这成为可能的一个技巧是使用 --user podman 运行 podman/stable 镜像。这会导致容器内的 Podman 以无根模式运行。由于 podman/stable 镜像在容器内使用 fuse-overlay 驱动程序,因此你还需要添加 /dev/fuse 设备:
$ podman run --device /dev/fuse --user podman quay.io/podman/stable podman
➥ run ubi8-micro echo hi
Resolved "ubi8" as an alias (/etc/containers/registries.conf.d/
➥ 000-shortnames.conf
Trying to pull registry.access.redhat.com/ubi8:latest...
Getting image source signatures
Copying blob sha256:5368f457acd16b337e2b150741f727c46f886c69eea
➥ 1a4d56d0114c88029ed87
...
hi
注意:您检查了几个 containers.conf 字段,但还有很多。使用 container.conf(5) 手册页来探索所有这些字段:
$ man containers.conf
https:/ /github.com/containers/common/blob/main/docs/containers.conf.5.md
现在你已经了解了针对容器工具如 Podman 的特定配置工具。接下来,你将了解 Podman 需要的一些其他系统配置文件。
5.4 系统配置文件
当你运行无根 Podman 时,你正在使用 /etc/subuid 和 /etc/subgid 文件来指定容器的 UID 范围。正如我在 3.1.2 节中解释的那样,Podman 读取 /etc/subuid 和 /etc/subgid 文件以获取为你的用户账户分配的 UID 和 GID 范围。然后,Podman 启动 /usr/bin/newuidmap 和 /usr/bin/newgidmap,这些程序会验证 Podman 指定的 UID 和 GID 范围实际上是否分配给了你。在某些情况下,你需要修改这些文件来添加 UID。例如,当我安装我的笔记本电脑时,useradd 将我的用户账户设置为使用 UID 3267,并将映射 dwalsh:100000:65536 添加到 /etc/subuid 和 /etc/subgid。图 5.2 展示了基于此映射在我的系统上看起来像什么容器。

图 5.2 容器用户命名空间映射
注意:您希望为每个用户保留唯一的 UID 范围,并确保它们不与任何系统 UID 冲突。Podman 和系统不会验证是否存在冲突。如果两个不同的用户在其范围内有相同的 UID,容器中的进程将允许从用户命名空间的角度相互攻击。验证这是一个手动过程。useradd 工具会自动选择唯一的范围。
如 subuid(5) 和 subgid(5) 手册页所述,/etc/subuid 和 /etc/subgid 中的每一行都包含一个用户名和一个用户允许使用的从属用户 ID 或 GID 的范围。条目由冒号分隔的三个字段指定。这些字段如下:
-
登录名或 UID
-
数值从属用户 ID 或组 ID
-
数值从属用户 ID 或组 ID 计数
操作系统的较新版本,特别是包含 /usr/bin/newuidmap 和 /usr/bin/newgidmap 软件的包,正在获得通过 LDAP 服务器共享这些文件内容的能力。在 Fedora 上,这些可执行文件包含在 shadow-utils 包中。版本 4.9 或更高版本具有此功能。
小贴士:对 /etc/subuid 和 /etc/subgid 的更改可能不会立即反映在用户的账户中。这是修改这些文件后已经运行 Podman 的用户常见的问题。但请记住:当 Podman 首次运行时,它会在用户命名空间中启动 podman pause 进程,然后所有其他容器都加入这个 Podman 进程的用户命名空间。要使新的用户命名空间生效,您必须执行 podman system migrate 命令,该命令停止 podman pause 进程并重新创建用户命名空间。
摘要
-
Podman 根据所使用的库具有多个配置文件。
-
配置文件在 rootful 和 rootless 环境之间共享。
-
storage.conf文件用于配置containers/storage,包括存储驱动程序以及容器及其镜像的存储位置。 -
registries.conf和policy.json文件用于配置容器/镜像库——主要影响对容器注册库、短名称和镜像站的访问。 -
containers.conf文件用于配置 Podman 内部使用的所有其他默认设置。 -
系统配置文件
/etc/subuid和/etc/subgid用于配置运行无根 Podman 所需的用户命名空间。
6 无 root 容器
本章涵盖了
-
为什么无 root 模式更安全
-
Podman 如何与用户和挂载命名空间协同工作
-
无 root 模式运行 Podman 的架构
在本章中,你将深入了解在无 root 模式下运行 Podman 时发生的情况。我相信了解运行无 root 容器时发生的情况,以及了解在无 root 模式下运行可能引起的问题,是有帮助的。随着过去几年容器化应用程序的引入,某些高度安全的环境无法利用这项新技术。
高性能计算(HPC)系统运行着世界上最快的计算机。这些系统通常位于国家实验室和大学,处理高度机密信息。它们还处理世界上一些最安全的数据,并明确禁止使用 root 容器。HPC 系统处理大量数据集,包括人工智能、核武器、全球天气模式和医学研究。这些系统通常有数千台共享计算机,由于它们的多用户共享环境,需要被锁定。HPC 计算认为以 root 身份运行守护进程太不安全。如果一个恶意容器进程突破限制并获得 root 访问权限,它可以访问高度敏感的数据。HPC 环境的管理员在 Podman 出现之前无法使用开放容器倡议(OCI)容器。现在,HPC 社区正在努力迁移到无 root 的 Podman。
同样,大型金融公司管理员出于对涉及财务数据的担忧,不允许用户和开发人员访问他们共享计算机系统上的 root 权限。世界上最大的金融公司发现很难完全采用 OCI 容器。图 6.1 显示,尽管 Docker 客户端可以以非 root 身份运行,但它连接到一个 root 运行的守护进程,从而为宿主操作系统提供完整的 root 访问权限。

图 6.1 多个用户的工作负载共享同一个以 root 身份运行的守护进程,这在本质上是不安全的。
核心问题是,允许共享计算系统上的用户运行访问相同 root 运行守护进程的容器工作负载,这太不安全了。在各个用户的账户下以无 root 模式运行每个用户的容器更安全。图 6.2 显示了多个用户独立运行 Podman,没有任何 root 访问权限。

图 6.2 每个工作负载在其独特的用户空间中运行,这更安全。
Linux 从一开始就被设计为在特权模式(rootful)和非特权模式(rootless)之间进行分离。在 Linux 中,几乎所有任务都是非特权运行的。特权操作仅需要修改核心操作系统。几乎在容器、Web 服务器、数据库和用户工具中运行的所有应用程序都不需要 root 权限。这些应用程序不会修改系统的核心部分。遗憾的是,你将在容器注册表中找到的大多数镜像都是构建为需要 root 权限,或者至少以 root 身份启动然后降级权限的。
在企业界,管理员非常不愿意向用户授予 root 访问权限。如果你从雇主那里收到了一台企业笔记本电脑,通常你不会获得任何 root 访问权限。管理员需要控制他们系统上安装的内容,因为规模的原因,他们需要能够同时更新数百到数千台机器,因此控制操作系统中的内容至关重要。如果有人正在管理你的机器,他们需要控制谁可以获得 root 访问权限。
作为一名安全人员,当我看到没有密码的 sudo 时,我仍然会有些紧张。当我最初开始使用 Docker 时,我震惊地发现它鼓励使用 Docker 组,给予用户在主机上的完全 root 访问权限,而不需要密码。黑客的圣杯是获得根漏洞;这意味着黑客可以完全控制系统。
核心观点是,如果你遇到了容器逃逸,尽管这很糟糕,但在无根模式下你会更好。这是因为黑客只能控制非特权进程,而不是像根漏洞那样,他们可以完全控制整个系统和所有数据(忽略其他安全机制,如 SELinux)。Podman 的设计目标包括尽可能多地运行工作负载而不需要 root 权限,并推动核心操作系统,使其更容易以这种更安全的方式运行。
6.1 无根 Podman 是如何工作的?
你是否曾经好奇过无根 Podman 容器背后的情况?在第二章中,所有的 Podman 示例都是在无根模式下运行的。让我们看看无根 Podman 容器底下的情况。我会解释每个组件,然后分解所有涉及的步骤。
注意:本节中的一些内容是从“无根 Podman 容器背后的情况”博客(www.redhat.com/sysadmin/behind-scenes-podman)中复制和改写的,该博客由我和同事 Matthew Heon 和 Giuseppe Scrivano 撰写。
首先,让我们清理所有存储,以便你获得一个全新的环境,然后在 quay.io/rhatdan/myimage 上运行一个容器。(记住,podman rmi --all --force命令会从存储中删除所有镜像和容器。)
$ podman rmi --all --force
Untagged: registry.access.redhat.com/ubi8/httpd-24:latest
Untagged: registry.access.redhat.com/ubi8-init:latest
Untagged: localhost/myimage:latest
Untagged: quay.io/rhatdan/myimage:latest
Deleted: d2244a4379d6f1981189d35154beaf4f9a17666ae3b9fba680ddb014eac72adc
Deleted: 82eb390304938f16dd707f32abaa8464af8d4a25959ab342e25696a540ec56b5
Deleted: 8773554aad01d4b8443d979cdd509e7b8fa88ddbc966987fe91690d05614c961
现在您有一个干净的系统,您需要从第二章中您推送到容器注册中心的容器镜像 quay.io/ rhatdan/myimage 中检索应用程序镜像。在以下命令中,在您的机器上重新创建应用程序。该命令从容器注册中心拉取镜像,并在您的宿主机上启动myapp容器。
$ podman run -d -p 8080:8080 --name myapp quay.io/rhatdan/myimage
Trying to pull quay.io/rhatdan/myimage:latest...
...
2f111737752dcbf1a1c7e15e807fb48f55362b67356fc10c2ade24964e99fa09
现在我们深入探讨一下当你运行无根 Podman 容器时发生了什么。首先发生的事情是 Podman 需要设置用户命名空间。在下一节中,我将解释为什么需要这样做以及它是如何工作的。
6.1.1 图像包含多个用户标识符(UID)拥有的内容
在 Linux 中,用户标识符(UID)和组标识符(GID)被分配给进程并存储在文件系统对象上。文件系统对象也分配了权限值。Linux 根据这些 UID 和 GID 控制进程对文件系统的访问。这种访问称为自主访问控制(DAC)。当您登录 Linux 机器时,您的无根用户进程以单个 UID 运行——比如说1000——但容器镜像通常在其镜像层中包含多个不同的 UID。让我们检查运行我们的镜像所需的 UID。在这个例子中,您通过运行另一个容器来检查容器镜像中定义的所有 UID。
在以下命令中,使用 quay.io/rhatdan/myimage 镜像启动一个容器。您需要在容器内部以 root(- -user=root)身份运行容器,以检查镜像中的每个文件。
$ podman run --user=root --rm quay.io/rhatdan/myimage -- bash -c "find /
➥ -mount -printf \”%U=%u\n\” | sort -un" 2>/dev/null
由于这是一个临时容器,请使用--rm选项确保容器在运行完成后被删除。容器运行一个 Bash 脚本,该脚本查找容器中每个文件/目录关联的所有 UID 和用户。脚本将输出通过管道传递以显示唯一条目,并将stderr重定向到/dev/null以消除任何错误。
$ podman run --user=root --rm quay.io/rhatdan/myimage -- bash -c "find /
➥ -mount -printf \”%U=%u\n\” | sort -un" 2>/dev/null
0=root
48=apache
1001=default
65534=nobody
如您从输出中可以看到,我们的容器镜像使用了四个不同的 UID,如表 6.1 所示。
表 6.1 运行容器镜像所需的唯一 UID
| UID | Name | Description |
|---|---|---|
0 |
root |
拥有容器镜像中的大部分内容 |
48 |
apache |
拥有所有 Apache 内容 |
1001 |
default |
容器运行时的默认用户 |
65634 |
nobody |
分配给任何未映射到容器中的 UID |
为了将容器镜像拉取到您的家目录中,Podman 需要存储至少三个不同的 UID:0、48和1001。由于 Linux 内核阻止非特权账户使用超过一个 UID,因此您无法创建具有不同 UID 的文件。您需要利用用户命名空间。
用户命名空间
Linux 支持用户命名空间的概念,这是将主机上的 UID/GID 映射到命名空间内部的不同 UID 和 GID。以下是手册页对该概念的描述:
$ man user namespaces
...
用户命名空间隔离了与安全相关的标识符和属性——特别是用户 ID 和组 ID(见credentials(7))、根目录、密钥(见keyrings(7))和权限(见capabilities(7))。进程的用户和组 ID 可以在用户命名空间内外不同。特别是,进程可以在用户命名空间外有一个普通的无特权的用户 ID,同时在该命名空间内有一个用户 ID 为0;换句话说,进程在用户命名空间内的操作具有完全权限,但在命名空间外的操作是无特权的。
由于您的容器需要多个 UID,Podman 进程首先创建并进入一个用户命名空间,在那里它可以访问更多的 UID。Podman 还必须挂载几个文件系统来运行容器。这些挂载命令在用户命名空间之外不允许(包括挂载命名空间)。图 6.3 显示了用户命名空间内的 UID。

图 6.3 容器的用户命名空间映射
当我创建我的系统时,我使用了useradd程序来创建我的账户。它将3267分配给我作为 UID 和 GID,这些在/etc/passwd和/etc/group中定义。它还分配了 UID 100000-1065535——为我定义在/etc/subuid和/etc/subgid中的额外 UID 和 GID。让我们看看这些文件的内容:
$ cat /etc/subuid
dwalsh:100000:65536
Testuser:165536:65536
$ cat /etc/subgid
dwalsh:100000:65536
Testuser:165536:65536
您可以在您的系统上查看这些文件,您将看到类似的内容。在我的系统中,我还有一个testuser账户;useradd也为该用户添加了 UID/GID,在我分配之后立即开始。
在用户命名空间内,我能够访问 UID 3267(我的 UID)以及100000、100001、100002、...、165535,总共 65,537 个 UID。root 用户可以通过修改/etc/subuid和/etc/subgid文件来增加或减少这个数字。
useradd命令从 UID 100000开始,这样您就可以在 Linux 系统上拥有大约 99,000 个普通用户,以及为系统服务预留的 1,000 个 UID。内核支持超过 40 亿个 UID(2³² = 4,294,967,296)。由于useradd为每个用户分配 65,537 个,Linux 可以支持超过 60,000 个用户。选择 65,536(2¹⁶)这个数字是因为直到 Linux 内核 2.4,这曾是 Linux 系统上的最大用户数。让我们更深入地了解用户命名空间。
Linux 系统上的每个进程都在一个命名空间中,包括 init 进程和 systemd。这些都是主机命名空间。因此,每个进程都在一个用户命名空间中。您可以通过检查/proc 文件系统来查看您进程的用户命名空间映射。/proc/PID/uid_map 和/proc/PID/gid_map 包含了操作系统上每个进程的用户命名空间映射。/proc/self/uid_map 包含了当前进程的 UID 映射:
$ cat /proc/self/uid_map
0 0 4294967295
映射意味着从 UID 0开始的 UID 映射到 UID 0的范围为 4,294,967,295 个 UID。
另一种看待这种映射的方式是
UID 0->0, 1->1,...3267->3267,...,4294967294->4294967294.
基本上,没有映射,所以 root 是 root。我的 UID 3267映射到3267——它自己。
现在让我们进入用户命名空间,看看映射情况。Podman 有一个特殊的命令podman unshare,它允许你进入用户命名空间而不启动容器。它允许你在系统上作为常规进程运行的同时检查用户命名空间内发生的情况。
在以下命令中,我运行podman unshare以在我的账户的默认用户命名空间中启动cat /proc/self/uid_map:
$ podman unshare cat /proc/self/uid_map
0 3267 1
1 100000 65536
映射显示 UID 0映射到 UID 3267(我的 UID)的范围为1。然后 UID 1映射到 UID 100000的范围为65536个 UID。
任何未映射到用户命名空间的 UID 在用户命名空间内都报告为nobody用户。你之前在搜索容器镜像中的 UID 时已经看到了这一点:
$ podman run --user=root --rm quay.io/rhatdan/myimage -- bash -c "find /
➥ -mount -exec stat -c %u=%U {} \; | sort -un" 2>/dev/null
0=root
48=apache
1001=default
65534=nobody
如果你查看宿主机的/,你会发现它属于真正的 root:
$ ls -l -ld /
dr-xr-xr-x. 18 root root 242 Sep 21 22:32 /
如果你检查用户命名空间内的相同目录,你会发现它属于nobody用户:
$ podman unshare ls -ld /
dr-xr-xr-x. 18 nobody nobody 242 Sep 21 22:32 /
由于宿主机的 UID 0没有映射到用户命名空间,内核报告的 UID 是nobody用户。用户命名空间内的进程只能根据other或world权限访问nobody文件。在下面的例子中,你将启动一个 Bash 脚本,它显示用户在用户命名空间内是 root,但看到/etc/passwd属于用户nobody。你可以使用 grep 命令读取文件,因为/etc/passwd是可读的。但 touch 命令失败,因为即使是 root 也无法修改未映射到用户命名空间的 UID 的所有文件:
$ podman unshare bash -c "id ; ls -l /etc/passwd; grep *dwalsh*
➥ /etc/passwd; touch /etc/passwd"
uid=0(root) gid=0(root) groups=0(root),65534(nobody)
-rw-r--r--. 1 nobody nobody 2942 Sep 28 07:08 /etc/passwd
dwalsh:x:3267:3267:Dan Walsh:/home/dwalsh:/bin/bash
touch: cannot touch '/etc/passwd': Permission denied
在宿主机上查看你的家目录与用户命名空间内部,你会发现相同的文件被报告为属于你的 UID:
$ ls -ld /home/dwalsh
drwx------. 365 dwalsh dwalsh 24576 Sep 28 07:30 /home/dwalsh
在用户命名空间内,它们属于 root:
$ podman unshare ls -ld */home/dwalsh*
drwx------. 365 root root 24576 Sep 28 07:30 */home/dwalsh*
默认情况下,Podman 将你的 UID 映射到用户命名空间内的 root。Podman 默认为 root,因为我在本章开头指定了,大多数容器镜像假设它们以 root 身份启动。
我将给出最后一个例子。在用户命名空间内创建一个目录和一个文件,并使用chown命令将内容 UID 更改为1:1:
$ podman unshare bash -c "mkdir test;touch test/testfile; chown -R 1:1 test"
在用户命名空间外,你会看到测试文件的所有者是 UID 100000:
$ ls -l test
total 0
-rw-r--r--. 1 100000 100000 0 Sep 28 07:53 testfile
当你在用户命名空间内创建测试文件并将其chown为 UID/GID 1:1时,磁盘上的所有者实际上是 UID 100000/100000。记住,在用户命名空间内,UID 1映射到 UID 100000,所以当你创建用户命名空间内的 UID 1文件时,操作系统实际上创建的是 UID 100000。
如果你尝试在用户命名空间外删除文件,你会得到一个错误:
$ rm -rf test
rm: cannot remove 'test/testfile': Permission denied
在用户命名空间外,你只能访问你的 UID;你不能访问额外的 UID。
注意:在 3.1.2 节中,我展示了用户命名空间映射可能对容器卷造成问题,并讨论了你可以处理这些问题的方法。
重新进入用户命名空间,你可以删除文件:
$ podman unshare rm -rf test
希望你现在已经开始对用户命名空间有所感觉;podman 的 unshare 命令使得在用户命名空间内探索你的系统变得容易,并理解在无根容器中发生了什么。当运行无根容器时,Podman 需要的不仅仅是以 root 身份运行;它还需要访问一些称为 Linux 能力的 root 特权。
在 Linux 中,root 进程实际上并不都是同等强大的。Linux 将 root 权限分解为一系列 Linux 能力。具有所有 Linux 能力的 root 进程是全能的,而没有 Linux 能力的 root 进程则不允许操纵系统中的许多部分。例如,它不能读取非 root 文件,除非这些文件具有允许系统上所有 UID 读取的权限标志(世界可读)。
让我们看看能力如何与用户命名空间一起工作:
$ man capabilities
...
DESCRIPTION
For the purpose of performing permission checks, traditional UNIX
implementations distinguish two categories of processes: privileged
processes (whose effective user ID is 0, referred to as superuser or root),
and unprivileged processes (whose effective UID is nonzero). Privileged
processes bypass all kernel permission checks, while unprivileged processes
are subject to full permission checking based on the process's credentials
(usually: effective UID, effective GID, and supplementary group list).
Starting with kernel 2.2, Linux divides the privileges traditionally
associated with superuser into distinct units, known as capabilities, which
can be independently enabled and disabled. Capabilities are a per-thread
attribute.
Linux 目前大约有 40 个能力。例如,CAP_SETUID 和 CAP_SETGID 允许进程更改它们的 UID 和 GID。CAP_NET_ADMIN 允许你管理网络堆栈。
另一个名为 CAP_CHOWN 的能力允许进程更改磁盘上文件的 UID/GID。在先前的例子中,当你将测试目录 chown 为 1:1 时,你是在用户命名空间内使用了 CAP_CHOWN 能力:
$ podman unshare bash -c "mkdir test;touch test/testfile; chown -R 1:1 test"
当你在用户命名空间内运行时,你正在使用命名空间能力。你用户命名空间内的 root 用户拥有这些能力,而不仅仅是命名空间内定义的 UID 和 GID。具有命名空间能力 CAP_CHOWN 的进程允许将你用户命名空间内拥有的文件的所有权更改给也位于用户命名空间内的 UID。如果一个用户命名空间内的进程尝试将一个未映射到用户命名空间的文件(由 nobody 用户拥有)的所有权更改,该进程将被拒绝权限。同样,尝试将具有用户命名空间内未定义 UID 的文件的所有权更改的进程也会被拒绝。同样,CAP_SETUID 能力只允许进程将 UID 更改为用户命名空间内定义的 UID。
当 Podman 运行容器时,它需要为容器挂载几个文件系统。在 Linux 中,挂载文件系统需要 CAP_SYS_ADMIN 能力。从安全角度来看,在 Linux 上挂载文件系统可能是一件危险的事情。内核增加了对可以挂载的文件系统类型的额外控制,并要求你的用户命名空间进程也位于一个唯一的挂载命名空间中。在第十章中,你将看到 Podman 如何限制容器内命名空间 root 可用的 Linux 能力的数量。
挂载命名空间
挂载命名空间允许其内的进程挂载文件系统,其中挂载点对挂载命名空间外的进程是不可见的。在挂载命名空间内,你可以在 /tmp 上挂载一个 tmpfs,这将阻止命名空间内的进程看到 /tmp。在挂载命名空间外,进程仍然可以看到原始挂载和 /tmp 中的文件,但它们看不到你的挂载。
在无根容器中,Podman 需要挂载容器镜像中的内容以及 /proc、/sys、/dev 中的设备和一些 tmpfs 文件系统。为此,Podman 需要创建一个挂载命名空间:
$ man mount namespaces
...
Mount namespaces provide isolation of the list of mount points seen by the
processes in each namespace instance. Thus, the processes in each of the
mount namespace instances see distinct single-directory hierarchies.
当你执行 podman unshare 命令时,你实际上是在进入一个不同的挂载命名空间以及不同的用户命名空间。
你可以通过以下方式列出 /proc/self/ns/ 目录来检查进程的命名空间:
$ ls -l /proc/self/ns/user /proc/self/ns/mnt
lrwxrwxrwx. 1 dwalsh dwalsh 0 Sep 28 09:17 /proc/self/ns/mnt ->
➥ 'mnt:[4026531840]'
lrwxrwxrwx. 1 dwalsh dwalsh 0 Sep 28 09:17 /proc/self/ns/user ->
➥ 'user:[4026531837]'
注意,当你进入用户命名空间和挂载命名空间时,标识符会发生变化:
$ podman unshare ls -l /proc/self/ns/user /proc/self/ns/mnt
lrwxrwxrwx. 1 root root 0 Sep 28 09:17 /proc/self/ns/mnt ->
➥ 'mnt:[4026533087]'
lrwxrwxrwx. 1 root root 0 Sep 28 09:17 /proc/self/ns/user ->
➥ 'user:[4026533086]'
在以下测试中,你可以在 /tmp 上创建一个文件,然后尝试将其绑定挂载到 /etc/shadow。在命名空间外,内核正确地阻止了你挂载文件,如下面的输出所示:
$ echo hello > /tmp/testfile
$ mount --bind /tmp/testfile /etc/shadow
mount: /etc/shadow: must be superuser to use mount.
Once you enter the user namespace and mount namespace, your namespaced
process can successfully mount over the /etc/shadow file. You can see when
you run the following command that /etc/shadow is actually modified:
$ podman unshare bash -c "mount -o bind /tmp/testfile /etc/shadow; cat
/etc/shadow"
hello
一旦你退出 unshare,一切都会恢复正常。
用户命名空间和挂载命名空间
正如你之前看到的,当你覆盖挂载 /etc/shadow 文件时,你可能会欺骗一些 setuid 应用程序,如 /bin/su 或 /bin/sudo,让你获得完整的 root 权限。不允许无根用户挂载文件系统的原因是为了防止这种类型的攻击。
正如你所看到的,独立的挂载命名空间阻止了你影响主机对系统的视图,而你挂载的任何内容只会在挂载命名空间内可见。在用户命名空间内,容器已经有一个命名空间的根。对你的挂载点的攻击只能在用户命名空间内升级到 root,而不是主机上的真实 root。容器化进程不能更改它们的 UID (setuid) 为真实 root 或任何映射到用户命名空间的 UID。
即使有命名空间,Linux 内核也只允许挂载某些文件系统类型。许多文件系统类型对无根用户来说过于危险,因为它们可以访问内核的敏感部分。我正在与文件系统内核工程师合作,看看是否有方法可以锁定其他可以以无根模式挂载的文件系统类型,同时不影响系统的安全性。
截至 5.13 内核版本,内核工程师将原生 overlay 挂载添加到了允许挂载的列表中。当前允许的文件系统类型列在表 6.2 中。
表 6.2 当前在无根模式下支持挂载的文件系统
| 挂载类型 | 描述 |
|---|---|
bind |
在无根容器中大量使用。因为无根用户不允许创建设备,Podman 将主机上的/dev 挂载到容器中。Podman 还使用bind挂载来隐藏主机文件系统中的内容,使其对容器不可见。Podman 还将/dev/null 通过/proc 和/sys 中的文件bind挂载,以隐藏内容。第三章中描述的卷挂载也使用bind挂载。 |
binderfs |
用于 Android binder IPC 机制的文件系统。Podman 不支持它。 |
devpts |
虚拟文件系统挂载在/dev/pts 上。它包含用于终端模拟器的设备文件 |
cgroupfs |
用于操作 cgroups 的内核文件系统;无根容器可以使用cgroupfs在 cgroups v2 中操作 cgroups。在 v1 中不支持。它挂载在/sys/fs/cgroups 上。 |
FUSE |
用于在无根模式下使用fuse-overlayfs挂载容器镜像。在内核 5.13 之前,这是在无根模式下使用 overlay 文件系统的唯一方法。 |
procfs |
在容器内挂载在/proc 上。你可以检查容器内的进程。 |
mqueue |
实现 POSIX 消息队列 API。Podman 将此文件系统挂载在/dev/mqueue 上。 |
overlayfs |
用于挂载镜像。在fuse-overlayfs文件系统中表现更好。在特定用例中,它比原生 overlay 提供更多好处,例如 NFS 家目录。 |
ramfs |
动态可调整大小的基于 RAM 的 Linux 文件系统,目前与 Podman 不兼容。 |
sysfs |
挂载在/sys 上。 |
tmpfs |
用于隐藏内核文件系统目录,使其在/proc 和/sys 中的容器不可见。 |
6.2 无根 Podman 的内部机制
现在你已经对用户命名空间和挂载命名空间的工作原理以及为什么需要它们有了了解,让我们深入探讨 Podman 在运行容器时做了什么。当你首次登录后运行 Podman 容器时,Podman 读取/etc/subuid 和/etc/subgid 文件,寻找你的用户名或 UID。一旦 Podman 找到条目,它就会使用条目内容以及你的当前 UID/GID 为你生成一个用户命名空间。然后 Podman 启动podman pause进程以保持用户和挂载命名空间打开(图 6.4)。

图 6.4 Podman 启动 pause 进程以保持用户和挂载命名空间打开。
用户通常报告,在运行 Podman 容器后,当他们运行以下命令时,仍然可以看到一个podman进程在运行:
$ ps -e | grep podman
2541 ? 00:00:00 podman pause
Podman 命令的后续运行会将podman pause进程的命名空间连接起来。Podman 这样做是为了避免在用户命名空间上下文切换时发生竞争条件。pause进程会一直运行,直到你注销。你也可以执行podman system migrate命令来删除它。pause进程的作用是保持用户命名空间活跃,因为所有无根容器都必须在同一个用户命名空间中运行。如果不是这样,共享内容和其他命名空间(如从另一个容器共享网络命名空间)是不可能的。
注意,我经常有用户报告说,在更改/etc/subuid和/etc/subgid文件时,他们的容器不会立即反映这些更改。由于pause进程是在先前的用户命名空间设置下启动的,因此需要将其删除。执行podman system migrate命令会在用户命名空间内重新启动pause进程。
你可以随时终止pause进程,但 Podman 会在下一次运行时重新创建它。默认情况下,每个无根用户都有自己的用户命名空间,并且它们的所有容器都在同一个用户命名空间中运行。你可以细分用户命名空间,并使用不同的用户命名空间运行容器,但请注意,默认情况下,你只有 65,000 个 UID 可以工作。当运行有根容器时,在多个用户命名空间中运行多个容器要容易得多。现在用户命名空间和挂载命名空间已经创建,Podman 为容器的镜像创建存储,并设置挂载点以开始存储镜像。
6.2.1 拉取镜像
当拉取镜像(图 6.5)时,Podman 会检查容器镜像 quay.io/rhatdan/myimage 是否存在于本地容器存储中。如果存在,Podman 会设置容器网络(参见 6.2.3 节)。然而,如果容器镜像不存在,Podman 会使用 containers/image 库来拉取镜像。以下是 Podman 在拉取镜像时采取的步骤:
-
解析注册表的 IP 地址:quay.io。
-
通过 HTTPS 端口(
443)连接到 IP 地址。 -
使用 HTTP 协议开始拉取镜像的清单、所有层和配置。
-
查找 quay.io/rhatdan/myimage 的多个层或 blob。
-
从容器注册表同时复制所有层到主机。

图 6.5 Podman 从一个容器注册库拉取镜像并将其存储在容器存储中。
随着每一层被复制到主机,Podman 使用 containers/storage 库按顺序重新组装层,并在~/.local/share/containers/storage上为每个层创建一个 overlay 挂载点。如果没有先前的层,它将创建初始层。
接下来,containers/storage 将层的内容解压缩到新的存储层中。随着层的解压缩,containers/storage 会对 tar 包中的文件进行chown操作,将 UID/GID 更改为家目录。Podman 利用用户命名空间的CAP_CHOWN权限,如前几节所述。请记住,如果 TAR 文件中指定的 UID 或 GID 未映射到用户命名空间,Podman 将无法创建内容。
6.2.2 创建容器
一旦 containers/storage 库完成下载镜像和创建存储,Podman 将基于该镜像创建一个新的容器。Podman 将容器添加到 Podman 的内部数据库中。然后,它告诉 containers/storage 在磁盘上创建可写空间并使用默认的存储驱动程序,通常是overlayfs,将此空间挂载为新的容器层。新的容器层作为最终的读写层,并挂载在镜像之上。
注意:具有 root 权限的容器默认使用原生 Linux overlay 挂载。在无 root 模式下,对于版本高于 5.13 的内核或具有 rootless overlay 功能回滚的内核(RHEL 8.5 内核或更高版本也具有此功能)使用原生 overlay 挂载。在较旧的内核上,Podman 使用fuse-overlayfs可执行文件来创建层。在 Podman 中,overlay和overlay2是相同的驱动程序。
在这一点上,Podman 需要配置网络命名空间内的网络。
6.2.3 设置网络
在无 root Podman 中,您不能为容器创建完整的、独立的网络,因为无 root 进程不允许创建网络设备并修改防火墙规则。无 root Podman 使用 slirp4netns (github.com/rootless-containers/slirp4netns)来配置主机网络并为容器模拟 VPN。Slirp4netns 为无特权的网络命名空间提供用户模式网络(slirp)。见图 6.6。

图 6.6 Podman 创建一个网络命名空间并启动 slirp4netns 以中继网络连接。
注意:在具有 root 权限的容器中,Podman 使用 CNI 插件来配置网络设备。在无 root 模式下,尽管用户被允许创建和加入一个网络命名空间,但他们不允许创建网络设备。slirp4netns 程序模拟一个虚拟网络以连接主机网络到容器网络。更高级的网络设置需要具有 root 权限的容器。
记住,在我们的原始示例中,您指定了如下8080:8080端口映射:
$ podman run -d -p 8080:8080 --name myapp
registry.access.redhat.com/ubi8/httpd-24
Podman 配置 slirp4netns 程序在主机网络的端口 8080 上监听,并允许容器进程绑定到端口 8080。slirp4netns 命令创建一个 tap 设备,该设备注入到新的网络命名空间中,容器就位于其中。每个数据包都会从 slirp4netns 读取并模拟用户空间中的 TCP/IP 堆栈。容器网络命名空间之外的所有连接都会转换为无特权的用户可以在主机网络命名空间中运行的套接字操作。
注意 Linux TAP 设备创建一个用户空间网络桥。在用户空间中,TAP 设备可以模拟网络命名空间内的网络设备。命名空间内的进程与网络设备交互。从网络设备读取/写入的数据包通过 TUN/TAP 设备路由到用户空间程序:slirp4netns。
现在存储和网络配置完成,Podman 准备最终启动容器进程。
6.2.4 启动容器监控器:conmon
Podman 现在执行 conmon(容器监控器)以启动容器,并告诉它使用其配置的 OCI 运行时,通常是 crun 或 runc。当容器退出时,它还执行 podman container cleanup $CTRID 命令(见图 6.7)。conmon 在第 4.1 节中描述。

图 6.7 Podman 启动容器监控器,该监控器启动 OCI 运行时。
6.2.5 启动 OCI 运行时
OCI 运行时读取 OCI 规范文件并配置内核以运行容器(见图 6.8)。OCI 运行时执行以下操作:
-
为容器设置额外的命名空间。
-
配置 cgroups v2(cgroups v1 不支持无根容器)。
-
为运行容器设置 SELinux 标签。
-
将 /usr/share/containers/seccomp.json seccomp 规则加载到内核中。
-
设置容器的环境变量。
-
将任何卷绑定到 rootfs 中的路径。
-
将当前的
/切换到 rootfs 的/。 -
分叉容器进程。
-
执行任何 OCI 钩子程序,并将 rootfs 以及容器的 PID 1 传递给它们。
-
执行由镜像指定的命令。
-
退出 OCI 运行时,留下 conmon 监控容器。

图 6.8 conmon 启动 OCI 运行时,该运行时配置内核。
最后,conmon 将成功报告回 Podman(见图 6.9)。

图 6.9 Podman 和 OCI 运行时退出,留下容器在 conmon 监控下运行,并由 slirp4netns 提供网络。
Podman 命令现在退出,因为它在 --detach (-d) 模式下运行。
$ podman run -d -p 8080:8080 --name myapp
registry.access.redhat.com/ubi8/httpd-24
注意:如果您以后想让 Podman 与分离的容器交互,请使用 podman attach 命令,该命令连接到 conmon 套接字。conmon 允许 Podman 通过 STDIN、STDOUT 和 STDERR 文件描述符与容器进程交互,这些文件描述符是 conmon 监控的。
6.2.6 容器化应用程序运行至完成
应用进程可以自行退出,或者你可以通过执行 podman stop 命令来停止容器:
$ podman stop myapp
当容器进程退出时,内核向 conmon 进程发送 SIGCHLD 信号。反过来,conmon 执行以下操作:
-
记录容器的退出代码
-
关闭容器的日志文件
-
关闭 Podman 命令的
STDOUT/STDERR -
执行
podmancontainercleanup$CTRID命令 -
退出本身
podman container cleanup 命令关闭 slirp4netns 网络,并卸载所有容器的挂载点。如果你指定了 --rm 选项,容器将被完全删除——层将从容器/存储中移除,容器定义将从数据库中删除。
摘要
-
运行无根容器比运行有根容器更安全。
-
用户命名空间使普通用户能够操作多个 UID,是运行容器的关键。
-
挂载命名空间允许 Podman 在用户命名空间内挂载文件系统。
-
Podman 使用 slirp4netns 为容器提供网络访问。
-
Podman 启动
conmon进程以监控容器。
第三部分. 高级主题
在本书的第三部分,您将了解如何使用 Podman 的高级方法。这部分讨论了将 Podman 集成到您的系统中,以及 Podman 如何与其他工具和编排器协同工作。
在第七章中,我介绍了 systemd 集成。Podman 是为了完全集成到系统中而开发的,并利用了初始化系统:systemd。systemd 可以轻松地在 Podman 容器中运行,本章将向您展示如何做到这一点。同样,Podman 也可以在 systemd 服务中运行,并提供命令,允许您自动创建服务配置文件以实现这一功能。
第八章向您展示了 Podman 如何与 Kubernetes 协同工作。Podman 不是 Kubernetes 下的容器引擎,但它可以与 Kubernetes YAML 文件协同工作。因为 Kubernetes YAML 文件用于定义在 Kubernetes 中运行的应用程序,所以 Podman 使得将应用程序从完全编排的环境移动到单个节点,或者从单个节点移动到完全编排的环境变得容易。这个特性使得您更容易开发最终在 Kubernetes 下运行的应用程序,或者通过在您的笔记本电脑上本地运行这些应用程序来调试在 Kubernetes 下发生的问题。当在单个节点上运行多个容器时,Kubernetes YAML 是docker-compose YAML 的一个很好的替代品。
第九章介绍了 Podman 作为服务的概念,它允许编写用于使用 RESTful API 的工具生成和管理 Podman 的 pods 和容器。像docker-compose和其他基于 docker-py 构建的 Python 工具可以与 Podman 服务接口,从而完全不需要 Docker。Podman 服务甚至允许在远程系统(如 Windows、macOS 和 Linux)上运行的 Podman 与 Linux Podman 容器协同工作。
7 与 systemd 的集成
本章涵盖
-
在容器中将 systemd 作为主进程运行
-
从现有容器生成 systemd 单元文件
-
套接字激活的容器化服务
-
使用
sd-notify容器化服务 -
使用 journald 作为日志驱动程序和事件后端的优势
-
使用 Podman 和 systemd 管理边缘设备上容器化服务的生命周期
Systemd 是 Linux 的事实上的初始化系统。几乎每个 Linux 发行版都将 systemd 作为内核启动后的第一个进程默认启动,然后启动所有服务,包括用户的登录会话。Podman 拥抱 systemd 的力量,并使用它来启动许多服务。在启动引导时的容器化服务时,Podman 鼓励用户使用 systemd 单元文件与 Podman 命令一起使用。单元文件是 systemd 所说的配置文件。Systemd 支持几种不同类型的单元文件,包括可以定义服务的服务文件,你希望 systemd 管理这些服务。SystemD.socket 是 systemd 使用的另一种类型的单元文件(见第 7.6 节)。systemd 服务单元文件是向世界分享你的容器化服务的一种方式。如图 7.1 所示,Podman 的 fork/exec 模型赋予了 systemd 跟踪容器化服务内进程的能力。

图 7.1 Systemd 执行 Podman 容器
Systemd 将一个单元文件服务(称为作用域)内的所有进程放入相同的 cgroup 层次结构中。然后它使用 PID cgroup 来跟踪所有进程,并使用这些信息来管理服务。使用客户端-服务器方法的容器引擎阻止 systemd 跟踪容器化进程。
Podman 也利用了其他服务,正如你将在本章中看到的,来处理容器的自动重启、自动更新以及容器化服务的常规管理。在本章中,你将接触到许多 Podman 和 systemd 的功能,但首先你将在 Podman 容器中运行 systemd。
7.1 在容器中运行 systemd
当容器化技术刚开始流行时,许多传教士教授了微服务概念。微服务被定义为容器中的一个专业服务。这个单一的服务作为容器中的初始 PID(PID 1)运行,并将其日志直接写入stdout和stderr。Kubernetes 假设微服务,因此从它运行的容器中收集stdin/stderr的日志。图 7.2 显示了 Podman 运行微服务。

图 7.2 Podman 运行三个微服务
另一个想法是在容器内以初始 PID 运行 systemd,然后允许 systemd 在容器内启动一个或多个服务。这种观点认为,容器化服务应以与在虚拟机内启动相同的方式启动。因为服务包设计者(例如,RPM 和 APT)将 systemd 单元文件作为在操作系统内启动其服务的一种精确方式,容器开发者应该利用这些单元文件。这种方法允许在同一个容器内运行多个服务,利用本地通信路径,并加快将大型多服务应用程序转换为容器,然后随着时间的推移,将每个服务分解为其自己的微服务。
systemd 在容器中的最后一个巨大优势是 init 系统处理僵尸进程的清理。在 Linux 中,当进程退出时,内核向父进程发送信号 SIGCHLD,父进程应该收集退出进程的退出状态。当父进程读取退出状态时,内核从系统中删除该进程。如果没有父进程读取退出状态,退出的进程将保留在退出状态,被称为 僵尸进程。init 系统,systemd,回收系统中的大多数进程。在容器中,容器内运行的初始进程应该回收这些进程。有时容器进程会退出,如果 PID1 不回收它们,它们就会徘徊,永远不会消失。
注意:podman-run 命令支持 –init 选项,该选项将启动一个微小的 init 程序,专门用于回收僵尸进程。
Podman 被设计为支持两种方法——微服务和多服务容器。图 7.3 显示了 systemd 在容器内运行多服务应用程序。

图 7.3 Podman 在容器中运行 systemd 并包含三个服务
Podman 检查容器的 cmd 选项,然后启动 systemd 以进行 init 或系统。然后它自动以 systemd 模式启动容器。
以下列表显示了所有触发 Podman 在 systemd 模式下运行的命令:
-
/sbin/init
-
/usr/sbin/init
-
/usr/local/sbin/init
-
/*/systemd(任何以 systemd 命令结尾的路径)
registry.access.redhat.com/ubi8-init 图像是一个旨在以 systemd 模式运行的图像示例。
下载 ubi8-init 图像,并检查命令:
$ podman pull ubi8-init
Resolved "ubi8-init" as an alias (/etc/containers/registries.conf.d/
➥ 000-shortnames.conf)
Trying to pull registry.access.redhat.com/ubi8-init:latest...
...
8cb83279f877a4bf3412827bf71c53188c3983194bd4663a1fc1378360844463
$ podman inspect ubi8-init --format '{{ .Config.Cmd }}'
[/sbin/init]
systemd 需要环境以某种方式配置;否则,systemd 会尝试纠正环境。下一节将解释 Podman 如何满足 systemd 的要求。
7.1.1 容器化 systemd 要求
systemd 对其启动的环境做出一些假设,例如 /run 和 /tmp 需要挂载上 tmpfs。当环境不正确时,systemd 会尝试通过在 /run 和 /tmp 上挂载 tmpfs 来纠正它。挂载需要在容器内具有 CAP_SYS_ADMIN 权限,这在非特权容器中是不允许的。然后 systemd 会崩溃。
为了解决这个问题,在检查容器镜像的入口点和 CMD 以查看它们是否运行 systemd 之后,Podman 修改容器环境以匹配 systemd 的期望。当 systemd 看到挂载时,它会跳过它们,允许 systemd 在锁定环境中运行。表 7.1 描述了 systemd 需要的要求和 Podman 提供的要求,以便在非特权容器中成功运行。
表 7.1 在非特权容器中运行 systemd 的要求
| Systemd expectations | 描述 |
|---|---|
| /run on a tmpfs | systemd 需要挂载在 /run 上的 tmpfs。如果 /run 没有使用 tmpfs 挂载,systemd 将尝试在 /run 上挂载一个 tmpfs。默认的锁定容器被阻止挂载,因此 systemd 将失败。 |
| /tmp on a tmpfs | 类似于 /run,systemd 将尝试在 /tmp 上挂载一个 tmpfs,如果那里还没有挂载的话。 |
| /var/log/journald as a tmpfs | 容器内的 systemd 期望能够写入 /var/log/journald,因此 Podman 挂载一个 tmpfs 来实现这一点。 |
container environment variable |
systemd 利用 container 环境变量已设置的事实来改变其一些默认行为,使其在容器内运行得更好。 |
STOPSIGNAL=SIGRTMIN+3 |
与系统上的大多数进程不同,systemd 忽略 SIGTERM,并且只有在接收到信号 SIGRTMIN+3 (37) 时才会干净地退出。 |
7.1.2 Podman 容器在 systemd 模式下
您可以使用 --systemd =always 标志检查基于 systemd 的容器的环境。首先,使用 --systemd=always 标志启动一个启用 systemd 模式的容器。此选项即使在未运行 systemd 的情况下也会以 systemd 模式运行容器,这使得调试环境更容易。您现在可以 exec systemd 并将其作为 PID1 启动:
$ podman create –rm –name SystemD -ti –systemd=always ubi8-init sh
774a50204204768edd73f178b6afdf975cf9353e3b90af9df77273d639f60ac3
使用 podman inspect 检查容器的 StopSignal;Podman 将其设置为 37 (SIGRTMIN+3):
$ podman inspect SystemD --format '{{ .Config.StopSignal}}'
37
现在,启动容器,查看 /run 和 /tmp 的挂载情况;您将看到两者都使用 tmpfs 挂载。最后,检查容器环境变量是否已设置:
$ podman start --attach SystemD
# mount | grep -e /tmp -e /run | head -2
tmpfs on /tmp type tmpfs
➥ (rw,nosuid,nodev,relatime,context="system_u:object_r:container_file_t:s0:
➥ c37,c965",uid=3267,gid=3267,inode64)
tmpfs on /run type tmpfs
➥ (rw,nosuid,nodev,relatime,context="system_u:object_r:container_file_t:s
➥ 0:c37,c965",uid=3267,gid=3267,inode64)
# printenv container
Oci
如果您仅运行基于 ubi8-init 的容器,您将看到 systemd 启动:
$ podman run -ti ubi8-init
SystemD 239 (239-45.el8_4.3) running in system mode. (+PAM +AUDIT +SELINUX
➥ +IMA -APPARMOR +SMACK +SYSVINIT +UTMP +LIBCRYPTSETUP +GCRYPT +GNUTLS
➥ +ACL +XZ +LZ4 +SECCOMP +BLKID +ELFUTILS +KMOD +IDN2 -IDN +PCRE2
➥ default-hierarchy=legacy)
Detected virtualization container-other.
Detected architecture x86-64.
Welcome to Red Hat Enterprise Linux 8.4 (Ootpa)!
Set hostname to <26bbf9077219>.
Initializing machine ID from random generator.
Failed to read AF_UNIX datagram queue length, ignoring:
➥ No such file or directory
[ OK ] Listening on initctl Compatibility Named Pipe.
[ OK ] Reached target Swap.
[ OK ] Listening on Journal Socket (/dev/log).
[ OK ] Listening on Journal Socket.
...
在这里,您可以注意到 systemd 通过按 Ctrl-C 忽略 SIGTERM。因此,要停止此容器,您需要进入不同的终端并执行
# podman stop -l
这导致 Podman 向容器中的 systemd 发送正确的 STOPSIGNAL (SIGRTMIN+3)。当 systemd 收到此信号时,它会立即关闭。
现在你已经了解了 systemd 的要求,是时候创建一个 systemd 将要运行的服务了。在下面的章节中,你将构建一个基于 systemd 的 Apache 服务,该服务将在容器内与 systemd 一起运行。
7.1.3 在 systemd 容器内运行 Apache 服务
在本节中,你将创建一个 Containerfile,使用 ubi8-init 作为基础镜像,然后安装 Apache httpd。最后,你将启用此服务并设置我们一直在使用的 Apache 脚本。
创建一个 Containerfile:
$ cat << _EOF > /tmp/Containerfile
FROM ubi8-init
RUN dnf -y install httpd; dnf -y clean all
RUN systemctl enable httpd.service
_EOF
回想一下,FROM ubi8-init 这行命令将告诉 Podman 使用 ubi8-init 镜像作为新镜像的基础镜像:
FROM ubi8-init
RUN dnf -y install httpd; dnf -y clean all
RUN systemctl enable httpd.service
RUN dnf -y install httpd; dnf -y clean all 这行命令告诉 Podman 运行一个容器,执行 dnf 命令并在 ubi8-init 镜像上安装 httpd 软件包。第二个 dnf 命令删除多余的文件并记录安装过程中创建的 dnf 日志,因为这些文件没有必要包含在镜像中:
FROM ubi8-init
RUN dnf -y install httpd; dnf -y clean all
RUN systemctl enable httpd.service
最后的 RUN systemctl enable httpd.service 命令告诉 Podman 启动另一个构建容器并执行 systemctl 命令以启用 httpd .service。当 systemd 在从新创建的镜像创建的容器上运行时,httpd 服务将被启动:
FROM ubi8-init
RUN dnf -y install httpd; dnf -y clean all
RUN systemctl enable httpd.service
现在使用 podman 的 build 命令构建镜像,并将镜像命名为 my-systemd:
$ podman build -t my-systemd /tmp
STEP 1/3: FROM ubi8-init
STEP 2/3: RUN dnf -y install httpd; dnf -y clean all
Updating Subscription Management repositories.
Unable to read consumer identity
...
COMMIT my-systemd
--> 104fa99d9a2
Successfully tagged localhost/my-systemd:latest
104fa99d9a2138404039cf15b470ab04784cdaab2226f29bd8343f8e24ec60e2
现在运行一个基于 systemd 的容器镜像,并从主机挂载一个卷。由于默认的 Apache 软件包监听端口 80,使用 --p 8080:80,正如你所学的,这会将端口 8080 映射到容器内的端口 80。使用来自第 3.1 节的 html 文件夹和 index.xhtml 文件:
$ podman run -d --rm -p 8080:80 -v ./html:/var/www/html:Z my-systemd
71f1678084390925b7488f68ab58cd55e16009d69b717045b8ed5ef14e8599ce
你在 ./html 目录中挂载了卷 (-v ./html/:/var/www/html:Z),并包含 goodbye world index.xhtml 文件:
$ podman run -d --rm -p 8080:80 -v ./html:/var/www/html:Z my-systemd
启动一个网页浏览器来检查容器化服务是否正常工作(如图 7.4 所示):
$ web-browser localhost:8080

图 7.4 显示基于系统容器的镜像运行你的内容的网页浏览器窗口
注意,在设计镜像时,你不需要特别处理 HTTPD 服务器进程;你的容器以与虚拟机相同的方式运行 HTTPD。如果你需要在镜像内启用另一个服务,你可以通过安装软件包并启用其单元文件轻松地做到这一点。
要查看这种设置的不足之处,你可以运行 podman 的 logs 命令:
$ podman logs 71f1678084
没有输出。由于 systemd 在容器的 PID1 上运行,它没有将任何输出写入日志。你需要进入容器并使用 journalctl 或读取 /var/log/httpd/error_log 中的 httpd 日志来查看是否有任何问题。现在你已经看到了如何在容器中使用 systemd,是时候看看你如何可以使用 systemd 和 Podman 利用高级 systemd 功能了。
7.2 Journald 用于日志和事件
systemd 日志(journald)是 Linux 上的现代日志系统。它是一个收集和存储日志数据的系统服务。使用 journald 的一个主要优势是记录永久存储,并且日志轮转是内置的。Podman 默认使用 journald 存储其日志数据。
7.2.1 日志驱动程序
Podman 默认在以 systemd 作为初始化系统的系统上使用 journald 作为日志驱动程序。如果你在没有 systemd 运行的容器中运行 Podman,它将回退到使用文件驱动程序。在选择日志驱动程序时,需要考虑的一个因素是当容器被移除时日志数据是否会持久化。
第二个关注点是日志文件的大小。日志记录了容器内的所有 stdout 和 stderr。运行时间非常长的容器可以创建大量的日志内容。只有 journald 驱动程序内置了日志轮转,由 systemd 提供。如果你使用 k8s-file 驱动程序,你的系统可能会耗尽空间。表 7.2 展示了可用的日志驱动程序以及日志数据是否持久化以及系统是否支持日志轮转。
表 7.2 日志驱动程序选项
| Library | 描述 | 容器移除后持久化日志 | 日志轮转 |
|---|---|---|---|
| Journald | 使用 systemd 日志存储日志信息 | ✔ | ✔ |
| k8s-file | 以 Kubernetes 格式将日志数据存储在平面文件中 | ✘ | ✘ |
| None | 不存储任何日志信息 | ✘ | ✘ |
虽然我推荐你使用 journald 作为日志驱动程序,但根据系统配置,一些无根用户可能不允许使用 journald。在其他情况下,例如在容器内运行 Podman,journald 可能不可用。
你可以通过以下命令查看系统上的默认日志驱动程序:
$ podman info --format '{{ .Host.LogDriver }}'
k8s-file
由于某种原因,你的主机系统设置被设置为将日志记录到 k8s-file。使用 containers.conf 覆盖系统默认日志驱动程序很简单。在主目录中创建一个 log_driver.conf 文件,$HOME/.config/containers/containers .conf.d,并设置 log_driver 选项:
$ mkdir -p $HOME/.config/containers/containers.conf.d
$ cat > $HOME/.config/containers/containers.conf.d/log_driver.conf << _EOF
[containers]
log_driver="journald"
_EOF
$ podman info --format '{{ .Host.LogDriver }}'
journald
很好。接下来,你将通过使用 --rm 选项启动容器来移除容器退出时的日志驱动程序的好处:
$ podman run --rm --name test2 ubi8 echo "Check if logs persist"
Check if logs persist
检查日志是否记录了容器启动的情况:
$ journalctl -b | grep "Check if logs persist"
Nov 10 06:19:54 fedora conmon[657915]: Check if logs persist
如果你使用 k8s_file 选项启动,当容器被移除时,Podman 会移除日志文件。不会留下任何日志条目。与日志一样,Podman 支持使用 systemd 日志存储事件。
7.2.2 事件
Podman 事件记录了容器生命周期中的不同步骤;例如,你可以看到你运行的最后一个容器的启动事件:
$ podman events --filter event=start --since 1h
2021-11-10 06:35:06.780429582 -0500 EST container start
➥ ecf04c4802bb120f34533560fbfc19ab023bcce63d48945ab0e8ff06cc6eeda1
...
使用 Podman info 命令检查默认的事件记录器:
$ podman info --format '{{ .Host.EventLogger }}'
journald
你可以通过在 containers.conf 中使用 events_logger 选项来修改事件记录器,类似于你为 log_driver 所做的修改。表 7.3 展示了可用的日志记录选项。
表 7.3 事件记录器选项
| Library | 描述 | 重启后持久化日志数据 | 日志轮转 |
|---|---|---|---|
| Journald | systemd 日志将记录所有事件。 | ✔ | ✔ |
| File | 将事件存储在文件中,通常在 /run。 | ✘ | ✘ |
| None | 不存储任何事件信息。 | ✘ | ✘ |
如果你的系统使用文件事件记录器,事件后端文件存储在 $XDG_RUNTIME_DIR,对于 rootless 用户默认在 tmpfs 上。事件后端文件会持续增长,直到你使用文件驱动程序重启系统。这可能会导致容器运行失败或系统空间不足,因为事件后端不会自动回滚,除非你使用 journald。此外,当你重启时,事件日志会丢失。切换到 journald 可以保留事件并处理事件日志的轮换。我建议你保持日志驱动程序和事件驱动程序具有相同的值,无论是 journald、平面文件还是 none,如果你不需要事件和日志。
你已经检查了在 Podman 中使用 systemd 以及 journald 来管理日志文件和事件。现在,你将了解如何设置系统,以便在系统启动时使用 systemd 自动运行容器。
7.3 启动时启动容器
正如你在第一章中学到的,Podman 不会作为守护进程运行,这意味着你无法依赖守护进程在启动时自动启动容器。通常,你需要通过 systemd 运行容器化服务。Systemd 可以配置为安装、运行和管理容器化应用程序。许多应用程序以容器镜像的形式提供,并将包含用于启动的系统服务单元文件。systemd 提供了许多功能,以改善容器化服务在你的系统上的运行方式。
7.3.1 容器重启
Podman 依赖于 systemd 通过在 systemd 单元文件中启动 Podman 来启动容器化服务。podman run 命令允许你选择是否在容器未由用户停止时重启容器(例如,如果容器崩溃或系统重启)。表 7.4 显示了 Podman 可用的重启策略。
systemd 帮助的一个简单方法是使用 always 重启策略启动容器。如果你设置了 always 选项并且系统重启,Podman 将使用两个 systemd 服务自动重启标记为 --restart=always 的容器。一个服务处理 rootful 容器,另一个处理系统上的所有 rootless 容器。
表 7.4 重启策略
| 选项 | 描述 | 启动时重启 |
|---|---|---|
no |
容器退出时不重启。 | ✘ |
on-failure[:max_retries] |
当容器以非零退出码退出时重启容器,无限重试或直到达到可选的 max_retries 重试次数。 |
✘ |
always 或 unless-stopped |
当容器退出时重启容器,无论状态如何,无限重试。 | ✔ |
当您的系统启动时,systemd 会运行以下 Podman 命令以启动任何设置为 always 重启策略的容器:
/usr/bin/podman start --all --filter restart-policy=always
注意 Podman 随附两个 systemd 服务文件,用于重启服务——一个用于 rootful,一个用于 rootless:
/usr/lib/systemd/system/podman-restart.service
/usr/lib/systemd/user/podman-restart.service
--restart=always 工作得很好,但它要求你在系统上创建一个容器,并且即使容器失败也会重启容器。systemd 是为了运行服务而设计的;你将在下一节中看到,你可以使用 Podman 轻松创建一个服务单元文件来运行你的容器化服务。
7.3.2 Podman 容器作为 systemd 服务
如你所见,systemd 使用单元文件来指定如何运行一个服务。图 7.5 展示了 systemd 如何与 Podman 协同启动一个容器。

图 7.5 Podman fork/exec 架构非常适合 systemd 服务管理。
在图 7.5 中,我指出 systemd 能够监控 systemd 单元文件中运行的所有进程。这使得它能够轻松地启动和停止进程。conmon 进程也在 systemd 服务中运行,监控容器进程。conmon 仍然会在容器退出时注意到,保存其退出代码,并干净地关闭容器环境。systemd 并不知道容器;它只知道单元文件中运行的进程,包括容器进程。
Systemd 单元文件有许多不同的方式来运行和启动进程,而 Podman 提供了许多不同的选项来运行容器。配置单元文件可能非常复杂。许多用户已经编写了单元文件来运行容器,但在这样做时遇到了一些问题。最常见的问题是,在单元文件中运行 podman run --detach 命令。当 Podman 命令断开连接并退出时,systemd 假设服务已完成并将其关闭,即使 conmon 和容器仍在运行。我经常从用户那里听到以下问题:“我应该如何在 systemd 单元文件中运行我的容器?”
Podman 有一个功能可以生成具有最佳默认设置的单元文件。首先,从 myimage 重新创建容器,然后使用 podman systemd generate 创建一个 systemd 服务单元文件来管理你的容器。
基于你在第二章中创建的镜像创建一个容器:
$ podman create -p 8080:8080 --name myapp quay.io/rhatdan/myimage
...
8879112805e976b4b6d97c07c9426bdde22ee4ffc7ba4daa59965ae25aa08331
现在用 Podman 生成一个基于此容器的单元文件:
$ mkdir -p $HOME/.config/systemd/user
$ podman generate systemd myapp > $HOME/.config/systemd/user/myapp.service
注意在 myapp.service 脚本中,Podman 创建了一个 ExecStart 字段。在服务启动时,systemd 将执行 ExecStart 命令,该命令简单地启动你创建的容器:
ExecStart=/usr/bin/podman start 8879112805…
在服务停止时,systemd 执行单元文件中添加的 ExecStop 命令:
ExecStop=/usr/bin/podman stop -t 10 8879112805...
Let's take a look at the generated service file:
$ cat $HOME/.config/systemd/user/myapp.service
# container-8879112805e976b4b6d97c07c9426bdde22ee4ffc7ba4daa59965ae25aa08331.service
# autogenerated by Podman 3.4.1
# Wed Nov 10 08:23:06 EST 2021
[Unit]
Description=Podman container-8879112805...service
Documentation=man:podman-generate-SystemD(1)
Wants=network-online.target
After=network-online.target
RequiresMountsFor=/run/user/3267/containers
[Service]
Environment=PODMAN_SYSTEMD_UNIT=%n
Restart=on-failure
TimeoutStopSec=70
ExecStart=/usr/bin/podman start 8879112805...
ExecStop=/usr/bin/podman stop -t 10 8879112805...
ExecStopPost=/usr/bin/podman stop -t 10 8879112805...
PIDFile=/run/user/3267/containers/overlay-containers/8879112805.../userdata/conmon.pid
Type=forking
[Install]
WantedBy=multi-user.target default.target
为了让这一切都能正常工作,你需要告诉 systemd 重新加载其数据库,这样它就会注意到单元文件中的更改:
$ systemctl --user daemon-reload
使用以下命令启动服务:
$ systemctl --user start myapp
检查服务是否正在运行:
$ systemctl --user status myapp
• myapp.service - Podman container-8879112805....service
Loaded: loaded (/home/dwalsh/.config/SystemD/user/myapp.service;
➥ disabled; vendor preset: disabled)
Active: active (running) since Thu 2021-11-11 07:19:08 EST; 3min 9s ago
...
$ podman ps
CONTAINER ID IMAGE COMMAND
➥ CREATED STATUS PORTS NAMES
8879112805e9 quay.io/rhatdan/myimage:latest /usr/bin/run-http...
➥ 23 hours ago Up 5 minutes ago 0.0.0.0:8080->8080/tcp myapp
现在,你可以通过 localhost 端口 8080 运行浏览器来查看它是否正在运行(见图 7.6):
$ web-browser localhost:8080

图 7.6 浏览器窗口连接 myapp
要关闭服务,执行
$ systemctl --user stop myapp
生成 systemd 服务文件的能力为用户提供了很多灵活性,并且故意模糊了容器与主机上任何其他程序或服务之间的区别。
这个单元文件的一个问题是它特定于你创建的容器。你需要首先创建容器并生成特定的服务文件。你不能将单元文件交给另一个用户,让他们在你的机器上运行你的服务。幸运的是,Podman 支持创建更便携的 systemd 单元文件:podman generate systemd --new。
7.3.3 分发 systemd 单元文件以管理 Podman 容器
如前所述,podman generate systemd command 生成了一个单元文件,该文件启动并停止了一个现有的容器。--new 标志指示 Podman 生成运行、停止和删除容器的单元。在同一个容器中试一试:
$ podman generate systemd --new myapp > $HOME/.config/systemd/user/
➥ myapp-new.service
注意,使用 --new 选项时,Podman 创建了一个略微不同的单元文件。检查以下 ExecStart 命令,你会看到你用来创建容器的原始 podman create -p 8080:8080 --name myapp quay.io/rhatdan/myimage 命令已被更改为使用 podman run 命令。同时注意,Podman 添加了额外的选项,以使在 systemd 下运行更容易(--cidfile =%t/%n.ctr-id --cgroups=no-conmon --rm --sdnotify=conmon -d --replace)。
Podman 现在添加了 ExecStop 命令 (/usr/bin/podman stop --ignore --cidfile=%t/%n.ctr-id),它告诉 systemd 当有人执行 systemctl stop 或系统关闭时如何停止容器。
最后,Podman 添加了一个 ExecStopPost 命令 (/usr/bin/podman rm -f --ignore --cidfile=%t/%n.ctr-idType=notify),systemd 在 ExecStop 命令完成后执行该命令。Podman 命令从系统中删除容器:
$ cat $HOME/.config/systemd/user/myapp-new.service
# container-8879112805....service
# autogenerated by Podman 3.4.1
# Thu Nov 11 07:40:34 EST 2021
[Unit]
Description=Podman container-8879112805...service
Documentation=man:podman-generate-SystemD(1)
Wants=network-online.target
After=network-online.target
RequiresMountsFor=%t/containers
[Service]
Environment=PODMAN_SystemD_UNIT=%n
Restart=on-failure
TimeoutStopSec=70
ExecStartPre=/bin/rm -f %t/%n.ctr-id
ExecStart=/usr/bin/podman run --cidfile=%t/%n.ctr-id --cgroups=no-conmon –
➥ rm --sdnotify=conmon -d --replace -p 8080:8080 --name myapp
➥ quay.io/rhatdan/myimage
ExecStop=/usr/bin/podman stop --ignore --cidfile=%t/%n.ctr-id
ExecStopPost=/usr/bin/podman rm -f --ignore --cidfile=%t/%n.ctr-idType=notify
NotifyAccess=all
[Install]
WantedBy=multi-user.target default.target
你可以从系统中删除容器和镜像,当你告诉 systemctl 启动服务时,Podman 将拉取镜像并创建一个新的容器。这意味着 myapp-new.service 单元文件可以与其他用户共享,当他们运行服务时,Podman 同样会拉取镜像并在他们的系统上运行容器,而他们从未创建过容器。表 7.5 显示了根据你是否使用了 --new 标志添加到单元文件中的不同命令。
表 7.5 单元文件之间的差异
| 选项 | 命令 |
|---|---|
使用 --new |
ExecStart=/usr/bin/podman run ...--cidfile=%t/%n.ctr-id --cgroups=no-➥ conmon --rm --sdnotify=conmon -d --replace -p 8080:8080 --name➥ myapp quay.io/rhatdan/myimage``ExecStop=/usr/bin/podman stop --ignore --cidfile=%t/%n.ctr-id``ExecStopPost=/usr/bin/podman rm -f --ignore --cidfile=%t/%n➥ .ctr-idType=notify |
不使用 --new |
ExecStart=/usr/bin/podman start 8879112805...``ExecStop=/usr/bin/podman stop -t 10 8879112805...``ExecStopPost=/usr/bin/podman stop -t 10 8879112805... |
一旦你的容器化服务在多台机器上运行,你需要考虑如何维护它。Podman 有一种无需人工干预的方式来维护:自动更新。
7.3.4 自动更新 Podman 容器
在第二章中,我们讨论了容器镜像像臭奶酪一样老化。当容器镜像通过新的软件或漏洞修复进行更新时,你需要联系这些机器,拉取更新的镜像,并重新创建容器化服务。当机器自己管理自己的更新时,这要少得多的人工干预。
想象一下,你配置了一个服务在数百个节点上运行在容器镜像上。几个月后,你在镜像中的应用程序中添加了新功能,或者更重要的是,发现了一个新的 CVE。现在你需要更新镜像,然后在所有节点上重新创建服务。
Podman 通过自动更新自动化此过程;每个节点都会监视容器注册库中出现的新镜像。当镜像出现时,节点会拉取镜像并重新创建容器。无需人工交互。
Podman 自动更新功能使你能够在边缘用例中使用 Podman,一旦连接到网络,就更新工作负载,并将故障回滚到已知良好状态。此外,在远程数据中心或物联网(IoT)设备上实施边缘计算时,运行容器是至关重要的。自动更新使你能够在边缘用例中使用 Podman,一旦连接到网络就更新工作负载,并降低维护成本。
要实现此行为,Podman 需要容器具有特殊的标签,--label "io.containers.autoupdate=registry",并且容器必须以由podman generate systemd --new生成的 systemd 单元运行。表 7.6 描述了可用的自动更新模式。
表 7.6 自动更新模式
io.containers.autoupdate |
描述 |
|---|---|
registry |
Podman 连接到容器注册库,并检查是否有与创建容器时使用的不同镜像可用;如果有,Podman 将更新容器。 |
local |
Podman 连接到容器注册库,但将本地镜像与创建容器时使用的镜像进行比较;如果它们不同,Podman 将更新容器。 |
首先,如果 systemd 服务正在运行,请停止它,并删除现有的myapp容器:
$ systemctl --user stop myapp-new
$ podman rm myapp --force -t 0
使用特殊标签"io.containers.autoupdate=registry"重新创建 myapp 容器:
$ podman create --label "io.containers.autoupdate=registry" -p 8080:8080
➥ --name myapp quay.io/rhatdan/myimage
397ad15601868eb6fd77fe0b67136869cde9e0ffad90ee5095a19de5bb4b999e
使用--new选项重新创建 systemd 单元文件:
$ podman generate systemd myapp --new > $HOME/.config/systemd/user/
➥ myapp-new.service
通过执行daemon-reload告诉 systemd 单元文件已更改,并启动服务:
$ systemctl --user daemon-reload
$ systemctl --user start myapp-new
myapp-new服务现在已准备好自动更新。当您执行podman auto-update命令时,Podman 会检查运行中的容器是否有设置为image的io.containers.autoupdate标签。对于带有该标签的每个容器,Podman 会联系容器注册表,检查镜像自容器创建以来是否已更改。如果镜像已更改,Podman 将重启相应的 systemd 单元。回想一下,在 systemd 重启时,以下步骤会发生:
-
systemd 通过执行
podmanstop命令来停止服务:ExecStop=/usr/bin/podman stop --ignore --cidfile=%t/%n.ctr-id -
systemd 通过执行
ExecStopPost脚本来停止服务。一旦容器停止,此脚本将使用podmanrm删除容器:ExecStopPost=/usr/bin/podman rm -f --ignore --cidfile=%t/ ➥ %n.ctr-idType=notify -
systemd 使用
podmanrun命令重启服务,包括--label"io.containers.autoupdate=registry"选项:ExecStart=/usr/bin/podman run --cidfile=%t/%n.ctr-id --cgroups=no-conmon --rm ➥ --sdnotify=conmon -d --replace --label ➥ io.containers.autoupdate=registry -p 8080:8080 ➥ --name myapp quay.io/rhatdan/myimage
第三步中的podman run命令将联系注册表并拉取更新的容器镜像,并在其上重新创建容器化应用程序。容器、其环境和所有依赖项都将重新启动。
您可以通过更改镜像,将其推送到注册表,然后按照以下方式运行podman auto-update命令来测试:
$ podman exec -i myapp bash -c 'cat > /var/www/html/index.xhtml' << _EOF
<html>
<head>
</head>
<body>
<h1>Welcome to the new Hello World<h1>
</body>
</html>
_EOF
现在,将镜像提交为myimage-new,并使用原始名称myimage推送到注册表。最后,从本地存储中删除镜像以模拟该镜像从未在您的系统上存在:
$ podman commit myapp quay.io/rhatdan/myimage-new
...
226ec055eef82ac185c53a26de9e98da4e6403640e72c7461a711edcbcaa2422
$ podman push quay.io/rhatdan/myimage-new quay.io/rhatdan/myimage
...
$ podman rmi quay.io/rhatdan/myimage-new
一旦新镜像在注册表中,并且您已将其从本地存储中删除,您就可以运行podman auto-update,它会注意到新镜像并重启服务。这会触发 Podman 拉取新镜像并重新创建容器化服务:
$ podman auto-update
Trying to pull quay.io/rhatdan/myimage...
Getting image source signatures
Copying blob ecfb9899f4ce done
Copying config 37e5619f4a done
Writing manifest to image destination
Storing signatures
UNIT CONTAINER IMAGE
➥ POLICY UPDATED
myapp-new.service c8888d1319c4 (myapp) quay.io/rhatdan/myimage registry
➥ true
您的应用程序已更新到镜像的最新版本。
一些显著的podman auto-update选项包括以下内容:
-
--dry-run——此选项很有用,可以查看是否有任何容器需要更新,而实际上并不更新它们。 -
--roll-back——此选项告诉 Podman 在更新失败时回滚到上一个镜像,如下一节所述。
systemd 定时器触发 Podman 更新
Podman 附带两个自动更新 systemd 定时器单元和两个自动更新服务单元——每个用于 rootful 容器和 rootless 容器。systemd 每天触发一次的定时器单元如下:
-
/usr/lib/systemd/system/podman-auto-update.timer
-
/usr/lib/systemd/user/podman-auto-update.timer
定时器单元告诉 systemd 执行适当的自动更新服务单元文件:
-
/usr/lib/systemd/system/podman-auto-update.service
-
/usr/lib/systemd/user/podman-auto-update.service
使用此功能,systemd 将启动 Podman,Podman 会寻找带有"io.containers.autoupdate=registry"标签的容器,就像你在上一节创建的那样。一旦 Podman 找到带有标签的容器,它会检查容器镜像是否已在注册表中更新。如果镜像已更改,Podman 将启动更新过程。这意味着你可以无人值守地运行系统,每次你向注册表推送更新镜像时,系统都会在 24 小时内更新到最新的容器镜像版本。如果你与他人共享你生成的单元文件,那么他们也会获得自动更新。
自动更新的一大担忧是如果更新失败会发生什么。在这种情况下,你将有数百个节点更新到一个损坏的服务。Systemd 有一个名为sd-notify的功能,允许服务声明其初始化已完成,并且它已准备好作为服务使用。
注意:本节的部分内容基于我之前撰写的博客,从“如何在 Podman 中使用自动更新和回滚”博客(mng.bz/neDK)中复制并改写,该博客由我和同事 Valentin Rothberg 和 Preethi Thomas 撰写。
7.4 在 notify 单元文件中运行容器
单元文件服务可以指定它们等待其他服务启动并运行后再开始。例如,你可以有一个在网站接受连接之前需要数据库运行的网站。Systemd 通常认为在启动服务的主要进程之后启动的服务已启动。然而,许多服务需要一段时间才能初始化,不能立即接受连接。在先前的例子中,数据库可能需要几分钟才能准备好让网站开始接收连接。
Systemd 定义了一种特殊的服务类型,称为notify(或sd-notify),允许服务进程在实际上完全启动并运行时通知 systemd。Systemd 仅在接收到数据库已准备好的通知后才会启动网络服务。
Systemd 通过传递指向要通知的 systemd 套接字的NOTIFY_SOCKET环境变量来告诉服务它需要通知服务已准备好。默认情况下,systemd 监听在/run/SystemD/notify 套接字上。当 Podman 在NOTIFY单元文件中执行时,它需要将套接字挂载到容器中,并将环境变量传递到容器中(图 7.7)。

图 7.7 Podman 启动的容器化sd_notify systemd 服务
如果服务在指定时间内没有通知 systemd,systemd 会将该服务标记为失败。Podman 自动更新会检查新服务是否完全启动并运行,如果检查失败,Podman 可以自动回滚到之前的容器——同样,无需人工干预。
7.5 更新后回滚失败的容器
如果您定义的服务支持sd-notify并在时间限制内写入通知套接字,则podman auto-update命令将成功。然而,如果失败,Podman 将删除新的容器并重新标记原始镜像。最后,它将在之前的镜像上创建容器,并且您的服务将恢复到之前的状态。您甚至可以设置基于系统的容器化服务来通知您的日志系统更新失败。回滚给您时间来找出问题所在并发布新镜像,再次触发自动更新。如您所见,systemd 可以用作单个系统的容器编排器。
您现在已经发现了一些 systemd 提供的功能,可以在无需人工干预的情况下运行容器。Podman 可以利用的一个额外功能是套接字激活,它允许您在单元文件中指定一个容器,该容器将在第一个数据包到达其套接字之前不会运行。
7.6 套接字激活的 Podman 容器
当 systemd 首次推出时,它因加快系统启动速度而受到赞誉。在 systemd 之前,每个服务都是顺序启动的,依赖于其他服务启动的服务需要等待。为了加快启动速度并优化资源分配,systemd 使用套接字激活服务。当您设置套接字激活服务时,systemd 代表您的服务设置监听 IP 或 UNIX 域套接字,而无需启动服务(见图 7.8)。

图 7.8 Systemd 在套接字上监听套接字激活的容器
当套接字连接到达时,systemd 激活服务并将连接交给它。之后,服务处理连接。服务可以在未来的某个时刻通过退出使自己空闲。如果新的连接进来,systemd 接受新的连接并再次启动服务。
套接字激活允许 systemd 指示一个服务立即启动,而无需实际启动或等待服务启动,从而加快启动过程。套接字激活允许 systemd 在系统上运行更多服务,因为许多服务处于空闲状态且未使用系统资源。基本上,您的服务可以在实际需要时停止,而不是空闲等待另一个连接。对于容器化服务,服务的主要进程是 Podman,它需要将连接传递给容器内运行的服务(见图 7.9)。

图 7.9 当连接到 systemd 正在监听的套接字时,systemd 激活 Podman,它启动容器,并将套接字传递给容器。
关闭 myapp.service,并创建 myapp.socket:
$ systemctl --user stop myapp.service
$ cat > $HOME/.config/systemd/user/myapp.socket <<_EOF
[Unit]
Description=myapp socket service
PartOf=myapp.service
[Socket]
ListenStream=127.0.0.1:8080
[Install]
WantedBy=sockets.target
_EOF
现在,启用套接字,并确保没有容器正在运行:
$ systemctl --user enable --now myapp.socket
$ podman ps
CONTAINER ID IMAGE COMMAND CREATED STATUS
➥ PORTS NAMES
将网络浏览器连接到套接字(见图 7.10):
$ web-browser localhost:8080

图 7.10 一个网络浏览器窗口连接到在 Podman 中运行的更新后的 ubi8/httpd-24 容器,该容器运行了 Hello World HTML。
注意 podman.socket 启动了 podman.service,该服务创建了一个容器来处理连接:
$ podman ps
CONTAINER ID IMAGE COMMAND CREATED
➥ STATUS PORTS NAMES
69c34949d632 quay.io/rhatdan/myimage:latest /usr/bin/run-http...
➥ 2 minutes ago Up 2 minutes ago 0.0.0.0:8080->8080/tcp myapp
现在如果您停止服务,不仅容器会停止,它还会被移除:
$ systemctl --user stop myapp.service
$ podman ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS
➥ PORTS NAMES
Socket 激活允许您仅在需要时运行服务,从而节省系统资源。稍后,您可以停止该服务,知道如果新的连接到来,systemd 和 Podman 将会处理它。
摘要
-
Podman 允许在容器内以 systemd 作为主要进程运行。
-
Journald 推荐用于 Podman 的日志和事件。
-
Systemd 可以在启动时启动和重启容器。
-
Podman 自动更新用于管理容器及其镜像的生命周期。
-
可以使用基于 Podman 的容器与 Socket-activated systemd 服务一起使用。
-
podmangeneratesystemd命令可以轻松生成运行您的容器的 systemd 服务文件。
8 使用 Kubernetes
本章涵盖
-
从现有的 Podman pods 和容器创建 Kubernetes YAML 文件
-
从 Kubernetes YAML 文件创建 Podman 容器和 pods
-
使用 Kubernetes YAML 文件关闭和删除 pods 和容器
-
在从 Kubernetes YAML 文件启动 pods 和容器之前即时构建容器镜像
-
在 Podman 和 Kubernetes 容器内运行 Podman
一些读者期待在本章中看到如何将 Podman 用作 Kubernetes 的容器引擎,类似于它过去如何使用 Docker。虽然有一些努力将 Podman 用作 Kubernetes 的容器引擎(例如,kind 项目支持这一点),但我通常不推荐您使用 Podman 来实现这一目的。我建议您使用附录 A 中描述的 CRI-O,因为它专门为与 Kubernetes 一起工作而构建,并且与 Podman 共享底层库。现在 Kubernetes 正在劝阻用户使用 Docker 后端,并鼓励他们使用 CRI-O 或 containerd 作为后端。
本章介绍了使用与 Kubernetes 和 Podman 相同的结构化语言,以及如何在 Kubernetes 集群内运行 Podman 容器。您已经学习了如何使用 Podman 从命令行创建作为容器和 pods 的微服务。通常,软件开发人员和打包人员需要将他们的应用程序部署到多台机器上。您可能想将您的 Web 应用程序添加一个数据库后端。如果 Web 应用程序变得流行,您将需要在不同的节点上运行多个实例。将不同的微服务连接在一起并协调所有这些服务不是 Podman 能做到的。这正是 Kubernetes 发挥作用的地方。
在本章中,您将学习如何在 Kubernetes 中运行这些相同的容器和 pods。kubernetes.io 网站表示,“Kubernetes,也称为 K8s,是一个开源系统,用于自动化容器化应用程序的部署、扩展和管理。”我将 Kubernetes 视为在多台机器上同时运行容器的工具——一种协调大量容器化微服务集群的方式。
您可能会遇到的一个问题是,大多数容器开发都是使用 Podman 和 Docker 等工具进行的,这些工具使用相当简单的命令行界面来创建容器和 pods。但 Kubernetes 使用的是以 YAML 文件编写的声明性语言。
我不会在本章深入探讨 Kubernetes 的工作原理,因为已经有许多关于该主题的深入书籍,包括 Marko Lukša 的《Kubernetes in Action》(Manning,2020)和 William Denniss 的《Kubernetes for Developers》(Manning,2020),它们描述了 Kubernetes 的所有功能。但我将描述 Kubernetes 的开发者语言:Kubernetes YAML 文件。
注意:yaml.org 网站首先将 YAML 描述为“YAML Ain’t Markup Language”。它进一步阐述,“YAML 是一种适用于所有编程语言的友好数据序列化语言。”
将命令行选项转换为结构化语言如 YAML 对从单个节点上的容器迁移到大规模运行的容器中的开发者来说是一个障碍。你如何指定卷、要使用的镜像、安全约束、网络端口等等?在第 8.2 节中,你将学习如何使用 Podman 将你本地创建的 pods 和容器转换为 Kubernetes YAML 文件。
在使用 Kubernetes YAML 文件编写和部署你的应用程序后,用户可能会发现你的应用程序在 Kubernetes 中运行时存在问题。在规模上测试应用程序可能很困难,而且你通常只想在本地系统上运行应用程序,而不需要设置和配置 Kubernetes 集群。在第 8.3 节中,你将了解 podman play kube。这个 Podman 命令允许你在没有 Kubernetes 的情况下本地运行 Kubernetes YAML 文件,以便你可以测试和调试问题。
本章的最后一部分将涵盖在容器中运行 Podman,包括在 Kubernetes 集群中运行它。管理员、开发人员和质量工程师需要使用 Podman 在他们的持续集成(CI)系统中测试容器。通常这些 CI 系统建立在 Kubernetes 集群之上。第 8.4 节将教你如何在 Podman 和 Kubernetes 启动的容器中运行 Podman 命令的不同方法。
8.1 Kubernetes YAML 文件
Kubernetes YAML 文件是在 Kubernetes 中启动 pods 和容器的对象。在第五章中,你学习了 Podman 使用的配置文件是用 TOML 编写的,它与 YAML 非常相似。这两种配置语言都试图做到人类可读。YAML 依赖于缩进子句,这与你学过的 TOML 语法不同。你可以访问 yaml.org 网站了解更多关于这种语言的信息。
如果你打算大量使用 Kubernetes YAML 文件,那么拥有一个至少能理解 YAML 的文本编辑器或 IDE,比如 Visual Studio 和 VS Code,会很好;如果它还了解 Kubernetes 语言,那就更好了。Kubernetes YAML 是描述性和强大的。它允许你使用声明性语言来建模你应用程序的期望状态。正如本章引言中所述,编写这些 YAML 文件是开发者在将容器从本地系统迁移到 Kubernetes 时需要克服的障碍。大多数开发者只是在网上搜索现有的 Kubernetes YAML 文件,然后将他们的容器命令、镜像和选项剪切粘贴到 YAML 文件中。虽然这样做可以工作,但它可能导致意想不到的后果——并且通常是多余的工作。
Podman 的产品经理 Scott McCarty 提出了一个想法:“我真正想做的就是帮助用户从 Podman 过渡到使用 Kubernetes 管理他们的容器。”这促使 Podman 开发者创建了一个新的 Podman 命令:podman generate kube。
8.2 使用 Podman 生成 Kubernetes YAML 文件
假设你想要在 Kubernetes 中运行之前章节中生成的容器。你需要编写 Kubernetes YAML 文件来实现这一点。你应该从哪里开始?
在本章中,你将学习一个新命令:podman generate kube。这个 Podman 命令捕获本地 pod 和容器的描述,然后将它们转换为 Kubernetes YAML 格式。这有助于你过渡到更复杂的编排环境,如 Kubernetes。生成的 Kubernetes YAML 文件可以由 Kubernetes 命令使用,以将你的 pod 和容器部署到 Kubernetes 集群中。
你可以使用你在前几章中学到的相同的 Podman run、create和stop命令,在命令行上重新创建容器或 pod。使用以下命令重新创建你一直在使用的容器。
首先,使用podman rm删除容器(如果存在)。你将引入一个新的标志--ignore,它告诉podman rm命令在容器不存在时不报告错误。然后,从命令行重新创建容器:
$ podman rm -f --ignore myapp
$ podman create -p 8080:8080 --name myapp quay.io/rhatdan/myimage
9305822e6089ca28a1fdbb005c12f57f4a26be273fe5d49a1908eadbcfdcb7d4
现在,使用命令podman generate kube myapp生成 Kubernetes YAML 文件。Podman 检查其数据库中现有的容器或 pod,以获取在 Kubernetes 中运行容器所需的所有字段,并将它们填充到 Kubernetes YAML 文件中:
$ podman generate kube myapp > myapp.yaml
图 8.1 显示了podman generate kube命令的结果。

图 8.1 展示了从myapp容器生成的myapp.yaml文件
检查 YAML 文件的各个部分。理解 Kubernetes 与 pod 协同工作,尽管你创建了一个容器,podman generate kube,它创建了一个 pod 规范。Podman 根据原始容器的名称命名 pod 为myapp-pod,容器为myapp:
metadata:
creationTimestamp: "2021-11-22T11:57:12Z"
labels:
app: myapppod
name: myapp-pod
spec:
containers:
- args:
- /usr/bin/run-httpd
image: quay.io/rhatdan/myimage:latest
name: myapp
注意,在容器部分,记录了镜像名称quay.io/rhatdan/myimage: latest,这告诉 Kubernetes 从哪里下载容器的镜像。它还告诉 Kubernetes 在容器内启动应用程序的命令参数,即/usr/bin/run-httpd:
spec:
containers:
- args:
- /usr/bin/run-httpd
image: quay.io/rhatdan/myimage:latest
在相同的容器部分,你可以看到 Podman 端口被记录,-p 8080: 8080 spec:
containers:
- args:
- /usr/bin/run-httpd
image: quay.io/rhatdan/myimage:latest
name: myapp
ports:
- containerPort: 8080
hostPort: 8080
最后,在容器部分的末尾,你可以看到securityContext,它记录了 Podman 默认丢弃三个额外的 Linux 功能:CAP_MKNOD、CAP_NET_RAW和CAP_AUDIT_WRITE:
securityContext:
capabilities:
drop:
- CAP_MKNOD
- CAP_NET_RAW
- CAP_AUDIT_WRITE
大多数容器在没有这些 Linux 功能的情况下运行良好,但 OCI 规范默认启用这三个功能。这告诉 Kubernetes,这个 pod 可以在没有这些功能的情况下更安全地运行,并且 Kubernetes 将丢弃它们。你可以通过运行命令man capabilities来了解更多关于 Linux 功能的信息。
到目前为止,你可以在任何 Kubernetes 集群中运行这个 Kubernetes YAML 文件,通常运行以下命令:
kubectl create -f myapp.yml
通常,你需要在 YAML 文件中添加复杂性和编排,并利用 Kubernetes 的高级功能。例如,生成的 Kubernetes YAML 文件只会生成你应用程序的单个实例。如果你想在不同节点上运行你应用程序的多个版本,你可以在 YAML 文件中添加replicas选项,如图 8.2 所示。

图 8.2 修改后的 Kubernetes YAML 文件,准备运行两个副本
replicas标志告诉 Kubernetes,myapp.yaml 文件希望始终在两个不同的节点上运行两个myapp Pod。副本和其他高级 Kubernetes 功能超出了 Podman 的范围。podman play kube命令忽略这些字段。
一些显著的podman generate kube选项包括以下内容:
-
-f,--filename—这会将输出写入指定的路径。 -
-s,--service—这会为 Kubernetes 服务对象生成 YAML。
现在你已经生成了一个 Kubernetes YAML 文件,能够反转这个过程会很好。如果你有一个 Kubernetes YAML 文件,你可能希望生成 Podman 的 Pod 和容器。
8.3 从 Kubernetes YAML 生成 Podman 的 Pod 和容器
假设你得到了一个 Kubernetes YAML 文件并想在本地上运行它进行检查。你可以设置一个本地的 Kubernetes 集群,但如果你能直接在本地上运行 Pod 会更好。Podman 提供了一个执行此操作的命令。podman play kube命令根据结构化的 Kubernetes YAML 文件创建 Pod、容器和卷。创建的 Pod 和容器会自动启动。为了测试这一点,你可以简单地删除你创建的容器,然后使用以下命令运行生成的 myapp.yaml 文件:
$ podman rm -f --ignore myapp
$ podman play kube myapp.yaml
Pod:
b70aedd8105a6915428928a2b33fd7ecede632298088ea25d9db74ba9b16201e
Container:
a4d78fdfa5d8f751aafb06f3782e36a3aaf5b3804ca57694385de2ea1e400fe6
Kubernetes 只运行带有容器的 Pod;它不会单独运行容器。当podman play kube命令读取 YAML 文件时,它会启动 Pod 以及容器。注意图 8.3 中play命令创建了一个包含你的容器以及基础设施容器的 Pod。

图 8.3 myapp-pod与myapp容器和基础设施容器一起运行
podman generate kube命令根据 myapp.yaml 文件中的名称创建名为myapp-pod的 Pod。容器的名称通过将 Pod 的名称附加到容器名称来生成:myapp-pod-myapp。如果 YAML 文件定义了额外的容器,它们需要以类似的方式进行标记:
$ cat myapp.yaml
...
name: myapp-pod
spec:
containers:
- args:
name: myapp
你可以使用podman pod ps命令显示系统上运行的 Pod。添加--ctr-names选项也可以列出 Pod 内运行的容器:
$ podman pod ps --ctr-names
POD ID NAME STATUS CREATED INFRA ID NAMES
b70aedd8105a myapp-pod Running 1 day ago b7a276c62c1d
➥ myapp-pod-myapp ,b70aedd8105a-infra
现在检查使用podman ps命令运行的两个容器,使用以下命令:
$ podman ps
CONTAINER ID IMAGE COMMAND CREATED
➥ STATUS PORTS NAMES
b7a276c62c1d k8s.gcr.io/pause:3.5
➥ 3 minutes ago Up 3 minutes ago 0.0.0.0:8080->8080/tcp b70aedd8105a-infra
a4d78fdfa5d8 quay.io/rhatdan/myimage:latest /usr/bin/run-http...
➥ 3 minutes ago Up 3 minutes ago 0.0.0.0:8080->8080/tcp myapp-pod-myapp
使用podman pod stop命令关闭 Pod 和容器:
$ podman pod stop myapp-pod
b70aedd8105a6915428928a2b33fd7ecede632298088ea25d9db74ba9b16201e
podman play kube 可以执行更复杂的 YAML 文件,包括定义了多个 pod、卷和容器的文件。在之前的简单示例中,你可以使用 podman pod stop 命令来关闭 pod,但当 podman play kube 生成多个唯一的 pod 时,关闭它们会变得稍微复杂一些。
8.3.1 基于 Kubernetes YAML 文件关闭 pod 和容器
虽然你可以停止 podman play kube 启动的每个 pod,但有时你不仅想要停止 pod 和容器,实际上还想从系统中删除它们。podman play kube --down 命令销毁 play kube 上一次运行创建的 pod。pod 被停止然后删除。任何创建的卷都保持完整。关闭之前示例中创建的 myapp.yaml pod:
$ podman play kube myapp.yaml --down
Pods stopped:
B70aedd8105a6915428928a2b33fd7ecede632298088ea25d9db74ba9b16201e
Pods removed:
b70aedd8105a6915428928a2b33fd7ecede632298088ea25d9db74ba9b16201e
注意 Podman 不仅停止了 pod,还将其删除了。你可以使用 podman pod ps 命令来验证 pod 是否已消失:
$ podman pod ps
POD ID NAME STATUS CREATED INFRA ID # OF CONTAINERS
这让你回到了可以再次运行 podman play kube 的状态,它将创建新的 pod 和容器:
$ podman play kube myapp.yaml
Pod:
302b1d2c0048a49ea32c2e6ffa0e0549af199ab2bc32de285eef5da628efe28c
Container:
b9f080dc6e13b4a4c37fa66a9b727dbeb2af30f0c3824044aba8a46eebfe15c5
这模仿了 Kubernetes 运行 pod 和容器时发生的情况。Kubernetes 总是创建新的 pod 和容器,并在完成时将其销毁。从 YAML 文件生成所有 pod 和容器,然后使用 --down 标志将其删除的功能类似于 docker-compose 的工作流程。Podman 的一个重大优势是使用与在具有 Kubernetes 的多节点编排环境中运行 pod 和容器相同的 YAML 文件。docker-compose 另一个具有的功能是能够构建 YAML 文件中定义的镜像,Podman 开发者也将其添加到了 podman play kube。
8.3.2 使用 Podman 和 Kubernetes YAML 文件构建镜像
使用 podman play kube 作为 docker-compose 替代品的用户请求 Podman 添加一个构建镜像的功能,而不是总是从容器仓库中拉取它们。虽然 Kubernetes 不支持这样的功能,但 Podman 开发者决定向 podman play kube 添加 --build 标志。因为 podman build 可以处理 Containerfile 或 Dockerfile,增强 podman play kube 是简单的。
想法是通过一个按需生成的容器镜像来创建容器化应用程序。正常的 Kubernetes 工作流程要求开发者使用 podman build 构建镜像,并使用 podman push 将其推送到容器仓库,正如你在第二章中学到的。然后你可以使用 podman play kube 从仓库检索镜像。podman play kube --build 选项允许它内部执行 podman build 并按需生成镜像,而不是强迫你使用容器仓库。
注意:--build 选项在远程 Podman 客户端中不可用,因此你无法在 Mac 或 Windows 上使用它。
在这个示例中,你将重新创建 6.1.3 节中使用的 Containerfile:
$ cat > ./Containerfile << _EOF
FROM ubi8-init
RUN dnf -y install httpd; dnf -y clean all
RUN systemctl enable httpd.service
_EOF
回想一下,这个 Containerfile 构建了一个以 systemd 作为 init 系统运行的容器镜像,HTTPD 服务在端口 80 上运行并监听。首先,删除所有 pod 和容器:
$ podman pod rm --all --force
$ podman rm --all --force
现在重新构建 my-systemd 镜像:
$ podman build -t mysystemd.
STEP 1/3: FROM ubi8-init
STEP 2/3: RUN dnf -y install httpd; dnf -y clean all
Updating Subscription Management repositories.
Unable to read consumer identity
...
Successfully tagged localhost/mysystemd:latest
bb1634ce1457f2eb70f84af33599d211eae64cb5f951e40e91481b6e58b747bf
现在重新创建一个包含 ./html 目录的容器镜像(使用第 3.1 节的代码示例):
$ podman create --rm -p 8080:80 --name myapp -v ./:/var/www/
➥ html:Z mysystemd
fec6de5716ac246613723a4cc26407005e0bc315affdc62b56883bd94acd795e
现在生成 Kubernetes YAML 文件,使用 podman generate kube:
$ podman generate kube myapp > myapp2.yaml
注意这次 Podman 生成了包含 html 卷部分的 YAML 文件:
$ cat myapp2.yaml
...
spec:
containers:
- image: localhost/mysystemd:latest
...
volumeMounts:
- mountPath: /var/www/html
name: home-dwalsh-podman-html-host-0
volumes:
- hostPath:
path: /home/dwalsh/podman/html
type: Directory
name: home-dwalsh-podman-html-host-0
使用 podman pod rm --all --force 命令删除所有 pod,以回到一个干净的环境。使用 podman rm 和 podman rmi 命令删除所有容器和镜像,以便您可以从头开始:
$ podman pod rm --all --force
$ podman rm --all --force
fec6de5716ac246613723a4cc26407005e0bc315affdc62b56883bd94acd795e
$ podman rmi mysystemd
Untagged: localhost/mysystemd:latest
Deleted: bb1634ce1457f2eb70f84af33599d211eae64cb5f951e40e91481b6e58b747bf
Deleted: 70e0c1a7580089420267b5928210ad59fdd555603e647b462159ea94f97946f9
podman play kube --build 命令需要存在与镜像名称匹配的子目录,以便构建镜像。Podman 检查 Kubernetes YAML 文件中的所有镜像,然后寻找匹配的子目录。每个目录都被视为上下文目录,应包含一个 Containerfile 或 Dockerfile。然后 Podman 在每个子目录上执行 podman build。由于 YAML 文件需要 mysystemd 镜像,您需要创建一个 mysystemd 目录并将 Containerfile 放入该目录:
$ mkdir mysystemd
$ mv Containerfile mysystemd/
您现在可以运行 podman play kube --build,它将重新构建容器镜像并启动您的应用程序的 Pod 和容器:
$ podman play kube myapp2.yaml --build
STEP 1/3: FROM ubi8-init
STEP 2/3: RUN dnf -y install httpd; dnf -y clean all
Updating Subscription Management repositories.
...
--> 305bb9b8da1
Successfully tagged localhost/mysystemd:latest
305bb9b8da12db682b0eae93ad492e632d2ba43e03f6a6b68467d7429a8a2664
a container exists with the same name ("myapp") as the pod in your YAML file;
➥ changing podname to myapp-pod
Pod:
30739dd554acfeab66a9767301127bab0fe994461686f45a3a89b137c3954840
Container:
ce633ac4e7a1e4d08e0428a8401fcfc4ac75fbcca4be07bc167add6093a44afa
Podman 基于 mysystemd/Containerfile 重建了 mysystemd 镜像,然后为您的应用程序生成了 myapp-pod pod 和 myapp 容器,甚至没有接触到容器注册库。
您可以将此 YAML 文件和 mysystemd 目录与其他用户共享,他们可以使用 Podman 构建和启动您的应用程序。不过,如果您想在 Kubernetes 内部启动它,您需要将构建的镜像推送到容器注册库,然后编辑 YAML 文件以指向注册库中的镜像。现在您已经了解了 Podman 与 Kubernetes 的集成,我想探讨最后一个想法:在 Podman 和 Kubernetes 容器内运行 Podman。
8.4 在容器内运行 Podman
在容器内或 Kubernetes 集群内运行 Podman 是一个常见问题。用户希望在 CI/CD 系统中使用容器测试容器镜像和工具。通常,他们想使用 podman build 构建容器镜像。有时,他们只想测试比他们发行版中发布的 Podman 更新版本的 Podman。
Podman 的一大挑战是它可以以多种不同的方式配置,以至于用户在容器内运行 Podman 时寻找最佳实践。正因为如此,我以及我的几位同事决定创建一个容器镜像,quay.io/podman/stable,这使得在容器内运行 Podman 更加容易。如您所知,Podman 可以以两种不同的模式运行:有根模式和 无根模式。默认情况下,Podman 容器在其用户命名空间内以容器根用户启动。为了帮助您理解在容器内运行 Podman,您将首先尝试在 Podman 内运行 Podman。表 8.1 描述了您可以在容器内运行容器的方式以及允许内部 Podman 执行容器的所需能力。
表 8.1 在容器内运行 Podman 的要求
| 主机模式 | 容器模式 | 能力 | 说明 |
|---|---|---|---|
| 有根模式 | 有根模式 | CAP_SYS_ADMIN |
具有对主机用户命名空间的完全访问权限 |
| 有根模式 | 无根模式 | CAP_SETUIDCAP_SETGID |
在容器内部基于 /etc/subuid 和 /etc/subgid 运行在单独的用户命名空间中 |
| 无根模式 | 有根模式 | 命名空间 CAP_SYS_ADMIN |
具有对用户用户命名空间的完全访问权限 |
| 无根模式 | 无根模式 | 命名空间 CAP_SETUID, CAP_SETGID |
在容器内部基于 /etc/subuid 和 /etc/subgid 运行在单独的用户命名空间中。用户命名空间必须是您运行 Podman 命令的用户命名空间的子集。 |
8.4.1 在 Podman 容器内运行 Podman
在第一个示例中,您将在无根容器内运行有根 Podman。您需要使用 --privileged 命令,因为为了成功运行,Podman 需要能够挂载文件系统。当 Podman 以 root 用户运行时,挂载需要 CAP_SYS_ADMIN 能力,这由 --privileged 选项提供。通过执行以下命令来尝试:
$ podman run --privileged quay.io/podman/stable podman version
Trying to pull quay.io/podman/stable:latest...
Getting image source signatures
Copying blob b1f89b7294d7 done
...
Version: 4.1.0
API Version: 4.1.0
Go Version: go1.18.2
Built: Mon May 30 12:03:28 2022
OS/Arch: linux/amd64
quay.io/podman/stable 镜像还配置为在 Podman 容器内运行无根 Podman。您可以通过添加 --user podman 选项以 Podman 用户身份运行来激活此行为。在此模式下,容器内的 Podman 需要 CAP_SETUID 和 CAP_SETGID 来设置用户命名空间。幸运的是,Podman 默认将此访问权限提供给容器:
$ podman run --user podman quay.io/podman/stable podman version
如果您真的想锁定容器,您可以使用 --cap-drop=all --cap-add CAP_SETUID,CAP_SETGID 选项来丢弃除 CAP_SETUID 和 CAP_SETGID 之外的所有能力:
$ podman run --cap-drop=all --cap-add CAP_SETUID,CAP_SETGID
➥ --user podman quay.io/podman/stable podman version
Version: 4.1.0
API Version: 4.1.0
Go Version: go1.18.2
Built: Mon May 30 12:03:28 2022
OS/Arch: linux/amd64
这些示例展示了如何在 Podman 容器内运行 Podman,同样也可以用 Docker 在容器内运行 Podman 来轻松实现。
注意,Docker 在运行时使用 seccomp 过滤器,该过滤器阻止了 unshare 和 mount 系统调用。您需要要么在 Docker 中禁用 seccomp 过滤器——
docker run –security-opt seccomp=unconfined ...
—或者运行带有 Podman 的 seccomp 过滤器的 Docker:
docker run –security-opt seccomp=/usr/share/containers/seccomp.json ... .
在本节中,您学习了 Podman 与 Kubernetes 的集成。在下一节中,您将学习如何配置 Podman 以在 Kubernetes pod 或容器中运行。
8.4.2 在 Kubernetes 容器中运行 Podman
CI/CD 系统的一个常见用例是使用 Podman 在 Kubernetes 中运行容器。正如你所学的,在容器中运行 Podman 需要 CAP_SYS_ADMIN 权限用于 rootful 容器,或者 CAP_SETUID 和 CAP_SETGID 以 rootless 模式运行。理解 Podman 容器几乎总是需要多个 UID 来运行,尤其是在运行 podman build 时。许多 Kubernetes 用户在尝试在锁定环境中的 Kubernetes 容器中运行 Podman,只有一个 UID 且没有 Linux 权限时遇到了 Podman 问题。这些容器是 OpenShift 的默认设置,以及许多基于云的 Kubernetes 环境的默认设置。在没有某些 Linux 权限和访问多个 UID 的情况下,在环境中运行容器引擎如 Podman 是不可能的。
使用 quay.io/podman/stable 镜像在 privileged Kubernetes 容器中运行 rootful Podman 的等效版本可以通过以下 Kubernetes YAML 文件启动:
apiVersion: v1
kind: Pod
metadata:
name: podman-priv
spec:
containers:
- name: priv
image: quay.io/podman/stable
args:
- podman
- version
securityContext:
privileged: true
类似地,你可以通过以下 YAML 文件在 Kubernetes 容器中启动无根 Podman。请注意,你指定 runAsUser: 1000 作为 UID,而不是 podman 用户。Kubernetes 不支持在容器内将用户名转换为 UIDs:
apiVersion: v1
kind: Pod
metadata:
name: podman-rootless
spec:
containers:
- name: rootless
image: quay.io/podman/stable
args:
- podman
- version
securityContext:
capabilities:
add:
- "SETUID"
- "SETGID"
runAsUser: 1000
注意:请参阅以下由我和我的同事 Urvashi Mohnani 撰写的文章,这些文章提供了更多关于在容器中运行 Podman 的示例:
-
“如何在容器中使用 Podman” (
mng.bz/vXDM) -
“如何在 Kubernetes 中使用 Podman” (
mng.bz/49EV)
如你所见,只要理解 Podman 的要求,在 Kubernetes 中运行 Podman 容器相当容易。Kubernetes 社区正在进行工作,利用用户命名空间,使在 Kubernetes 容器中运行 Podman 容器变得更加容易,并使它们更加安全。
摘要
-
podman generate kube命令可以轻松地将本地运行的 pod 和容器移动到适合在 Kubernetes 集群中运行的 Kubernetes YAML 文件。 -
这些 YAML 文件也可以通过
podman play kube命令生成本地 pod 和容器。 -
--down选项允许podman play kube命令关闭由之前的podman play kube命令启动的所有 pod 和容器。 -
--build选项允许podman play kube命令根据 Containerfile/Dockerfile 生成 Kubernetes YAML 文件中定义的容器镜像,从而消除了将镜像推送到容器注册库的需要。 -
podman play kube是docker-compose的合适替代品,因为它与 Kubernetes 使用相同的 YAML 格式。 -
只要理解在锁定环境中运行 Podman 的要求,在 Podman 和 Kubernetes 容器中运行 Podman 是可能的。
9 Podman 作为服务
本章涵盖
-
以服务形式运行 Podman
-
Podman 服务支持两个 REST API
-
用于管理 Podman 容器的 Python 库 podman-py 和 docker-py
-
支持
docker-compose -
使用 Podman 服务进行远程命令行通信
-
管理与远程 Podman 实例的 SSH 通信
在前面的章节中,你学习了 Podman 命令行。问题是有时你想要从远程系统与容器一起工作。同样,你可能想用脚本语言编写代码来与容器交互。Docker 作为客户端-服务器应用程序编写,支持流行的远程 API,这导致了用 Python 和 JavaScript 编写的库的创建,用于访问守护进程。Docker-py 是一个流行的 Python 库,用于与 Docker 守护进程交互。
已经构建了许多 CI/CD、GUI 和远程管理系统来管理 Docker 容器。像 Visual Studio 这样的代码编辑器甚至有内置的插件,可以直接与 Docker API 通信。像 docker-compose 这样的高级工具导致了一种新的编程语言的出现,该语言通过与 Docker 守护进程交互来在主机上编排多个容器。
Podman 提供类似的功能,可以作为服务运行。Podman 支持以无根模式以及有根模式运行 Podman 服务。在本章中,你将了解服务以及如何与之交互。你将编写一个简单的 Python 程序,使用 docker-py 和较新的 podman-py 库与 Podman 服务进行交互。你将学习如何设置基于 Docker 的远程工具,包括 docker-compose,以实际使用 Podman 服务,即使没有可用的 Docker 守护进程。
注意 Podman 服务仅在 Linux 上受支持。因为 Podman 服务启动 Linux 容器,所以它只能在 Linux 机器上运行。Podman 的 Windows 和 Mac 版本通过 REST API 与 Podman 服务通信以启动容器。有关 Podman 在 Mac 上的更多信息,请参阅附录 E,有关 Windows 的更多信息,请参阅附录 F。
Podman 命令有一个 --remote 选项,允许你与 Podman 服务交互,无论是在本地机器上还是在远程机器上。你将学习如何设置 Podman 连接,以便轻松且安全地与远程服务交互。但首先你需要知道如何启用 Podman 服务。
9.1 Podman 服务的介绍
Podman 项目支持 REST(或 RESTful)API。podman system service 命令创建一个监听服务,用于响应 Podman 的 API 调用。该服务可以在有根模式或无根模式下运行。此命令提供了一个可选参数,用于指定 Podman 服务将监听的 URI。例如,unix:///tmp/podman.sock URI 告诉 Podman 在 /tmp/podman.sock UNIX 域套接字上监听。tcp:localhost:10000 URI 套接字告诉 Podman 在 TCP 套接字、端口 10000 上监听。默认情况下,Podman 在 /run 目录下的 UNIX 域套接字上监听(表 9.1)。
注意:如果您不熟悉 REST API 或一般远程 API,我建议您阅读 Red Hat 的“什么是 REST API?”:www.redhat.com/en/topics/api/what-is-a-rest-api。
在这种情况下,Podman 作为服务运行与像 Docker 那样拥有集中式守护进程的方式在多个方面不同。最大的区别是 Podman 命令可以在没有服务的情况下运行,并与由服务创建的容器和镜像交互。其他容器工具可以在不通过服务的情况下与存储和容器交互。当没有连接到服务时,服务也会退出。您甚至可以在同一数据存储上同时运行多个服务(尽管我不推荐这样做)。Docker 守护进程强制所有与容器和镜像的交互都通过守护进程进行。表 9.1 显示了 Podman 服务监听传入连接的默认位置。
表 9.1 podman.socket 的默认位置
| 模式 | 默认位置 |
|---|---|
| 有根 | unix:///run/podman/podman.sock |
| 无根 | unix://$XDG_RUNTIME_DIR/podman/podman.sock(例如 unix:///run/user/1000/podman/podman.sock) |
尽管 Podman 服务也可以设置为在 TCP 套接字上运行,但我警告您要非常小心,因为服务中没有任何授权或额外的安全措施来防止黑客获取访问权限。服务依赖于 SSH 服务来获取对 Podman 服务的远程访问,并且这种方法是推荐的。
Podman 服务被设计为按需服务,在最后一个连接后退出,5 秒后结束。这个时间限制避免了即使服务未被使用,长时间运行的守护进程也会占用系统资源。虽然 Podman 服务可以为每个连接启动一个单独的进程,但这可能会成为瓶颈。通过运行以下命令来尝试一下;5 秒后,您将看到命令退出。如果您与服务有活跃的连接,它将继续运行:
$ podman system service
您可以使用 --time 选项指定退出超时时间(以秒为单位)。指定 --time 0 将导致 podman system service 命令运行,直到您停止它。大多数用户从不直接与 Podman 系统服务交互以激活服务,而是依赖于 systemd 服务来管理它。
9.1.1 系统服务
Podman 提供了多个 systemd 单元文件,用于以服务形式运行 Podman。由于 Podman 并未设计为守护进程,并且开发者不希望总是有一个长时间运行的守护进程,他们决定利用 systemd 套接字激活。这允许 Podman 服务作为按需服务启动。图 9.1 展示了 systemd 如何监听 Podman 套接字,并在收到连接时启动 Podman 服务。

图 9.1 在 systemd 下运行的 Podman 服务
Podman 软件包提供了两个 podman.socket 单元文件:一个用于有根 Podman,另一个用于无根 Podman。表 9.2 定义了在有根和无根模式下使用的 systemd 套接字文件的位置。
表 9.2 Podman 套接字单元文件
| 模式 | Systemd 套接字文件 |
|---|---|
| 有根 | /usr/lib/systemd/system/podman.socket |
| 无根 | /usr/lib/systemd/user/podman.socket |
这两个套接字激活服务告诉 systemd 监听表 9.1 中列出的默认 UNIX 域套接字。当进程连接到套接字时,systemd 启动匹配的服务,该服务运行 podman system service 命令。然后 systemd 将套接字传递给服务。当 Podman 服务完成 API 请求后,它等待另一个连接。如果 5 秒内没有连接发生,Podman 将退出,释放其使用的资源。如果出现新的连接,systemd 重复此过程并启动 Podman 服务的另一个实例。
在本章的其余部分,你将交互使用 Podman 服务,因此你需要开始运行它。你可以使用 --user 选项在你的机器上启用并启动 Podman 套接字,这告诉 systemd 启用用户服务(或无根模式服务):
$ systemctl --user enable podman.socket
Created symlink
➥ /home/dwalsh/.config/systemd/user/sockets.target.wants/podman.socket →
➥ /usr/lib/systemd/user/podman.socket.
$ systemctl --user start podman.socket
你可以看到 podman.sock 已在你的 XDG_RUNTIME_DIR 中创建:
$ ls $XDG_RUNTIME_DIR/podman/podman.sock
/run/user/3267/podman/podman.sock
在这一点上,systemd 正在监听套接字,但没有 Podman 进程在运行。当服务接收到数据包时,systemd 启动 Podman 服务进程来处理连接。
要尝试该服务,你可以运行以下 curl 命令来探测 Podman 服务的版本:
$ curl -s --unix-socket $XDG_RUNTIME_DIR/podman/podman.sock
➥ http://d/v1.0.0/libpod/version | jq
{
"Platform": {
"Name": "linux/amd64/fedora-35"
},
"Components": [
{
"Name": "Podman Engine",
"Version": "4.0.0-dev",
"Details": {
"APIVersion": "4.0.0-dev",
"Arch": "amd64",
"BuildTime": "2022-01-04T13:42:14-05:00",
"Experimental": "false",
"GitCommit": "66ffbc845d1f0fd5c29611ac3f09daa24749dc1e-dirty",
"GoVersion": "go1.16.12",
"KernelVersion": "5.15.10-200.fc35.x86_64",
"MinAPIVersion": "3.1.0",
"Os": "linux"
}
},
{
"Name": "Conmon",
"Version": "conmon version 2.0.30, commit: ",
"Details": {
"Package": "conmon-2.0.30-2.fc35.x86_64"
}
},
{
"Name": "OCI Runtime (crun)",
"Version": "crun version 1.4\ncommit:
3daded072ef008ef0840e8eccb0b52a7efbd165d\nspec: 1.0.0\n+SYSTEMD
➥ +SELINUX +APPARMOR +CAP +SECCOMP +EBPF +CRIU +YAJL",
"Details": {
"Package": "crun-1.4-1.fc35.x86_64"
}
}
],
"Version": "4.0.0-dev",
"ApiVersion": "1.40",
"MinAPIVersion": "1.24",
"GitCommit": "66ffbc845d1f0fd5c29611ac3f09daa24749dc1e-dirty",
"GoVersion": "go1.16.12",
"Os": "linux",
"Arch": "amd64",
"KernelVersion": "5.15.10-200.fc35.x86_64",
"BuildTime": "2022-01-04T13:42:14-05:00"
}
现在你已经运行了服务,是时候调查 API 了。
9.2 Podman 支持的 API
Podman 服务在相同的套接字(表 9.1)上提供两个 API。兼容 API 面向 Docker API 的最新发布版本,实现了所有端点,除了 Swarm API。Podman 团队将任何与 Docker API 差异相关的问题视为一个错误。如果 API 对 Docker 守护进程有效,则它必须对 Podman 服务有效。
Podman Libpod API 提供了对 Podman 独特功能的支持,例如 pods。虽然所有项目都支持本机 Libpod API 将是件好事,但过渡需要时间,并且对于基于 Docker API 的较老且不再维护的项目来说可能是不可能的。
我建议所有新的 Podman 用户使用 Libpod API,但如果你正在使用遗留代码或想要开发既可与 Podman 也可与 Docker 一起工作的代码,那么你应该使用兼容 API。表 9.3 列出了 Podman 提供的两个不同的 REST API。
表 9.3 Podman 支持的 API
| 模式 | 描述 | 文档 |
|---|---|---|
| 兼容性 | 提供对 Docker v1.40 API 支持的兼容层 | docs.docker.com/engine/api/ |
| Libpod | Podman 原生的 Libpod 层 | docs.podman.io/en/latest/_static/api.xhtml |
与远程 API 交互的最简单方法是使用 curl 命令。检查使用 curl 命令和 jq 命令可用的图像列表,并注意 URL 中的 libpod 字段。此字段告诉 Podman 使用其原生 API。
列表 9.1 将 curl 连接到 Podman 套接字时的默认输出
$ curl -s --unix-socket $XDG_RUNTIME_DIR/podman/podman.sock
➥ http://d/v1.0.0/libpod/images/json | jq
[
{
"Id":
"Sha256:2c7e43d880382561ebae3fa06c7a1442d0da2912786d09ea9baaef87f73c29ae",
"ParentId": "",
"RepoTags": [
"quay.io/rhatdan/myimage:latest" ❶
],
...
}
]
❶ 你一直在工作的镜像
你也可以通过消除 libpod 字段来运行 Docker API。对于这个命令,你得到相同的输出,因为 API 有相同的输出:
$ curl -s --unix-socket $XDG_RUNTIME_DIR/podman/podman.sock
➥ http://d/v1.0.0/images/json | jq
[
{
"Id":
"Sha256:2c7e43d880382561ebae3fa06c7a1442d0da2912786d09ea9baaef87f73c29ae",
"ParentId": "",
"RepoTags": [
"quay.io/rhatdan/myimage:latest"
],
...
}
]
一个 API 不同的例子是列出 pods,因为 Docker 不支持 pod 的概念,所以 compat API 没有针对它的接口。
首先,通过运行以下命令为测试创建一个 pod:
$ podman pod create --name mypod
116291543d5691c597132ec73a428f29f2c1f71a65fdfbaca17eb5440a5d47f6
现在,使用 Libpod pods 或 JSON API 来查看与你刚刚创建的 pod 相关的 JSON:
$ curl -s --unix-socket $XDG_RUNTIME_DIR/podman/podman.sock
➥ http://d/v1.0.0/libpod/pods/json | jq
[
{
"Cgroup": "user.slice",
“Containers": [
{
"Id": "8eeceeb4fd6aa3897e05b5361b5c27c6e98bc29707484f95994f49437536599e",
"Names": "4b10a21c5b8c-infra",
"Status": "running"
}
],
"Created": "2022-01-05T06:51:52.604528462-05:00",
"Id": "4b10a21c5b8c2b4f8a598de1eace7b94918d813055891276c2472df856a7fbc1",
"InfraId":
➥ "8eeceeb4fd6aa3897e05b5361b5c27c6e98bc29707484f95994f49437536599e",
"Name": "test_pod",
"Namespace": "",
“Networks": [],
"Status": "Running",
"Labels": {}
},
{
"Cgroup": "user.slice",
"Containers": [
{
"Id": "7a7405a31917da7bde01a6000809e0ee12f40b69fc76963d87a8ae254b34d8c7",
"Names": "e10eb9303705-infra",
"Status": "configured"
}
],
"Created": "2022-01-05T09:18:01.648324833-05:00",
"Id": "e10eb930370592834fc168a7460fabe9b3e0e20a54b48a2bf3236cecd75f8138",
"InfraId":
➥ "7a7405a31917da7bde01a6000809e0ee12f40b69fc76963d87a8ae254b34d8c7",
"Name": "mypod",
"Namespace": "",
"Networks": [],
"Status": "Created",
"Labels": {}
}
]
如果你尝试对 Docker API 端点执行相同的查询,它将因找不到错误而失败。
$ curl -s --unix-socket $XDG_RUNTIME_DIR/podman/podman.sock
➥ http://d/v1.0.0/pods/json
Not Found
这是因为 Docker API 和 Docker 本身都不理解 pods。虽然你可以使用 curl 等工具直接通过 API 进行大量测试,但使用像 Python 这样的高级语言与 API 交互会更好。
9.3 与 Podman 交互的 Python 库
Python 可以说是 Linux 平台上最受欢迎的脚本语言。几乎每个 Linux 系统都默认安装了 Python。就像 API 一样,有两个非常相似的 Python 库可用:与兼容库一起工作的 docker-py 库,以及支持较新 Libpod API 的 podman-py。本节将使用一些 Python 命令,可能需要有限的 Python 知识,但如果你经验有限,也足够你跟随。
9.3.1 使用 docker-py 与 Podman API 一起使用
与容器交互最流行的 Python 包是 docker-py (github.com/docker/docker-py). Docker-py 是一个最初用于与 Docker 守护进程通信的 Python 绑定库。它也可以与 Podman 兼容服务通信。Docker-py 库允许你运行与 Podman 命令相同的容器,但你可以在 Python 中完成这项操作。
基于 docker-py 构建了数千个工具和示例,并在生产环境中运行。这些工具已被用于 CI/CD 系统、GUI、管理工具和调试工具。对于这些命令,你可以使用 Podman compat API,它与 docker-py 一起工作得很好。
通常,你可以使用 apt-get 或 dnf install 安装 docker-py。它也通过 PyPI 提供。请咨询您 Linux 平台的安装命令。在基于 RPM 的系统上,该软件包称为 python-docker。
在我的基于 Red Hat 的系统上,我使用以下 dnf 命令安装它:
$ sudo dnf install -y python-docker
在 docker-py 安装后,你可以开始使用它来与 Podman 服务交互。想象一下,你想要编写一个 Python 脚本来与 Podman 服务交互,列出当前可用的镜像。注意我必须将 DockerClient 的 URL 重置为指向 Podman 套接字。你可能需要修改系统上 podman.sock 的位置:
$ cat > images.py << _EOF
import docker
client=docker.DockerClient(base_url='unix:/run/user/1000/podman/podman.sock')
print(client.images.list(all=True))
_EOF
运行 images.py 脚本,查看你盒子上安装的镜像:
$ python images.py
[<Image: 'quay.io/rhatdan/myimage:latest'>, <Image: 'k8s.gcr.io/pause:3.5'>]
在 Python 脚本中完全指定 Podman 套接字的路径是不方便的,但幸运的是,Docker 工具支持一个名为 DOCKER_HOST 的特殊环境变量。你可以设置 DOCKER_HOST 以指向实现 Docker API 的套接字。
首先,将 DOCKER_HOST 环境变量设置为指向 podman.sock:
$ export DOCKER_HOST=unix://$XDG_RUNTIME_DIR/podman/podman.sock
现在,将脚本更改为使用 docker.from_env() 函数:
$ cat > images.py << _EOF
import docker
client=docker.from_env()
print(client.images.list(all=True))
_EOF
运行新的脚本,你会看到它使用 DOCKER_HOST 环境变量来发现 Podman 服务套接字:
$ python images.py
[<Image: 'quay.io/rhatdan/myimage:latest'>, <Image: 'k8s.gcr.io/pause:3.5'>]
注意:在许多 Linux 发行版中,podman-docker 包是本地可用的。当你安装此包时,它会安装一个 Docker 脚本,该脚本将 Docker 命令重定向为运行 Podman 命令。它还将所有 Docker man 页链接到 Podman man 页。最后,它为 rootful 容器在 docker.sock 和 podman.sock 之间设置符号链接,允许 Docker 工具使用 /var/run/podman/podman.sock,无需修改环境变量。
优点在于这个 DOCKER_HOST 技巧可以用于大多数多年来编写的 docker-py 脚本,并且你可以轻松地将你的脚本从使用 Docker 守护进程切换到使用 Podman 服务。如果你想使用更高级的 Podman 功能,你需要使用 podman-py 包。
9.3.2 使用 podman-py 与 Podman API
Podman-py (github.com/containers/podman-py),像 docker-py 一样,是一个用于与 Podman 服务通信的 Python 绑定库。podman-py 库比 docker-py 库更新,并使用 Libpod API 支持 Podman 的所有高级功能。
Podman Python 库使用 podman.sock 的默认位置并自动连接到它。当以非 root 用户运行时,库连接到位于 /run/user/$UID/podman/podman.sock 的无根套接字。以 root 用户运行 Python 并使用 Podman 库时,会自动连接到 /run/podman/podman.sock。
与 docker-py 类似,在我的系统上,我可以通过 python-podman 包安装 podman-py 库:
$ sudo dnf install -y python-podman
Last metadata expiration check: 0:27:40 ago on Sun 19 Jun 2022 02:14:49 PM EDT.
Dependencies resolved.
...
Installed:
python3-podman-3:4.0.0-1.fc36.noarch
Complete!
现在构建一个功能相似的脚本,podman-images.py,使用 podman-py 库。这次你不需要担心 Podman 套接字的位置。podman-py 库连接到默认位置:
$ cat > podman-images.py << _EOF
import podman
client=podman.PodmanClient()
print(client.images.list())
_EOF
运行脚本,你将看到与 docker-py 示例相同的结果,但这个库使用的是 Libpod API:
$ python podman-images.py
[<Image: 'quay.io/rhatdan/myimage:latest'>, <Image: 'k8s.gcr.io/pause:3.5'>]
如果你想要展示高级功能,例如 Podman 数据库中所有 pod 的信息,调用 pod.lists() 函数,并遍历每个 pod:
$ cat >> podman-images.py << _EOF
for i in client.pods.list():
print(i.attrs)
_EOF
Now the script shows the images as well as information on the pods.
$ python podman-images.py
[<Image: 'quay.io/rhatdan/myimage:latest'>, <Image: 'k8s.gcr.io/pause:3.5'>]
{'Cgroup': 'user.slice', 'Containers': [{'Id':
➥ 'f8679839c25729eb422d38e505ae3a4b7ffe18942e2f77a997bd388e0f52313e',
➥ 'Names': '116291543d56-infra', 'Status': 'configured'}], 'Created':
➥ '2021-12-14T06:44:04.56055485-05:00', 'Id': '116291543d5691c597132ec73a428f29f2c1f71a65fdfbaca17eb5440a5d47f6',
➥ 'InfraId': 'f8679839c25729eb422d38e505ae3a4b7ffe18942e2f77a997bd388e0f52313e',
➥ 'Name': 'mypod', 'Namespace': '', 'Networks': None, 'Status':
➥ 'Created', 'Labels': {}}
如你所见,使用 Python 绑定,你可以开始构建一个 Python 版本的 Podman,它可以与远程套接字进行通信。
9.3.3 应该使用哪个 Python 库?
podman-py 库的开发者基于 docker-py 库进行设计,以便于开发者过渡。如果你想要构建一个与 Podman 和 Docker 兼容的应用程序,唯一的选择是 docker-py,因为 podman-py 不与 Docker 兼容。如果你想要利用 Podman 的高级功能,你必须使用 podman-py。Podman-py 正在积极开发中,但 docker-py 拥有庞大的安装基础。Podman-py 可以与 rootful 和 rootless Podman 服务无缝工作,而如果你使用 docker-py,你必须设置 DOCKER_ HOST 环境变量以指向 podman.socket。表 9.4 比较了 podman-py 和 docker-py 库的功能,以帮助你了解何时使用特定的库。
表 9.4 Podman-py 与 docker-py 对比
| 支持 | Podman-py | Docker-py |
|---|---|---|
| Podman 服务 | ✔ | ✔ |
| Docker 守护进程 | ✘ | ✔ |
| 支持 pods | ✔ | ✘ |
| 高级 Podman 功能 | ✔ | ✘ |
使用低级别的 Python 库 docker-py 和 podman-py 与容器引擎守护进程和服务进行通信,工程师开发了高级工具来编排和管理容器。其中最受欢迎的是 docker-compose。
9.4 使用 Podman 服务与 docker-compose
在前面的章节中,你已经看到了如何使用 Podman 命令行管理容器,以及如何使用 podman play kube 启动 Kubernetes YAML 来管理多个容器。你已经介绍了使用 Kubernetes 启动容器。在本节中,你将使用另一个编排工具,docker-compose (docs.docker.com/compose),通常简称为 compose。
compose 是启动容器中最受欢迎的工具之一。compose 工具早于 Kubernetes,专注于在单个节点上编排多个容器,而 Kubernetes 则在多个节点上编排多个容器。compose,就像 Kubernetes 一样,使用 YAML 文件来定义其容器。compose 被创建的一个原因是因为构建复杂的命令行来运行多个容器可能很复杂。使用结构化语言如 YAML 使得支持在单个节点上运行具有多个容器的复杂应用程序变得更加容易。
compose 拥有庞大的用户群,你可能会想在你的基础设施中运行一个 compose YAML 文件。如果你不相信这会发生,你可以跳过这一节。
compose 工具是用 docker-py 编写的,并通过使用 Docker REST API 启动容器。由于 Podman 现在支持 compat REST API,它也支持使用 docker-compose 启动 Podman 容器。因为 Podman 既可以以无根模式工作,也可以以有根模式工作,您甚至可以使用 docker-compose 启动无根 Podman 容器。
在本节的其余部分,您将创建一个 compose YAML 文件,只是为了了解 compose 命令如何与 Podman 服务一起工作。您首先需要安装 docker-compose。在我的 Fedora 系统上,我可以使用以下命令完成此操作:
$ sudo dnf -y install docker-compose
通过运行以下命令确保 Podman systemd socket-activated 服务正在运行:
$ systemctl –user start podman.socket
通过 ping 端点检查系统服务是否正在运行,并查看是否收到响应。在您继续下一步之前,此步骤必须成功。
$ curl -H "Content-Type: application/json" --unix-socket
➥ $XDG_RUNTIME_DIR/podman/podman.sock http://localhost/_ping
OK
由于 docker-compose 支持使用 DOCKER_HOST 环境变量,请确保使用以下命令设置它:
$ export DOCKER_HOST=unix://$XDG_RUNTIME_DIR/podman/podman.sock
如本节前面所述,compose 支持其自己的 YAML 文件,这与第八章中描述的 Kubernetes YAML 不同。
首先,创建一个名为 example 的目录,然后进入它。将您一直在使用的 html 目录移动到 example 目录中:
$ mkdir example
$ mv ./html example
$ cd example
您需要在您一直在工作的示例目录中创建 docker-compose.yaml 文件。该 YAML 文件将基于 quay.io/rhatdan/myimage:latest 创建一个名为 myapp 的容器。设置容器使用来自主机 ./html 目录的卷以及一个内置卷 myapp_vol,该卷仅用于示例:
cat > docker-compose.yaml << _EOF
version: "3.7"
services:
myapp:
image: quay.io/rhatdan/myimage:latest
volumes:
- ./html:/var/www/html
- myapp_vol:/vol
ports:
- 8080:80
volumes:
myapp_vol: {}
_EOF
现在,清理您系统上的镜像和容器,以确保您从一个干净的状态开始。运行以下命令来完成此操作:
$ podman pod rm --all --force
$ podman rm --all --force
$ podman rmi --all --force
$ podman volume rm --all --force
要展示 compose 如何与 Podman 服务交互,请使用 compose 命令启动容器。注意,compose 会告诉 Podman 下载镜像。然后 compose 会告诉 Podman 创建一个名为 example_myapp_1 的容器,以及一个名为 example_myapp_vol 的卷,该卷将与 ./html 目录一起挂载到容器中。
列表 9.2 执行 docker-compose 对 Podman 套接字的输出
$ docker-compose up
Pulling myapp (quay.io/rhatdan/myimage:latest)... ❶
59bf1c3509f3: Download complete
c059bfaa849c: Download complete
Creating example_myapp_1 ... done ❷
Attaching to example_myapp_1
❶ 拉取 myimage 镜像
❷ 创建 example_myapp_1 容器
在不同的终端中运行 podman ps 命令:
$ podman ps --format "{{.ID}} {{.Image}} {{.Ports}} {{.Names}}"
230fce823ff6 quay.io/rhatdan/myimage:latest 0.0.0.0:8080->80/tcp
➥ example_myapp_1
现在检查 Podman 是否创建了一个卷:
$ podman volume ls
DRIVER VOLUME NAME
local example_myapp_vol
返回到原始窗口,并按 Ctrl-C 停止 docker-compose:
^CGracefully stopping... (press Ctrl+C again to force)
Stopping example_myapp_1 ... done
这将关闭容器:
$ podman ps --format "{{.ID}} {{.Image}} {{.Ports}} {{.Names}}"
如果您执行 podman ps -a 命令,您将看到容器仍然存在,但未运行:
$ podman ps -a --format "{{.ID}} {{.Image}} {{.Ports}} {{.Names}}"
230fce823ff6 docker.io/library/alpine:latest 0.0.0.0:8080->80/tcp
➥ example_myapp_1
现在,如果您运行 docker-compose down,它将告诉 Podman 从系统中删除容器:
$ docker-compose down
Removing example_myapp_1 ... done
Removing network example_default
再次使用 podman ps -a 命令验证所有容器都已消失:
$ podman ps -a --format "{{.ID}} {{.Image}} {{.Ports}} {{.Names}}"
如您所见,Podman 与 docker-compose 一起很好地工作,以编排容器。
小贴士:虽然 docker-compose 与 Podman 服务配合得很好,但我认为如果您正在启动一个全新的项目,最好使用 Kubernetes YAML 和 podman play kube,因为这可以使您更容易地将容器移动到 Kubernetes。
正如您所看到的,Podman 服务对于允许远程进程操作您的 pods 和容器非常有用。甚至 Podman 命令也可以用作客户端并与 Podman 服务进行通信。
9.5 podman - -remote
当您扩展应用程序时,您可能希望在多台机器上运行容器化的应用程序。您可以通过 ssh 登录到每个盒子并在本地运行 Podman 命令来管理环境,或者您可以编写代码来使用 9.4 节中描述的 Python 库。Podman 开发者还在 Podman 命令中构建了客户端支持。您可以使用 podman 命令直接连接到这些远程 Podman 服务并管理远程机器上的容器环境。
Podman 命令有一个特殊选项,--remote,允许它与通过套接字激活的 Podman 服务进行通信。它不是作为 Podman 进程的子进程执行命令和容器,而是通过 REST API 与服务进行通信。

图 9.2 podman --remote 连接到本地 podman.socket
由于 Podman 是运行 Linux 容器的工具,因此 complete podman 命令只能在 Linux 上运行。Podman 开发者希望支持其他操作系统,至少在客户端模式下。为了在非 Linux 机器上运行 Podman,Podman 可以以两种不同的方式构建。到目前为止,您一直在使用具有 --remote 选项的完整 Podman。Podman 可执行文件可以编译成仅支持与 Podman 服务通信的形式。以这种方式构建的 Podman 通常被称为 podman-remote。podman-remote 命令是某些操作系统(如 Mac 和 Windows,在附录 E 和 F 中有更全面的介绍)中提供的命令。如果您在阅读这本书的同时在 Mac 或 Windows 机器上测试 Podman,那么您已经在使用 podman-remote,它透明地与在虚拟机或不同机器上运行的 Podman 服务进行通信。
9.5.1 本地连接
如前所述,podman --remote 命令默认连接到本地的 podman.socket,称为本地连接(图 9.2)。尝试使用在 9.1.1 节中启用的 Podman 系统服务运行 podman --remote。注意 podman --remote 版本向您显示了 Podman 客户端和 Podman 服务器的版本;在这种情况下,它们是相同的可执行文件。
列表 9.3 执行 podman --remote 的版本 API 的输出
$ podman --remote version
Client:
Version: 4.1.0 ❶
API Version: 4.1.0
Go Version: go1.18.2
Built: Sun Jun 19 07:35:42 2022
OS/Arch: linux/amd64
Server:
Version: 4.1.0 ❷
API Version: 4.1.0
Go Version: go1.18.2
Git Commit: a2b78b627f0a9deef83a5b5e4ecffc9cdb5a72b1-dirty
Built: Sun Jun 19 07:35:42 2022
OS/Arch: linux/amd64
❶ Podman 的客户端版本
❷ Podman 的服务器版本
您可以使用完全相同的命令来启动容器:
$ podman --remote run ubi8 echo hi
Resolved "ubi8" as an alias (/etc/containers/registries.conf.d/
➥ 000-shortnames.conf)
Trying to pull registry.access.redhat.com/ubi8:latest...
..
hi
如您所想,在这种模式下并不那么有用,因为您可以在不使用--remote选项的情况下运行 Podman,并管理相同的容器环境。本地连接主要用于 API 的测试,尤其是在持续集成(CI)系统中。当您使用podman --remote与真正远程的机器进行通信时,它变得更有趣。
9.5.2 远程连接
podman --remote命令的主要目的是允许您使用 Podman 服务在另一台机器上操作 pods 和容器。在 Linux 机器或 VM 上安装 Podman,该机器也运行着 SSH 守护进程。在本地操作系统上,当您运行 Podman 命令时,Podman 通过 SSH 连接到服务器。然后,它通过 systemd 套接字激活连接到 Podman 服务,并通过我们的 REST API 进行通信,如图 9.3 所示。

图 9.3 podman --remote通过 SSH 连接到服务器机器
带有--remote选项的 Podman 命令行界面与常规的 Podman 命令完全相同。当您运行 Podman 命令时,感觉就像您是在本地运行容器;然而,容器进程是在远程机器上运行的。有一些选项在远程模式下不支持,列于表 9.5 中。
表 9.5 podman --remote命令不支持选项
| 选项 | 说明 |
|---|---|
--env-host |
在两台不同的机器上共享环境几乎没有意义;在某些情况下,这些可能是不同的操作系统,例如 Windows 和 Mac 与 Linux Podman 服务进行通信。 |
--group-add=keep-groups |
--group-add选项在--remote模式下工作,但keep-groups特殊标志不行。keep-groups标志告诉 Podman 将当前进程可访问的组泄露到容器中。由于这是一个客户端-服务器过程,泄露是不可能的。 |
--http-proxy |
--http-proxy选项告诉 Podman 使用客户端机器上的 HTTP 代理环境变量,并将它们泄露到服务器。由于代理通常设置在服务器上,因此不允许与--remote选项一起使用--http-proxy选项。 |
--preserve-fds |
--preserve-fds选项会将调用进程的文件描述符泄露到容器中;由于这是一个远程连接,没有方法可以泄露文件描述符。 |
--volume |
这是支持的,但源卷将来自远程机器,不一定是从运行podman命令的机器(除非它们在同一台机器上)。如果您正在使用 VM,您需要首先将宿主机的目录挂载到 VM 中;然后 VM 内部的 Podman 看到挂载并将其挂载到容器中。 |
--latest, -l |
由于可能同时有多个不同的用户在与同一服务器通信,--latest的概念太冒险,因此不支持。 |
Podman 命令在服务器上执行。从客户端的角度来看,Podman 好像是在本地运行。现在您需要完成远程服务器上 Podman 服务的配置。
启用 SSHD 连接
为了 Podman 客户端能够与服务器通信,您需要在您的 Linux 机器上启用并启动 SSH 守护进程,如果它尚未启用的话:
$ sudo systemctl enable --now -s sshd
现在 SSHD 守护进程正在运行,您需要在远程机器上启用 Podman 服务。
在服务器机器上启用 Podman 服务
在执行任何 Podman 客户端命令之前,您必须在 Linux 服务器或虚拟机上启用 podman.sock systemd 服务。在这些示例中,您是以普通、非特权用户身份运行 Podman 的。为了使服务器上的无根 Podman 正确运行,请使用以下命令永久启用此套接字:
$ systemctl --user enable --now podman.socket
通常,当您从系统中注销时,systemd 会停止系统上的所有进程。您需要告诉 systemd 允许远程用户进程在无根模式下 linger:
$ sudo loginctl enable-linger $USER
这也告诉 systemd 在启动时监听此套接字。一旦在一个系统上运行了该服务,您可以使用 Podman 命令验证套接字是否正在监听:
$ podman --remote info
Host:
arch: amd64
buildahVersion: 1.16.0-dev
...
注意:您可以使用以下命令启用根有 podman 服务:
$ sudo systemctl enable --now podman.socket
之前的 enable-linger 命令仅适用于无根模式。现在您已经启用了远程服务并使其与 SSHD 守护进程一起运行,您可以回到客户端机器。
9.5.3 在客户端机器上设置 SSH
远程 Podman 使用 SSH 在客户端和服务器之间进行通信,当它们位于不同的机器上时。默认情况下,SSH 会要求您在每次命令中提供用户名和密码,除非您设置了 SSH 密钥。要设置您的 SSH 连接,您需要从您的客户端机器生成一个 SSH 密钥对。如果您已经有了现有的 SSH 密钥,您可以直接使用它们;如果您已经与服务器共享了密钥,那就更好了。在我的 Linux 系统上,我可以用以下类似的命令生成 SSH 密钥:
$ ssh-keygen -t ed25519
Generating public/private ed25519 key pair.
Enter file in which to save the key (/home/myuser/.ssh/id_ed25519):
生成密钥完成后,您可以使用 ssh-copy-id 命令或类似命令在客户端和服务器机器之间设置信任。默认情况下,公钥将位于您的家目录下 $HOME/.ssh/id_ed25519.pub。您需要将 id_ed25519.pub 的内容复制并追加到 Linux 服务器上的 ~/.ssh/authorized_keys。有关配置您的 SSH 环境的更多信息,请参阅 red.ht/3HuxPT6:
$ ssh-copy-id myuser@192.168.122.1
passwd:
如果您不想使用 SSH 密钥,每次执行 Podman 命令时都会提示您输入登录密码。现在您已经与服务器共享了 SSH 密钥,下一步是配置 Podman 的连接。
9.5.4 配置连接
podman system connection 命令允许您管理用于 podman --remote 命令的 SSH 连接。您可以使用 podman system connection add 命令添加连接,将连接命名为 server1。默认将选择身份文件,或者您可以使用 -–identity 选项指定要使用的 SSH 密钥。最后,您需要指定 Podman 套接字的完整 SSH URL。这包括用户账户 myuser、IP 地址以及用户账户的 Podman 套接字路径:
$ podman system connection add server1 --identity ~/.ssh/id_ed25519
➥ ssh://myuser@192.168.122.1/run/user/1000/podman/podman.sock
此 Podman 命令向 Podman 添加远程连接。由于这是第一个添加的连接,Podman 将该连接标记为默认连接。
使用 podman system connection list 命令列出可用的连接。注意,连接名称后面的 * 表示它是默认连接:
$ podman system connection list
Name Identity URI
system1* id_ed25519
➥ ssh://myuser@192.168.122.1/run/user/1000/podman/podman.sock
现在,您可以使用 podman info 测试连接:
$ podman --remote info
host:
arch: amd64
buildahVersion: 1.23.1
cgroupControllers:
...
注意:如果您有多个连接并且想为所有可能的选项选择非默认的 man podman-system-connection,可以使用 --connection (-c) 选项。
您可以使用 podman 选项或 podman-remote 客户端来管理在 Linux 服务器或虚拟机上运行的容器。客户端与服务器之间的通信高度依赖于 SSH 连接,并鼓励使用 SSH 密钥。一旦您在远程服务器上安装了 Podman,您需要使用 podman system connection add 命令设置连接,然后后续的 Podman 命令可以使用该连接。表 9.6 列出了可用的 Podman 系统命令。
表 9.6 Podman 系统命令
| 命令 | 手册页 | 描述 |
|---|---|---|
connection |
podman-system-connection(1) |
管理远程 SSH 目标 |
df |
podman-system-df(1) |
显示 Podman 的磁盘使用情况 |
info |
podman-system-info(1) |
显示 Podman 系统信息 |
migrate |
podman-system-migrate(1) |
将容器迁移到新的用户命名空间 |
prune |
podman-system-prune(1) |
删除未使用的 pod、容器、卷和镜像数据 |
renumber |
podman-system-renumber(1) |
迁移锁号 |
reset |
podman-system-reset(1) |
重置 Podman 存储 |
service |
podman-system-service(1) |
运行 API 服务 |
摘要
-
Podman 可以作为 REST API 服务运行。
-
Podman 支持两个 REST API 端点。
-
Podman 套接字支持两个 API。
-
兼容模式或 Docker 模式允许 Docker 客户端工具与 Podman 一起工作。
-
Podman 模式允许远程客户端利用 Podman 的高级功能。
-
Podman-py 是一个用于与 Podman 服务通信的 Python 绑定库。
-
Docker-py 是一个用于与 Podman 兼容服务通信的 Python 绑定库。
-
Podman 支持使用兼容服务运行
docker-compose,以在单个节点上编排compose容器。 -
podman--remote命令通过 SSH 与 Podman 服务通信,以管理容器。 -
podmansystemconnect命令管理对远程 Podman 服务的 SSH 连接,使得管理您环境中的容器更加便捷。
第四部分:容器安全
在本书的最后一部分,第四部分,我透露了我所知道的关于容器安全的一切。这部分内容非常技术性,但你将学习到一些关键概念,这些概念将帮助你理解当容器被拒绝权限时的情况。它还从安全角度解释了在容器内运行应用程序的好处。将应用程序容器化可以为你的主机系统提供巨大的保护,防止潜在的攻击。
在第十章中,我解释了 Podman 使用来隔离容器彼此以及与主机系统之间的所有内核功能。我解释了 SELinux、seccomp、Linux 能力、只读挂载点以及许多其他功能。
第十一章深入探讨了安全考虑。你将学习在生产环境中运行容器的安全最佳实践,你应该如何设计你的应用程序,以及你应该如何在生产环境中运行你的容器化应用程序。
10 安全容器隔离
本章涵盖了
-
所有用于保持容器彼此隔离的 Linux 安全功能
-
容器内进程需要的对内核文件系统的只读访问,但必须阻止写入访问
-
隐藏内核文件系统以从宿主系统隐藏信息
-
Linux 能力限制 root 的权力
-
PID、IPC 和网络命名空间,它们隐藏了操作系统的大部分内容,使容器内的进程无法访问。
-
挂载命名空间,它与 SELinux 一起限制容器进程只能访问指定的镜像和卷
-
用户命名空间,它允许您在容器内写入不是 root 的 root 进程
在本章和第十一章中,我回顾并演示了使用 Podman 运行容器时的一些额外的安全考虑。其中一些内容在其他章节中已经介绍过,但我认为从安全角度集中关注这些功能是有用的。
我看到人们在运行容器时最常见的问题之一是,当容器进程被拒绝某些访问时,用户的第一个反应就是以--privileged模式运行容器,这会关闭您容器的所有安全隔离。了解如何处理本章中讨论的安全功能可以帮助您避免需要这样做。
当我从安全的角度看待容器时,我检查如何保护宿主内核和文件系统免受容器内进程的侵害。我写了一本彩绘书,《容器彩绘书》(red.ht/3gfVlHF),由 Máirín Duffy (@marin) 插图,描述了基于三只猪的容器安全功能(图 10.1)。

图 10.1 容器彩绘书 (red.ht/3gfVlHF)
我在书中使用的类比是三只猪是应用程序。然后我讨论了它们的生活方式和与计算机系统相比的住房选择。
单户住宅相当于单个隔离节点上的一个应用程序。住在联排别墅中相当于在单独的虚拟机中运行每个应用程序。住在酒店或公寓楼中类似于容器,您有自己的公寓,但您依赖于前台的安全来控制您生活空间的使用。如果前台被攻破,那么您的公寓也将被攻破。容器在这方面类似,因为它们依赖于内核的安全。如果一个容器可以接管宿主内核,那么它可以接管系统上运行的所有的容器应用程序。此外,如果它们逃逸到底层文件系统,它们可能能够读取和写入系统上所有容器的所有数据。
从这个角度来看,我认为宿主机的首要目标是保护宿主内核和文件系统免受容器进程的侵害。本章的其余部分描述了用于保护宿主内核和文件系统免受容器进程侵害的工具。
保护内核免受潜在敌对容器的侵害是容器安全的首要目标。如果内核存在漏洞,那么整个系统和所有容器都将面临风险。在许多情况下,容器对宿主系统的唯一暴露就是宿主内核本身。
容器内的进程可以通过许多不同的方式与内核进行交互。本节将检查这些通信以及用于保护容器进程的操作系统功能。
Linux 内核提供了允许进程进行通信和配置内核的文件系统。保护这些文件系统免受受限制的容器进程的侵害是您将首先检查的安全功能。
10.1 只读 Linux 内核伪文件系统
这些 Linux 内核伪文件系统通常挂载在/proc 和/sys 下。表 10.1 列出了我机器上挂载的一些 Linux 内核伪文件系统。
表 10.1 仅作为只读挂载的文件系统
| 文件系统挂载点 | 伪文件系统描述 |
|---|---|
| /sys | sysfs 文件系统允许从用户空间查看和操作由内核空间创建和销毁的对象。 |
| /sys/kernel/security | 安全伪文件系统用于读取和配置通用安全模块。一个例子是完整性测量架构(IMA)模型。 |
| /sys/fs/cgroup | cgroup 文件系统用于管理控制组。 |
| /sys/fs/pstore | pstore 文件系统存储对诊断系统崩溃原因有用的非易失性信息。 |
| /sys/fs/bpf | 勃朗克斯包过滤器(BPF)文件系统是一种机制,允许用户程序对 Linux 内核进行仪器化,以揭示内核信息并控制系统上进程的运行方式。 |
| /sys/fs/selinux | SELinux 文件系统用于在内核中配置 SELinux(见第 10.2.7 节)。 |
| /sys/kernel/config | configfs 文件系统用于从用户空间创建、管理和销毁内核对象。 |
大多数进程需要读取这些伪内核文件系统的访问权限才能成功,但只有管理员进程需要写入访问权限。通常,内核依赖于根用户与非根用户或拥有CAP_SYS_ADMIN能力(见第 10.2.2 节)来修改这些文件系统。
通常容器需要以 root 用户身份运行,需要容器安全使用其他方法来防止 root 进程写入这些内核文件系统。Podman 不挂载大多数这些高级内核伪文件系统。它只以只读方式挂载 /sys, /sys/fs/cgroup, 和 /sys/fs/selinux。当您处于 PID 命名空间中时,/proc 文件系统会发生变化,这意味着容器内的 /proc 不是主机的 /proc。容器内的进程只能影响容器内的其他进程。
/sys 文件系统和命名空间的 /proc 文件系统有时会将主机信息泄露到容器中。因此,Podman 在文件上挂载 /dev/null,并在目录上挂载只读 tmpfs 文件系统,以防止容器访问。Podman 还将某些子目录以只读方式绑定挂载到自身,以防止容器进程写入它们。请参阅表 10.2 以获取 Podman 为安全目的覆盖的文件和目录的完整列表。
表 10.2 Podman 覆盖的文件系统字段
| 隐藏类型 | 路径 |
|---|---|
| 只读 tmpfs 挂载覆盖目录 | /proc/acpi, /proc/kcore, /proc/keys, /proc/latency_stats, /proc/timer_list, /proc/timer_stats, /proc/sched_debug, /proc/scsi, /sys/firmware, /sys/fs/selinux, /sys/dev/block |
| 只读绑定挂载覆盖目录 | /proc/asound, /proc/bus, /proc/fs, /proc/irq, /proc/sys, /proc/sysrq-trigger |
我发现几乎所有的容器镜像都运行得很好,并带有这种额外的安全性。有时容器化的应用程序可能需要访问这些被覆盖的目录之一。
10.1.1 揭示被隐藏的路径
而不是强制容器以 --privileged 模式运行,您可以告诉 Podman 揭示一个目录。在以下示例中,您运行一个容器并看到 /proc/scsi 下没有文件或目录,因为它被 tmpfs 覆盖:
$ podman run --rm ubi8 ls /proc/scsi
您可以使用 --security-opt unmask=/proc/scsi 标志来移除挂载点并暴露底层文件和目录:
$ podman run --rm --security-opt unmask=/proc/scsi ubi8 ls /proc/scsi
device_info
scsi
sg
您甚至可以使用 * 来卸载特定路径下的所有目录:
$ podman run --rm --security-opt unmask=/proc/* ubi8 ls /proc/scsi
device_info
scsi
sg
揭示会使您的容器稍微不太安全,但比完全使用 --privileged 并关闭所有安全措施要好得多。在特定情况下,您可能希望通过覆盖伪文件系统的一部分来提高系统的安全性。podman 的 run 手册页列出了被隐藏的文件系统:
$ man podman run
...
• unmask=ALL or /path/1:/path/2, or shell expanded paths (/proc/*):
Paths to unmask separated by a colon. If set to ALL, it will unmask all the
paths that are masked or made read only by default. The default masked
paths are /proc/acpi, /proc/kcore, /proc/keys, /proc/latency_stats,
/proc/sched_debug, /proc/scsi, /proc/timer_list, /proc/timer_stats,
/sys/firmware, and /sys/fs/selinux.
The default paths that are read only are /proc/asound, /proc/bus,
/proc/fs, /proc/irq, /proc/sys, /proc/sysrq-trigger, /sys/fs/cgroup.
10.1.2 隐藏其他路径
如果您非常注重安全性或有一个您不信任的容器,它提供了某些访问权限给容器,您可以使用 --security-opt mask 标志添加额外的隐藏路径。例如,如果您想防止容器进程看到 /proc/sys/dev 中的设备,请运行以下命令:
$ podman run --rm ubi8 ls /proc/sys/dev
cdrom
hpet
i915
mac_hid
raid
scsi
tty
您可以使用 --security-opt mask=/proc/sys/dev 标志来覆盖它:
$ podman run --rm --security-opt mask=/proc/sys/dev ubi8 ls /proc/sys/dev
你看到了 Podman 如何防止 root 进程读取和,更重要的是,写入伪文件系统。容器进程实际上可以通过查看/proc/self/mountinfo 来看到容器内挂载了什么。
列表 10.1 Podman 容器内的挂载表
$ podman run –rm ubi8 cat /proc/self/mountinfo
...
1628 1610 0:5 /null /proc/kcore rw,nosuid –
➥ devtmpfs devtmpfs rw,seclabel,size=4096k,
➥ nr_inodes=1048576,mode=755,inode64 ❶
...
1620 1595 0:86 / /sys/firmware ro,relatime - tmpfs tmpfs ❷
rw,context="system_u:object_r:container_file_t:s0:c406,c915",size=0k,uid=32
➥ 67,gid=3267,inode64
...
❶ 显示/dev/null 挂载在/proc/kcore 上
❷ 显示 tmpfs 以只读方式挂载在/sys/firmware 上
你可能自己在想,“如果容器知道已经挂载了什么,那么是什么阻止容器内的 root 用户移除挂载或重新挂载文件系统的读写,然后攻击宿主内核?
10.2 Linux 能力
大多数 Linux 用户都了解 Linux 有两种类型的用户:root(特权进程)和所有人(非特权进程)。root 拥有全部权力,而 non-root 的权力则受到很大限制,特别是在配置和修改内核时。有时非特权进程需要特权来执行某些命令行ping或sudo。Linux 支持一种标记这些文件为setuid的方式,当非特权进程执行它们时,新进程会获得特权。
在 Linux 中,特权进程和非特权进程之间的二进制差异大约在 2000 年结束。内核工程师将 root 的权力分解为不同的特权能力组。目前,在我的系统中,Linux 内核支持 41 个。你可以使用capsh程序查看能力的完整列表。执行capsh程序以查看系统上的能力列表。你会看到你的进程的current能力集为空。Bounding能力集是进程可以通过执行setuid程序获得的能力集。
列表 10.2 capsh –print 显示用户进程可用的能力
$ capsh --print
Current: = ❶
Bounding set = ❷
cap_chown,cap_dac_override,cap_dac_read_search,cap_fowner,cap_fsetid,cap_kill,
➥ cap_setgid,cap_setuid,cap_setpcap,cap_linux_immutable,cap_net_bind_service,
➥ cap_net_broadcast,cap_net_admin,cap_net_raw,cap_ipc_lock,cap_ipc_owner,
➥ cap_sys_module,cap_sys_rawio,cap_sys_chroot,cap_sys_ptrace,cap_sys_pacct,
➥ cap_sys_admin,cap_sys_boot,cap_sys_nice,cap_sys_resource,cap_sys_time,
➥ cap_sys_tty_config,cap_mknod,cap_lease,cap_audit_write,cap_audit_control,
➥ cap_setfcap,cap_mac_override,cap_mac_admin,cap_syslog,cap_wake_alarm,
➥ cap_block_suspend,cap_audit_read,cap_perfmon,cap_bpf,cap_checkpoint_restore
Ambient set =
...
uid=3267(dwalsh) euid=3267(dwalsh) ❸
gid=3267(dwalsh)
❶ 当前能力集显示没有能力。
❷ 限制能力集显示所有(41)能力。
❸ 因为你是以普通用户身份运行capsh命令的,所以你看到你的 UID 和 GID 被列出。
这意味着你的用户进程可以执行sudo命令并获得与 root 相同的全部能力。你可以通过执行man capabilities来阅读关于每个能力做什么的信息。多年来,社区已经发现几乎所有的容器都不需要完整的能力列表,因为它们很少修改内核。
10.2.1 丢弃的 Linux 能力
由于容器限制进程不应该操作操作系统,特别是内核,Podman 可以在其容器中以远少于 root 的能力运行。你可以通过执行相同的capsh程序来检查 Podman 容器内可用的默认能力列表。
列表 10.3 Podman 容器内可用的默认能力列表
$ podman run --rm ubi8 capsh --print
Current: = ❶
cap_chown,cap_dac_override,cap_fowner,cap_fsetid,cap_kill,cap_setgid,
➥ cap_setuid,cap_setpcap,cap_net_bind_service,cap_sys_chroot,
➥ cap_setfcap+eip
Bounding set = ❷
cap_chown,cap_dac_override,cap_fowner,cap_fsetid,cap_kill,cap_setgid,
➥ cap_setuid,cap_setpcap,cap_net_bind_service,cap_sys_chroot,cap_setfcap
...
uid=0(root) ❸
gid=0(root)
groups=
❶ 当前能力集只显示 11 个能力,因为容器进程是以 root 身份运行的。
❷ 能力的边界集显示了相同的能力(11 个)。
❸ 因为容器默认以 root 身份运行,所以你看到 UID 和 GID 都是 root。
如你所见,Podman 默认在运行容器时放弃了 30 个能力——从 41 个减少到 11 个。即使容器具有 root 权限,它也比系统上的 root 权限弱得多。
注意 Docker 也放弃了能力,但留下了 14 个能力。Podman 通过放弃以下附加能力来运行更严格的安全:CAP_ MKNOD、CAP_AUDIT_WRITE和CAP_NET_RAW。
容器内允许的能力列表主要涉及控制多个进程;例如,CAP_SETUID和CAP_SETGID允许容器内的进程更改到不同的 UID。一个重要的例子是运行你的 Web 应用作为UID=60,但当容器进程启动时,它需要短暂地以 root 身份运行,然后将其 UID 更改为60。如果 Podman 放弃了CAP_SETUID,那么容器内的 root 进程不允许更改到 Web 服务的 UID。
Podman 允许的另一个有趣的能力是CAP_NET_BIND_SERVICE,它使进程能够绑定到小于1024的网络端口——例如端口80。回想第二章,你无法将主机上的端口80绑定到容器内的端口80。用户进程没有CAP_NET_BIND_SERVICE,因此它们无法绑定到端口80。表 10.3 列出了 Podman 在容器中允许的默认能力。此列表可以通过在容器表下的default_capabilities字段中修改容器.conf 文件来修改。
表 10.3 Podman 在容器中允许 root 进程的默认能力列表
| 选项 | 描述 |
|---|---|
CAP_CHOWN |
对文件 UID 和 GID 进行任意更改。 |
CAP_DAC_OVERRIDE |
绕过文件读取、写入和执行权限检查。 |
CAP_FOWNER |
在对文件系统 UID 的操作中绕过权限检查。 |
CAP_SETFSID |
修改文件时不要清除set-user-ID和set-group-ID模式位。 |
CAP_KILL |
绕过发送信号的权限检查。 |
CAP_NET_BIND_SERVICE |
将套接字绑定到互联网域的特权端口(端口号小于1024)。 |
CAP_SETFCAP |
在文件上设置任意能力。 |
CAP_SETGID |
更改进程的组 ID(GID)或附加 GID 列表。 |
SET_SETPCAP |
从调用线程的边界集中添加和删除任何能力。 |
CAP_SETUID |
对进程用户 ID(UID)进行任意操作。 |
CAP_SYS_CHROOT |
允许chroot,并更改挂载命名空间。 |
我通过询问什么阻止了 root 进程卸载或重新挂载只读文件系统来引入第 10.2 节。答案是 Podman 放弃了CAP_ SYS_ADMIN能力。
10.2.2 放弃 CAP_SYS_ADMIN
最强大的 Linux 能力是 CAP_SYS_ADMIN。我这样描述这个能力:想象你是一名内核工程师,正在内核中添加一个新特性,而这个特性需要特权访问。你查看能力列表,但没有找到与访问非常匹配的能力。内核工程师可以通过创建一个新的能力来避免麻烦;或者,可以说这是系统管理员需要做的事情,而有一个 CAP_SYS_ADMIN。我可能需要这个能力。如果你查看 man 能力信息,你会看到 CAP_SYS_ADMIN 能力阻止了多个页面的功能。
CAP_SYS_ADMIN 能力控制的一个功能是挂载和卸载文件系统的能力。因为这个能力默认被移除,Podman 容器中的 root 进程无法卸载或重新挂载只读挂载点。
如你之前所学,仍有 11 个能力被允许。在大多数情况下,你的容器化进程甚至不需要那些能力,这意味着你可以移除额外的能力。
10.2.3 移除能力
我建议人们尽可能以最低的权限运行他们的应用程序。提高系统安全性的一个方法就是移除额外的能力。
想象你的容器化进程不需要绑定到小于 1024 的端口。你可以使用 --cap-drop=CAP_NET_BIND_SERVICE 标志执行 Podman,并从你的容器中移除这个能力。
列表 10.4 当移除 CAP_NET_BIND_SERVICE 时的容器内能力
$ podman run --cap-drop CAP_NET_BIND_SERVICE ubi8 capsh --print
Current: = ❶
cap_chown,cap_dac_override,cap_fowner,cap_fsetid,cap_kill,cap_setgid,
➥ cap_setuid,cap_setpcap,cap_sys_chroot,cap_setfcap+eip
Bounding set = ❷
cap_chown,cap_dac_override,cap_fowner,cap_fsetid,cap_kill,cap_setgid,
➥ cap_setuid,cap_setpcap,cap_sys_chroot,cap_setfcap
...
❶ 注意当前能力列表不再包括 CAP_NET_BIND_SERVICE。
❷ 注意边界能力列表不再包括 CAP_NET_BIND_SERVICE。
你甚至可以使用 --cap-drop=all 标志移除所有能力:
$ podman run --cap-drop all ubi8 capsh --print
Current: =
Bounding set =
即使你的容器以 root 身份运行,它也没有修改内核的能力。有时你的容器因为 Podman 提供的有限能力列表而无法运行;在这种情况下,你可以添加所需的能力。
10.2.4 添加能力
在某些情况下,如果你的容器没有某个能力,它可能会失败。你可以简单地运行 --privileged 并关闭所有安全设置,但更好的解决方案是仅添加所需的能力。
想象你有一个容器,它想在它的命名空间网络中创建原始 IP 数据包,这需要 CAP_NET_RAW。默认情况下,Podman 不允许这样做。与其以 --privileged 运行容器,你可以使用 --cap-add CAP_NET_RAW 标志:
$ podman run --cap-add CAP_NET_RAW ubi8 capsh --print
Current: = cap_chown,cap_dac_override,cap_fowner,cap_fsetid,cap_kill,
➥ cap_setgid,cap_setuid,cap_setpcap,cap_net_bind_service,cap_net_raw,cap_
➥ sys_chroot,cap_setfcap+eip
Bounding set =cap_chown,cap_dac_override,cap_fowner,cap_fsetid,cap_kill,
➥ cap_setgid,cap_setuid,cap_setpcap,cap_net_bind_service,cap_net_raw,cap_
➥ sys_chroot,cap_setfcap
...
如果这是你的容器所需唯一的权限,你可以同时使用 --cap-drop 和 --cap-add 标志来移除所有能力并仅添加回 CAP_NET_RAW:
$ podman run --cap-drop=all --cap-add CAP_NET_RAW ubi8 capsh --print
Current: = cap_net_raw+eip
Bounding set =cap_net_raw
...
10.2.5 没有新权限
Podman 有一个选项--security-opt no-new-privileges,它禁用了容器进程获取额外权限的能力。基本上,它将进程锁定在它们启动时拥有的 Linux 能力组中。即使它们可以执行setuid程序,内核也会拒绝它们获取额外的能力。no-new-privileges选项还会影响 SELinux 并防止 SELinux 标签转换。即使 SELinux 在其规则数据库中存在错误,容器进程也不允许更改其标签。
10.2.6 没有能力的 root 仍然危险
降级能力意味着您的容器运行得更加安全,但运行没有任何 Linux 能力的所有容器会更加安全。当以 root 身份运行容器时,即使您降级了所有能力,也要考虑的一个问题是进程仍然以 root 身份运行。root 进程允许修改属于 root 的所有系统文件。root 进程可以修改系统文件,并诱使特权管理员执行它。此外,一些客户端-服务器应用程序仅当客户端以 root 身份运行时才信任连接的客户端(例如,Docker)。Podman 可以通过使用用户命名空间来解决这两个问题。
10.3 UID 隔离:用户命名空间
在 6.1.1 节中,我介绍了用户命名空间的概念。回想一下,UID 是通过/etc/subuid和/etc/subgid文件为无根用户分配的。对于我的账户,UID 的范围从100000到165535被分配,并使用我的 UID 3265,由 Podman 在启动容器时使用。参见图 10.2,了解用户命名空间映射的描述。

图 10.2 无根 Podman 为我账户使用的 UID 映射
这个用户命名空间允许我的账户在宿主机上不是 root 的容器内拥有 root 访问权限。在用户命名空间中运行容器消除了进程以 root 用户身份运行的问题,并且固有的信任被内置到某些守护程序中。
无根用户的一个问题是,默认情况下,所有容器都使用相同的用户命名空间。从用户命名空间的角度来看,理论上一个容器可以攻击另一个容器,因为它们运行时具有重复的 UID。此外,如果容器进程越界,它们可以读取/写入您的主目录内容,因为容器内的根进程是以您的 UID 运行的。
10.3.1 使用- -userns=auto标志隔离容器
Podman 为它启动的每个容器分配唯一的 UID 范围。由于为每个用户账户分配的 UID 有限,因此当由 root 用户启动时,此功能效果最佳。
要在各自的用户命名空间内启动多个容器,你首先需要为这些容器分配要使用的 UIDs 和 GIDs。在 Linux 系统上,有 40 亿个 UIDs 可用。Podman 建议你为容器分配最高的 20 亿个 UIDs。你可以通过将以下容器行添加到你的 /etc/subuid 和 /etc/subgid 文件中来实现这一点。
列表 10.5 /etc/subuid 和 /etc/subgid 文件的内容
# cat /etc/subuid
dwalsh:100000:65536
containers:2147483647:2147483648 ❶
# cat /etc/subgid
dwalsh:100000:65536
containers:2147483647:2147483648 ❶
❶ 为 Podman 使用的容器用户分配前 20 亿个 UIDs。添加此行会告诉系统上的其他工具,如 useradd,避免在此范围内分配 UIDs 和 GIDs。
你可以使用 --userns=auto 选项在独特的用户命名空间内启动容器。Podman 从 UID 2147483647 开始为容器分配 UIDs,这是你在 /etc/subuid 文件中指定的。然后 Podman 检查容器镜像中定义的所有 UIDs,以及如果镜像中存在 /etc/passwd 文件,还会检查该文件,然后使用这些信息来分配运行容器所需的 UIDs 数量,默认最小值为 1024:
# podman run --userns=auto ubi8 cat /proc/self/uid_map
0 2147483647 1024
如果我使用特定的用户 2000 运行第二个容器,那么 UIDs 的分配将反映这一点。你会看到分配的 UIDs 数量为 2001——UID 2000 加上根用户的 UIDs:
# podman run --user=2000 --userns=auto ubi8 cat /proc/self/uid_map
0 2147484671 2001
此外,请注意,第一个容器的起始 UID 为 2147483647,而第二个容器的起始 UID 为 2147484671。从第一个 UID 2147483647 减去第二个 UID 2147484671 得到 1024,这是第一个容器分配的 UIDs 数量。第一个容器内的任何 UID 都不会与第二个容器重叠,这意味着第一个容器内的任何进程都不能攻击第二个容器内的进程,反之亦然。
如果你发现 Podman 为你的容器分配的 UIDs 或 GIDs 数量不足,你可以使用大小选项覆盖容器内使用的用户命名空间的默认大小。在这个例子中,你告诉 Podman 使用 --userns=auto:size=5000 为容器分配 5000 个 UIDs:
# podman run --userns=auto:size=5000 ubi8 cat /proc/self/uid_map
0 2147486672 5000
当容器被移除时,Podman 会回收用于已删除容器的 UIDs,并将这些 UIDs 用于下一个使用 --userns=auto 标志创建的容器。当你使用 --rm 选项连续启动容器时,你会看到这一点。注意,它们以相同的 UID 开始。在以下示例中,两个容器都以 UID 2147491672 开始:
# podman run --rm --userns=auto ubi8 cat /proc/self/uid_map
0 2147491672 1024
# podman run --rm --userns=auto ubi8 cat /proc/self/uid_map
0 2147491672 1024
在 /etc/subuid 中使用的名称以及用于用户命名空间的 UIDs 的最小和最大数量在表 10.4 中描述的 storage.conf 文件中定义。
表 10.4 在 storage.conf 文件中用于覆盖用户命名空间自动设置的字段
| 选项 | 描述 |
|---|---|
root-auto-userns-user |
定义用于在/etc/subuid和/etc/subgid文件中查找一个或多个 UID/GID 范围的用户名。这些范围被分配给配置为自动创建用户命名空间的容器。配置为自动创建用户命名空间的容器还可以与设置了显式映射的容器重叠。root-auto-userns-user设置被无根用户忽略。默认为containers。 |
auto-userns-min-size |
定义自动创建的用户命名空间的最小大小。默认为1024。 |
auto-userns-max-size |
定义自动创建的用户命名空间的最大大小。默认为65536。 |
10.3.2 用户命名空间 Linux 功能
在第 10.2 节中,您学习了 Linux 功能以及它们是如何用来分割 root 的权力的。当一个容器在用户命名空间内启动时,它可以具有 Linux 功能。这些功能只能影响映射到用户命名空间中的 UID 和 GID。不涉及 UID 和 GID 的功能是有限的。通常,它们只会影响与用户命名空间一起映射的其他命名空间。
例如,CAP_NET_ADMIN是允许您操作网络堆栈的功能。它允许进程设置防火墙规则和网络路由表。具有命名空间CAP_NET_ADMIN的进程只能修改分配给用户命名空间的名字空间网络,而不能修改主机的网络命名空间。
在以下示例中,用户命名空间容器内的功能列表与您在没有用户命名空间的情况下启动容器时相同。在第二个命令中使用--userns=auto标志时,这些功能是命名空间功能:
# podman run --rm ubi8 capsh --print | grep Current
Current: = cap_chown,cap_dac_override,cap_fowner,cap_fsetid,cap_kill,
➥ cap_setgid,cap_setuid,cap_setpcap,cap_net_bind_service,cap_sys_chroot,
➥ cap_setfcap+eip
# podman run --rm --userns=auto ubi8 capsh --print | grep Current
Current: = cap_chown,cap_dac_override,cap_fowner,cap_fsetid,cap_kill,
➥ cap_setgid,cap_setuid,cap_setpcap,cap_net_bind_service,cap_sys_chroot,
➥ cap_setfcap+eip
为了证明这一点,尝试在容器内将文件的所有权chown给一个不存在的 UID。它失败了,因为CAP_CHOWN功能只允许容器内的 root 进程将文件的所有权chown给任何 UID,只要该 UID 映射到用户命名空间:
# podman run --rm --userns=auto:size=5000 ubi8 chown 6000 /etc/motd
chown: changing ownership of '/etc/motd': Invalid argument
如果您将chown到用户命名空间内映射的 UID,则操作成功:
# podman run --rm --userns=auto:size=5000 ubi8 chown 4000 /etc/motd
假设您使用--userns=auto标志启动了所有系统容器。在这种情况下,您将获得在独特的用户命名空间中运行容器的利益,该命名空间与其他所有容器和主机系统上的 UID 隔离。您还获得了有限的 root 权限,并且这些容器外的进程在主机系统上没有权限。
10.3.3 使用- -userns=auto标志的无根 Podman
--userns=auto与无根容器一起工作,基于用户可用的 UID 数量。但这个数量非常有限。您可以运行前面的示例并看到用户命名空间从 UID 1开始。UID 1相对于无根用户的用户命名空间:
$ podman run --userns=auto ubi8 cat /proc/self/uid_map
0 1 1024
$ podman run --userns=auto ubi8 cat /proc/self/uid_map
0 1025 1024
如果您检查您的用户命名空间,您会看到您的用户命名空间中的 UID 1是100000:
$ podman run --rm ubi8 cat /proc/self/uid_map
0 3267 1
1 100000 65536
这意味着第一个无 root 用户命名空间容器正在运行 UID 0,映射到无 root 用户命名空间的 UID 1。UID 1是主机系统上的无 root UID 100000。使用--userns=auto的 rootless 用户的几个问题是,由于默认用户只能获得 65,536 个 UID,最多可以启动 64 个容器,并且不能运行需要超过 65,536 个 UID 的任何容器。
注意:如果你不使用--userns=auto标志启动容器,映射到用户命名空间的 UID 可能会并且很可能与用户命名空间隔离容器中的 UID 重叠。你需要小心,确保这些容器内使用的 UID 中没有任何一个使用这些 UID,因为这些 UID 从 UID 的角度来看容易受到攻击。为了避免重叠,我建议使用高范围的 UID。
10.3.4 使用带有- -userns=auto标志的用户卷
当使用用户命名空间时,很难确定哪个用户的 UID 需要拥有你挂载到容器中的卷以允许访问。在下面的示例中,你首先创建一个目录,然后将卷挂载到容器中,并尝试在其中创建一个文件。
列表 10.6 使用用户命名空间内卷的缺点
# mkdir /mnt/test
# ls -ld /mnt/test
drwxr-xr-x. 2 root root 6 Feb 8 16:23 /mnt/test ❶
# podman run --rm -v /mnt/test:/mnt/test --userns=auto ubi8 ls -ld /mnt/test
drwxr-xr-x. 2 nobody nobody 6 Feb 8 21:23 /mnt/test ❷
# podman run --rm -v /mnt/test:/mnt/test:Z --userns=auto ubi8 touch /mnt/test
touch: setting times of '/mnt/test':
➥ Permission denied ❸
❶ 该目录由主机上的 root 所有。
❷ 目录列出的用户为 nobody,因为 root UID=0 没有映射到用户命名空间。所有未映射到容器的 UID 的所有文件和目录都被视为 nobody 用户。:Z告诉 Podman 重新标记 SELinux。
❸ 即使 root 也不允许写入未映射用户的目录,除非目录是可写给所有人的。
Podman 在--volume标志上支持一个特殊选项U``,,它告诉 Podman 将源目录中的所有文件或目录的所有权chown为与容器主进程的 UID 匹配:
# ls -ld /mnt/test
drwxr-xr-x. 2 root root 6 Feb 8 16:38 /mnt/test
# podman run --rm -v /mnt/test:/mnt/test:Z,U
➥ --userns=auto ubi8 touch /mnt/test/test1 ❶
# ls -ld /mnt/test
drwxr-xr-x. 2 2147503960 2147503960
➥ 19 Feb 8 16:38 /mnt/test ❷
❶ 添加 U 选项后,容器内的进程可以写入卷。
❷ Podman 将源卷的所有权更改为 2147503960,以匹配容器中的 root 用户映射。
Linux 内核的一个新、高级特性被称为idmapped mounts。它允许用户将源卷内的 UID 重新映射以匹配用户命名空间,而无需实际上在磁盘上chown文件。在下一个示例中,你将重新创建/mnt/test 目录,这次使用idmap选项挂载它。当 ID 映射卷出现在容器内时,文件看起来是由用户命名空间的 root 拥有的,并且你可以根据标准权限读取和写入文件。当你完成文件编写后,它们会被正确映射回用户命名空间,与U选项不同,它根据容器进程的真实 UID 将它们写回:
# chown -R root:root /mnt/test ❶
# podman run --rm -v /mnt/test:/mnt/test:idmap,Z
➥ --userns=auto ubi8 ls -ld /mnt/test ❷
drwxr-xr-x. 2 root root 31 Feb 9 11:56 /mnt/test
# podman run --rm -v /mnt/test:/mnt/test:idmap,Z
➥ --userns=auto ubi8 touch /mnt/test/test ❸
# ls -l /mnt/test ❹
total 0
-rw-r--r--. 1 root root 0 Feb 9 06:57 test
-rw-r--r--. 1 root root 0 Feb 8 17:02 test1
❶ 将源卷的所有权重置为 root。
❷ 使用 idmap 选项将源卷/mnt/test 挂载到容器中。注意路径在容器内属于 root。
❸ 在源目录内创建一个文件,以证明容器可以写入该目录。
❹ 注意在主机系统上,新创建的文件属于真实根用户。
注意 idmap 功能是写作时新出现的,并且不是所有文件系统都支持。目前,它只支持在特权模式下运行,但希望这种情况很快会改变。目前,支持此功能的 OCI 运行时是 crun。
理解运行带有用户命名空间的容器所带来的安全优势非常重要。接下来,我将向你展示其他命名空间的一些安全优势。
10.4 进程隔离:PID 命名空间
我经常说命名空间并非旨在作为安全机制,但事实上,它们通过隔离和信息隐藏提供了额外的安全。PID 命名空间隐藏了系统上运行着其他进程的事实。意识到某个应用程序正在系统上运行对于攻击容器的人来说可能是有价值的。当你在一个容器内运行其自己的 PID 命名空间时,它只能看到容器内运行的其它进程。默认情况下,Podman 在其自己的 PID 命名空间内运行容器。
一些作为容器镜像分发的应用程序需要额外的系统访问权限。如果你有一个需要监控主机进程的应用程序,你需要关闭 PID 命名空间以暴露系统上的所有进程。使用 Podman 关闭 PID 命名空间很简单:只需添加 --pid=host 标志。在接下来的几个示例中,你会看到,有了 PID 命名空间,你只能看到容器内的容器进程。第二个命令将系统内的所有进程暴露给容器。
列表 10.7 使用 pid 命名空间和不使用它的区别
$ podman run --rm ubi8 find /proc -maxdepth 1
➥ -type d -regex ".*/[0-9]*" ❶
/proc/1
$ podman run --rm --pid=host ubi8 find
➥ /proc -maxdepth 1 -type d -regex ".*/[0-9]*" ❷
/proc/1
/proc/2
/proc/3
/proc/4
...
❶ 运行查找容器内所有进程的 find 命令,你只能看到一个进程。
❷ 在 --pid=host 容器中运行 find 命令,你将看到系统上的所有进程。
注意 在 SELinux 系统上,通过 --pid=host 选项暴露主机的进程也会产生禁用 SELinux 分隔的副作用。SELinux 会阻止对主机进程的访问,当容器内的进程与这些进程交互时会引起问题。其他安全机制,如丢弃的能力和用户命名空间,不会被丢弃并且可以阻止对进程的访问。
10.5 网络隔离:网络命名空间
网络命名空间为主机网络设置隔离。它允许 Podman 设置虚拟专用网络,以控制哪些容器可以与其他容器通信。Podman 有能力创建多个网络,并将这些网络中的容器分配到这些网络中。默认情况下,所有容器都在主机网络中运行。但使用 podman network create 命令设置额外的网络很简单。在下一个示例中,你将创建两个网络——net1 和 net2:
$ podman network create net1
net1
$ podman network create net2
net2
当你创建新的容器时,你可以使用 --network net1 选项将它们分配到特定的网络:
$ podman run -d --network net1 --name
➥ cnet1 ubi8 sleep 1000 ❶
74ce5b2396f77fce8c499b121aeb8731f1e1b22e363a6a72d243487cf93a5897
$ podman run --network net1 alpine
➥ ping -c 1 cnet1 ❷
PING cnet1 (10.89.0.4): 56 data bytes
64 bytes from 10.89.0.4: seq=0 ttl=42 time=0.077 ms
❶ 在网络 net1 中启动一个后台容器。
❷ 确保容器可以从网络中的另一个容器访问。
如果您尝试通过容器名称或甚至 IP 地址从默认网络命名空间 ping 网络,它将失败:
$ podman run --rm alpine ping -c 1 cnet1
ping: bad address 'cnet1'
$ podman run alpine ping -c 1 10.89.0.4 ❶
PING 10.89.0.4 (10.89.0.4): 56 data bytes
64 bytes from 10.89.0.4: seq=0 ttl=42 time=0.073 ms
❶ 确保 cnet1 容器仍然可以通过 IP 地址访问。
类似地,如果您尝试从不同的网络 --network net2 ping 它,它也会失败:
$ podman run --rm --network net2 alpine ping -c 1 cnet1
ping: bad address 'cnet1'
为您的容器创建私有网络允许您使用网络命名空间将它们彼此隔离,即使在网络上也是如此。
注意:对于这些示例,我使用了 alpine 镜像,因为它自带安装了 ping 包,而 ubi8 镜像则没有包含它。您可以通过 Containerfile 和 podman build 容器轻松添加 ping 可执行文件。
您可以使用 --net=host 选项将主机网络暴露给容器,允许容器绑定到主机上的端口。在某些情况下,当您消除网络命名空间时,您可以获得更好的性能。
10.6 IPC 隔离:IPC 命名空间
进程间通信(IPC)命名空间隔离了某些 IPC 资源,即 System V IPC 对象和 POSIX 消息队列。它还隔离了主机和其他容器中的 /dev/shm tmpfs。IPC 命名空间允许容器创建与同一系统上其他容器具有相同名称的命名 IPC,而不会引起冲突。
因此,IPC 隔离防止一个容器通过 IPC 或 /dev/shm 攻击另一个容器。您可以使用 --ipc=container:NAME 将两个 IPC-命名空间容器连接在一起,或者在一个 pod 中运行它们。它们共享相同的 IPC 命名空间。它们可以一起使用 IPC,但仍然与主机隔离。
列表 10.8 IPC 命名空间保持 /dev/shm 对每个容器私有
$ podman run -d --rm --name ipc1 ubi8 bash
➥ -c "touch /dev/shm/ipc1; sleep 1000" ❶
93df44264dd4b87d24f59dfffb92a6a0b6359bc5bcf94213d5e38499a10d3f3e
$ podman run --rm ubi8 ls /dev/shm ❷
$ podman run --rm --ipc=container:ipc1 ubi8 ls /dev/shm ❸
ipc1
❶ 创建一个名为 ipc1 的容器,触摸 /dev/shm/ipc1,然后进入睡眠状态。
❷ 运行第二个容器以查看 /dev/shm/ipc 是否不存在,因为容器正在运行在独立的 IPC 命名空间中。
❸ 运行一个具有共享 IPC 命名空间的容器,您将看到 /dev/shm 是共享的,并且 IPC 文件存在。
您可以通过执行 --ipc=host 选项将主机的 IPC 命名空间与您的容器共享。
注意:在 SELinux 系统上,Podman 将所有共享相同 IPC 命名空间的容器修改为共享相同的 SELinux 标签。否则,当标签不匹配时,SELinux 会阻止容器之间的 IPC 通信。使用 --ipc=host 选项会导致 SELinux 分隔被禁用;否则,SELinux 会阻止对主机 IPC 的访问。
10.7 文件系统隔离:挂载命名空间
接下来,也许是最重要的,是命名空间隔离,即挂载命名空间。挂载命名空间隐藏了整个主机文件系统,使其对容器进程不可见。容器进程只能看到挂载命名空间中定义的文件系统内容。Podman 创建文件系统挂载点rootfs并将所有卷绑定到它上。然后 Podman 执行 OCI 运行时,该运行时再执行pivot_root系统调用,这反过来又改变了调用进程的挂载命名空间中的根挂载。它将根挂载移动到rootfs目录。因此,主机操作系统的所有内容都消失了,容器进程只能看到提供的内容。通过丢弃CAP_SYS_ADMIN能力,容器内的进程无法影响根 fs 的挂载,从而暴露底层文件系统。
注意:阅读pivot_root(2)手册页以了解更多关于pivot_root系统调用的信息:man 2 pivot_root。
虽然挂载命名空间和缺少CAP_SYS_ADMIN提供了优秀的隔离,但已经有一些容器逃逸到底层文件系统,这正是 SELinux 介入的地方。一个例子是 OCI 运行时runc(CVE-2019-5736)中的漏洞,该漏洞允许容器进程在 rootful 容器中覆盖runc可执行文件。这个漏洞允许容器逃出它们的容器并接管用户的系统。这个漏洞影响了所有容器引擎,包括 Podman、Docker、CRI-O 和 containerd。好消息是,配置良好的 SELinux 可以阻止它。Podman 主要在无根模式下运行,无根 Podman 通过两种方式得到保护:SELinux 和不以 root 身份运行。我在这篇“最新的容器漏洞(runc)可以通过 SELinux 阻止”博客文章中讨论了这个漏洞,该文章可在 Red Hat 网站上找到(mng.bz/Qn6j)。
10.8 文件系统隔离:SELinux
SELinux 是一个标签系统,其中每个进程和文件系统对象都会被标记。然后,将规则写入内核,关于进程标签如何与文件系统标签以及其他进程标签交互。SELinux 支持多种不同的安全机制;容器利用了其中两种。第一种被称为类型强制,通过它 SELinux 根据进程的类型控制进程可以做什么。第二种被称为MCS 强制,它还使用分配给进程的类别。
SELinux 并非在所有发行版上都得到支持。Fedora、RHEL 和其他 Red Hat 发行版支持 SELinux,而基于 Debian 的发行版,如 Ubuntu,通常不支持。如果你的 Linux 发行版不支持 SELinux,你可能想跳过这一节。
10.8.1 SELinux 类型强制
SELinux 标签有四个组成部分:SELinux 用户、角色、类型和 MCS 级别(见表 10.5)。
表 10.5 SELinux 标签类型示例
| 对象 | 用户 | 角色 | 类型 | MCS 级别 |
|---|---|---|---|---|
| 容器进程 | system_u |
system_r |
container_t |
s0:c1,c2 |
| 容器进程 | system_u |
system_r |
container_t |
s0:c361,c871 |
| 容器文件 | system_u |
object_r |
container_file_t |
s0:c1,c2 |
| 容器文件 | system_u |
object_r |
container_file_t |
s0:s361,c871 |
| /etc/shadow 标签 | system_u |
object_r |
shadow_t |
s0 |
| 容器进程 | system_u |
system_r |
spc_t |
s0 |
| 用户进程 | unconfined_u |
unconfined_r |
unconfined_t |
s0-s0:c0.c1023 |
在本节中,你将专注于 SELinux 类型。我写了SELinux 彩色画册来解释标签,使用了猫和狗的类比(图 10.3)。

图 10.3 SELinux 彩色画册 (mng.bz/Xay6)
如彩色画册所述,想象你有一组被标记为cat类型的进程和另一组被标记为dog类型的进程。想象你还有在文件系统中被标记为dog food类型和cat food类型的对象。最后,想象你向内核写入规则,说明cat类型可以吃cat food类型,而dog类型可以吃dog food类型。使用 SELinux,任何未明确允许的行为都会被拒绝。cat进程可以吃cat food,而dog进程可以吃dog food,但如果一个dog类型尝试吃cat food,Linux 内核会介入并阻止访问。
容器的工作方式相同。Podman 将每个容器进程标记为container_t类型。容器内的所有文件都被标记为container_file_t类型。规则被写入内核,说明container_t进程可以读取、写入和执行标记为container_file_t类型的文件。
注意 SELinux 不关心所有权和权限,因此你可以定义一个进程类型,它有权访问所有文件系统类型,并且不受 SELinux 的限制,通常称为未限制类型。你可以在 Linux 系统上看到一些未限制类型的运行。id -Z命令显示你的用户进程以unconfined_t类型运行,而特权容器以spc_t类型运行。
当 Podman 为容器构建 rootfs 时,它会将 rootfs 中的所有文件标记为container_file_t。这意味着容器进程可以读取、写入和执行容器 rootfs 中的所有文件,但如果它们逃逸到宿主文件系统,SELinux 内核会阻止对宿主文件系统对象的访问。在接下来的几个示例中,你可以检查 SELinux 容器中发生的情况。在这个第一个示例中,你可以看到容器化进程的标签;注意类型是container_t。但是当你使用--privileged标志运行时,Podman 将标签更改为spc_t,一个未限制域:
$ podman run --rm ubi8 cat /proc/self/attr/current
system_u:system_r:container_t:s0:c694,c944
$ podman run --rm --privileged ubi8 cat /proc/self/attr/current
unconfined_u:system_r:spc_t:s0
使用ls -Z命令检查容器内的文件。你看到所有文件都被标记为container_file_t:
$ podman run --rm ubi8 ls -Z /
system_u:object_r:container_file_t:s0:c88,c191 bin
system_u:object_r:container_file_t:s0:c88,c191 boot
system_u:object_r:container_file_t:s0:c88,c191 dev
system_u:object_r:container_file_t:s0:c88,c191 etc
system_u:object_r:container_file_t:s0:c88,c191 home
system_u:object_r:container_file_t:s0:c88,c191 lib
...
由于 Podman 正确配置了 SELinux 环境,容器进程可以完全访问容器 rootfs 中的所有对象,SELinux 基本上不会干涉,除非其他事情出了问题,并且容器进程从 rootfs 逃逸到宿主操作系统。在这种情况下,SELinux 开始阻止访问。想象一下,您在系统上运行的容器进程从容器中逃逸出来,试图读取您家目录中的 SSH 密钥。让我们看看这些文件的标签。您会看到这些文件被标记为ssh_home_t类型:
$ ls -1Z $HOME/.ssh/
unconfined_u:object_r:ssh_home_t:s0 authorized_keys
unconfined_u:object_r:ssh_home_t:s0 authorized_keys2
unconfined_u:object_r:ssh_home_t:s0 config
...
由于 SELinux 策略中没有允许container_t进程读取ssh_home_t文件的规则,SELinux 内核阻止了访问。您可以通过将.ssh 目录挂载到容器中来演示这一点。当您尝试列出目录时,容器进程会收到Permission denied:
$ podman run -v $HOME/.ssh:/.ssh ubi8 ls /.ssh
ls: cannot open directory '/.ssh': Permission denied
正如您在 3.1.2 节中学到的,Podman 有 SELinux 卷选项z和Z,这些选项告诉 SELinux 重新标记源卷的内容,使其在容器内可用。对于.ssh 目录来说,这样做并不是一个好主意。
相反,让我们创建一个临时文件并展示 SELinux 标签的实际应用。首先,在您的家目录中创建一个名为 foo 的临时文件。将其标记为user_home_t。将其挂载到容器中,并查看容器进程被拒绝访问。
列表 10.9 Podman 容器内卷的 SELinux 工作方式
$ mkdir foo
$ ls -Zd foo ❶
unconfined_u:object_r:user_home_t:s0 foo
$ podman run -v ./foo:/foo ubi8 touch /foo/bar ❷
touch: cannot touch '/foo/bar': Permission denied
$ podman run --privileged -v ./foo:/foo ubi8 touch
➥ /foo/bar ❸
$ ls -Z foo ❹
unconfined_u:object_r:user_home_t:s0 bar
$ rm foo/bar
$ podman run -v ./foo:/foo:Z ubi8 touch /foo/bar ❺
$ ls -Z ./foo ❻
system_u:object_r:container_file_t:s0:c454,c510 bar
❶ 在您的家目录中创建的文件默认为 user_home_t 类型。
❷ 默认情况下,容器进程不允许写入用户主目录中的内容。Podman 默认不会更改卷的标签。
❸ --privileged标志会导致 SELinux 隔离被禁用,以未限制的类型(spc_t)运行容器。该命令模拟容器逃逸,显示在没有 SELinux 的情况下,逃逸的容器被允许写入文件系统。
❹ 由特权容器创建的文件具有用户主目录的标签(user_home_t)。
❺ 卷挂载上的:Z选项告诉 Podman 将目录内容重新标记以匹配 rootfs(container_file_t)内文件的标签。
❻ 新创建的文件标签与容器内的标签相匹配。
SELinux 类型强制执行已经证明在阻止容器逃逸方面非常有价值,当没有其他机制可用时。表 10.6 显示了由 SELinux 阻止的容器逃逸列表。
SELinux 类型强制执行在保护宿主操作系统免受容器进程侵害方面做得很好。问题是类型强制执行并不能保护您免受一个容器攻击另一个容器。
表 10.6 SELinux 阻止的主要容器漏洞
| 常见漏洞和暴露 | 描述 |
|---|---|
| CVE-2019-5736 | 恶意容器的执行允许容器逃逸并访问宿主文件系统。 |
| CVE-2015-3627 | 不安全地打开文件描述符 1,导致权限提升 |
| CVE-2015-3630 | 读写 proc 路径允许主机修改和信息泄露。 |
| CVE-2015-3631 | 卷挂载允许 Linux 安全模块(LSM)配置文件提升。 |
| CVE-2016-9962 | runc 执行漏洞 |
10.8.2 SELinux 多类别安全分隔
SELinux 不会阻止同一类型的进程攻击同一类型的其他进程。一种思考方式是回到猫和狗的类比。类型强制防止 狗 吃 猫 的 食物,但它不会阻止 猫-A 吃 猫-B 的 猫 食物。
回想当我介绍这个部分时,我说过 Podman 利用 SELinux 安全性有两种类型。SELinux 有一种机制可以根据多类别安全(MCS)级别字段强制执行进程分隔。SELinux 定义了 1,024 个类别,可以组合在一起为每个容器提供一个级别。Podman 为每个容器分配两个类别,然后确保进程标签级别与文件系统标签级别相匹配。然后 SELinux 内核强制执行 MCS 级别匹配,否则拒绝访问。
注意,MCS 分隔实际上关于支配性。每个类别必须支配 MCS 级别。S0:C1,C2 级别可以写入 S0:C1,C2、S0:C1、S0:C2 和 S0 级别,但 S0:C1,C2 级别不允许写入 S0:C1,C3,因为原始标签不包括 C3。在实践中,Podman 只使用两个类别或没有类别。当你在一个卷上使用 :z 选项时,Podman 会将源目录重新标记为 s0 级别——没有类别。s0 级别允许来自任何容器的进程以该级别读取和写入文件系统对象,从 SELinux 的角度来看。
重新查看表 10.4,但这次专注于 MCS 级别字段(表 10.7)。
表 10.7 容器进程标签,MCS 级别已突出显示
| 对象 | 用户 | 角色 | 类型 | MCS 级别 |
|---|---|---|---|---|
| 容器进程 | system_u |
system_r |
container_t |
s0:c1,c2 |
| 容器进程 | system_u |
system_r |
container_t |
s0:c361,c871 |
| 容器文件 | system_u |
object_r |
container_file_t |
s0:c1,c2 |
| 容器文件 | system_u |
object_r |
container_file_t |
s0:s361,c871 |
| /etc/shadow 标签 | system_u |
object_r |
shadow_t |
s0 |
| 容器进程 | system_u |
system_r |
spc_t |
s0 |
| 用户进程 | unconfined_u |
unconfined_r |
unconfined_t |
s0-s0:c0.c1023 |
现在看看 MCS 级别如何与 Podman 一起工作。如果你连续运行容器并检查 SELinux 标签,你会注意到每个容器的 MCS 级别是唯一的:
$ podman run --rm ubi8 cat /proc/self/attr/current
System_u:system_r:container_t:s0:c648,c1009
$ podman run --rm ubi8 cat /proc/self/attr/current
system_u:system_r:container_t:s0:c393,c834
这个 MCS 级别阻止进程相互攻击。回想一下在 10.2.8 节中,你使用容器私有标签创建了 foo/bar 文件。如果你将此文件挂载到另一个容器中,然后尝试写入文件,你会得到权限被拒绝。
列表 10.10 SELinux 阻止不同容器共享卷
$ ls -Z ./foo ❶
system_u:object_r:container_file_t:s0:c454,c510 bar
$ podman run -v ./foo:/foo ubi8 touch /foo/bar ❷
touch: cannot touch '/foo/bar': Permission denied
$ podman run --security-opt label=level:s0:c454,c510
➥ -v ./foo:/foo ubi8 touch /foo/bar ❸
❶ 文件 foo/bar 有一个私有的 MCS 级别,Podman 不会将其提供给另一个容器。
❷ 其他容器不允许根据不同的 MCS 级别访问 foo/bar 文件。
❸ 如果您强制容器 MCS 级别与上一个容器的标签匹配,SELinux 允许访问。
回想一下,Z 卷选项告诉 Podman 将容器标记为对容器私有,而 z 卷选项告诉 Podman 将容器标记为对所有容器共享。如果您有一个希望允许多个容器使用的目录,可以使用此选项。
列表 10.11 卷选项 z 导致 Podman 重新标记卷为共享标签
$ podman run -v ./foo:/foo:z ubi8 touch /foo/bar ❶
$ ls -Z foo/ ❷
system_u:object_r:container_file_t:s0 bar
$ podman run --rm -v ./foo:/foo ubi8 touch /foo/bar ❸
❶ -v ./foo:/foo:z 告诉 Podman 将卷标记为共享。
❷ Podman 使用 :s0 MCS 级别,因为所有容器都被允许写入。
❸ 其他具有不同 MCS 级别的容器可以成功修改内容。
注意 SELinux 有 1,024 个类别,Podman 为每个容器选择两个类别。级别 s0:c1,c1 是不允许的。这些类别不得匹配,顺序也不重要。级别 s0:c1,c2 与 s0:c2,c1 相同。有 1024 x 1024 ÷ 2 – 1024 = ~500,000 种独特的组合,这意味着您可以在系统上创建五十万个独特的容器。
有时有必要禁用 SELinux 容器隔离以适应您的容器。例如,您可能希望在容器内共享您的家目录。使用 Z 或 z 选项重新标记您的家目录是一个糟糕的想法。回想一下,当重新标记卷时,它们需要对容器是私有的。重新标记家目录可能会引起其他受限域的 SELinux 问题。您可以使用带有 --privileged 标志的容器运行,但您可能希望其他安全机制仍然得到执行。为了实现这一点,您可以使用 --security-opt label:disable 标志:
$ podman run --rm --security-opt label=disable ubi8 cat
➥ /proc/self/attr/current
unconfined_u:system_r:spc_t:s0
$ podman run --rm -v $HOME/.ssh:/ssh --security-opt label=disable ubi8 ls /ssh
authorized_keys
authorized_keys2
config
fedora_rsa
fedora_rsa.pub
...
注意 udica 项目(github.com/containers/udica)的目标是为容器生成 SELinux 策略。基本上,Udica 通过 podman inspect 检查您创建的容器,然后编写一个策略类型,允许访问您想要挂载到容器中的卷。
SELinux 是一种非常强大的工具,可以保护宿主操作系统免受容器的影响。只要您了解如何处理卷,它就很容易处理容器。了解如何保护文件系统后,现在应该看看如何保护 Linux 内核免受可能存在漏洞的系统调用的影响。
10.9. 系统调用隔离 seccomp
系统调用,通常称为 syscall,是计算机程序请求其执行的操作系统内核上的服务的方式。常见的 syscalls 包括 open、read、write、fork 和 exec。在 Linux 中,有超过 700 个系统调用。
回想一下本章开头的内容,Linux 内核是敌意容器可以攻击以逃离限制的单一点。如果 Linux 内核中存在可以通过系统调用攻击的漏洞,容器进程可能会逃逸。Linux 内核的 seccomp 功能允许进程自愿限制它们及其子进程可以调用的系统调用数量。默认情况下,Podman 通过此功能消除了数百个系统调用。假设 Linux 内核在其系统调用中的一个存在缺陷,容器进程可以使用它来逃逸,但 Podman 已将其从容器可用的系统调用表中删除。在这种情况下,容器将无法使用它。
Podman 的 seccomp 过滤器存储在 /usr/share/containers/seccomp.json 文件中,以 JSON 格式保存。Podman 还会根据您允许容器拥有的能力来修改 seccomp 过滤器的列表。当您添加一个能力时,Podman 会添加实现该能力所需的系统调用。能力和 seccomp 都会分别强制执行;Podman 只是试图让用户更容易操作。如果用户提供了自己的 seccomp JSON 文件,它需要与默认文件相似,以便能力修改才能生效。
您可以通过编辑此文件来修改 seccomp 过滤器。在以下示例中,您从 seccomp.json 中删除了 mkdir 系统调用,然后在一个尝试创建目录的容器中运行。seccomp 过滤器阻止了系统调用,您的容器失败。
列表 10.12 如何使用 seccomp 过滤器在 Podman 容器内阻止系统调用
$ sed '/mkdir/d' /usr/share/containers
➥ /seccomp.json > /tmp/seccomp.json ❶
$ diff /usr/share/containers/seccomp.json/
➥ tmp/seccomp.json ❷
249,250d248
< "mkdir",
< "mkdirat",
$ podman run --rm --security-opt seccomp=/
➥ tmp/seccomp.json ubi8 mkdir /foo ❸
mkdir: cannot create directory '/foo': Function not implemented
$ podman run --rm ubi8 mkdir /foo ❹
❶ 使用 sed 命令删除所有创建 mkdir 和 /tmp/seccomp.json 的条目。
❷ 使用 diff 命令显示已删除的 mkdir 条目。
❸ 使用 --security-opt seccomp=/tmp/seccomp.json 标志使用替代的 seccomp 过滤器;mkdir 命令失败,因为 mkdir 系统调用不可用。
❹ 再次运行相同的命令,使用默认过滤器以显示 mkdir 成功。
注意:由于难以确定容器所需的系统调用数量,因此修改 seccomp 过滤器的人不多。有工具可以使用伯克利包过滤器(BPF)生成系统调用列表。以下网页上的软件包是一个钩子,它监控容器并自动生成 seccomp.json 文件,稍后可用于锁定容器:github.com/containers/oci-seccomp-bpf-hook/。
有时默认的容器 seccomp.json 文件过于严格。您的容器可能无法工作,因为它需要不可用的系统调用。在这种情况下,您可以使用 --security-opt seccomp=unconfined 标志来禁用 seccomp 过滤。
如您所见,系统调用过滤功能强大,确实可以限制容器进程对宿主内核的访问。下一级是使用 KVM 隔离。
10.10 虚拟机隔离
在本章的开头,我比较了基于三只小猪选择居住地点的过程隔离。它们可以选择住在单独的房子里、一栋联排别墅,或者公寓。每一个选择都稍微降低了安全性。默认情况下,容器安全就是住在公寓里。但你可以使用虚拟机隔离,这基本上是将你的容器放入虚拟机中,以获得更好的隔离。
在附录 B 中,我介绍了不同的 OCI 运行时,如 Kata 和 libkrun,如何利用基于内核的虚拟机(KVM)在轻量级虚拟机中运行它们的容器。这些虚拟机运行它们自己的内核和初始化工具来启动容器。通过这种方式,几乎消除了宿主机的所有系统调用,使得逃逸隔离变得更加困难。
这种隔离的问题在于它是有代价的。就像联排别墅一样,你最终会在容器之间共享更少的服务。内存管理、CPU 和其他资源更难共享。将卷共享到容器中也会表现得更差。
现在你已经完成了对用于容器隔离的 Podman 安全特性的检查。接下来,让我们看看其他的安全特性。
摘要
-
容器安全全部关乎保护 Linux 内核和宿主机文件系统免受敌对容器进程的侵害。
-
多层次防御意味着你的容器工具利用尽可能多的安全机制。如果一个安全机制失败,其他机制可能仍然可以保护你的系统。
11 额外的安全考虑
本章涵盖
-
在不同的独立服务器、不同的虚拟机(VM)和容器内安全运行应用程序
-
通过服务运行容器与通过 fork 和 exec 作为容器引擎子进程运行容器的比较
-
用于将容器彼此隔离的 Linux 安全功能
-
设置容器镜像信任
-
签署镜像并信任它们
在本章中,我回顾并演示了使用 Podman 运行容器时的一些额外的安全考虑。其中一些内容在其他章节中已经介绍过,但我认为从安全角度集中关注这些功能是有用的。
我看到人们在运行容器时最常见的一个问题是,当容器进程被拒绝某些访问时,用户的第一个反应是运行容器在--privileged模式下,这将关闭您容器的所有安全隔离。了解如何处理本章中讨论的安全功能可以帮助您避免这种情况。
11.1 守护进程与 fork/exec 模型
在前面的章节中,您已经学到了很多关于像 Docker 这样的守护进程与 Podman 使用的 fork/exec 模型之间的问题。
11.1.1 对 docker.sock 的访问
回想一下,Docker 默认运行一个由 root 用户拥有的守护进程。这意味着任何可以访问守护进程的用户都可以在系统上以完全 root 权限启动进程。Docker 建议一些用户将他们的账户添加到/etc/group中的 docker 组。在某些发行版中,这允许您无需 root 权限即可访问/run/docker.sock:
# ls -l /run/docker.sock
srw-rw----. 1 root docker 0 Jun 13 14:54 /run/docker.sock
您可以像运行 Podman 容器一样运行 Docker 容器:
$ docker run registry.access.redhat.com/ubi8-micro echo hi
Unable to find image 'registry.access.redhat.com/ubi8-micro:latest' locally|
latest: Pulling from ubi8-micro
4f4fb700ef54: Pull complete
b6d5e0581b2f: Pull complete
Digest: sha256:a519ab06c0287085c352af0d2b84f2a2b257d2afb2e554b8d38a076cd6205b48
Status: Downloaded newer image for registry.access.redhat.com/
ubi8-micro:latest
hi
这让许多用户感到兴奋,直到他们意识到他们也可以通过简单的 Docker 命令在他们的系统上启动 root shell:
$ docker run -ti --name hack -v /:/host --privileged
➥ registry.access.redhat.com/ubi8-micro chroot /host
# cat /etc/shadow
...
到目前为止,您在主机系统上拥有一个完全特权的 root shell,在其中您可以随意破解机器。不仅如此,Docker 默认将所有日志记录为基于文件的。当您完成破解系统后,您可以删除日志文件和您所有活动的记录:
$ docker rm hack
hack
使用无根 Podman,您无法这样做,因为当您运行容器时,容器进程是以您的用户 UID 运行的,并且只能访问与您账户中任何进程相同的文件。管理员确定他们是否被黑客攻击的一种方法是通过检查日志系统,包括审计日志。
11.1.2 审计和日志记录
Linux 系统的一个关键特性是跟踪当进程在系统上运行时它们做了什么。当您登录到 Linux 系统时,内核会将您的 UID 记录到/proc/self/loginuid 中的进程数据中。您可以通过执行以下命令查看这些数据:
$ cat /proc/self/loginuid
3267
此第一个进程创建的所有进程在登录后都保持此字段。即使您使用setuid程序,如su或sudo,您的loginuid仍然保持不变:
$ sudo cat /proc/self/loginuid
3267
即使您启动了一个容器,loginuid 仍然保持不变。在下一个例子中,您以守护进程模式运行一个简单的容器,然后使用 podman inspect 获取睡眠进程的 PID,最后检查容器化进程的 loginuid:
$ podman run -d ubi8-micro sleep 20
1c55b9cfa0cd20c36da4b606415e190a6c20cc868d3486981c7713d41ee9ea6a
$ podman inspect -l --format '{{ .State.Pid }}'
119394
$ cat /proc/119394/loginuid
3267
注意,容器化进程仍在使用您的 loginuid 运行。这表明只要容器引擎使用 fork/exec 模型,内核就可以通过此字段跟踪哪个用户在系统上启动了容器进程。如果您使用 Docker 运行相同的测试,您会得到非常不同的结果:
$ docker run -d registry.access.redhat.com/ubi8-micro sleep 20
df2302cf8c6385df2b86ccd3429166e0d8dd0c9f0d0139e98e6354809a04080e
$ docker inspect df2302cf8c6 --format '{{ .State.Pid }}'
120022
$ cat /proc/120022/loginuid
4294967295
您看到的不是您的 loginuid,而是 4294967295,这是 2³² – 1。这是 Linux 内核表示 -1 的方式,这是系统启动的所有进程的默认 loginuid,而不是登录系统的用户启动的进程。原因是 Docker 使用客户端-服务器模型,容器进程是 Docker 守护进程的子进程,而不是 Docker 客户端。由于 Docker 守护进程是在系统启动时由 systemd 启动的,因此所有子进程都具有 -1 的 loginuid。
内核的审计子系统在完成可审计事件时记录系统上每个进程的 loginuid。例如,当用户登录和注销系统时,这些事件会被记录下来。修改 /etc/passwd 和 /etc/shadow 也是可记录的事件。
以下是我今天登录系统时的 USER_START 审计日志条目。我的 UID 3267 被记录,以及我的用户名:
# ausearch -m USER_START
type=USER_START msg=audit(1651064687.963:315): pid=2579 uid=0 auid=3267
➥ ses=3 subj=system_u:system_r:xdm_t:s0-s0:c0.c1023 msg='op=PAM:session_open
➥ grantors=pam_selinux,pam_loginuid,pam_selinux,pam_keyinit,pam_namespace,
➥ pam_keyinit,pam_limits,pam_systemd,pam_unix,pam_gnome_keyring,pam_umask acct=
➥ "dwalsh" exe="/usr/libexec/gdm-session-worker" hostname=fedora addr=?
➥ terminal=/dev/tty2 res=success'UID="root" AUID="dwalsh"
如果您使用 Podman 命令启动了容器,审计子系统会在审计日志中记录您的 UID。如果容器是通过 Docker 启动的,它将 -1 记录为 loginuid。想象一下,如果您的系统被容器攻击,您需要回溯并检查哪个用户通过 audit.log 启动了攻击您系统的容器。
让我们通过一个例子来展示这一点。首先,成为 root 用户,并使用 auditctl 在 /etc/passwd 文件上设置监视:
# auditctl -w /etc/passwd -p wa -k passwd
现在运行一个使用 Docker 的 --privileged 容器,它接触宿主的 /etc/passwd 文件:
# docker run --privileged -v /:/host registry.access.redhat.com/ubi8-
➥ micro:latest touch /host/etc/passwd
这模拟了如果 Docker 容器逃离了限制并能够修改宿主的 /etc/passwd 文件会发生什么。现在检查 audit.log,其中应该有 /etc/passwd 修改的记录。注意,审计日志显示 auid=unset。这就是审计日志表示修改 /etc/passwd 文件的用户 loginuid 的方式。如您所见,因为没有用户直接启动 Docker 守护进程,审计日志没有记录启动容器的用户:
# ausearch -k passwd -i
...
type=SYSCALL msg=audit(05/03/2022 08:24:52.885:464) : arch=x86_64
➥ syscall=openat success=yes exit=3 a0=AT_FDCWD a1=0x7ffef7a9ef75
➥ a2=O_WRONLY|O_CREAT|O_NOCTTY|O_NONBLOCK a3=0x1b6 items=2 ppid=6723
➥ pid=6743 auid=unset uid=root gid=root euid=root suid=root fsuid=root
➥ egid=root sgid=root fsgid=root tty=(none) ses=unset comm=touch
➥ exe=/usr/bin/coreutils
现在用 Podman 运行相同的命令:
# podman run --privileged -v /:/host registry.access.redhat.com/
➥ ubi8-micro:latest touch /host/etc/passwd
检查修改 /etc/passwd 文件的 Podman 容器的 audit.log,您会看到 auid=dwalsh。因为 Podman 遵循 fork/exec 模型,并且是由登录系统的用户启动的,该用户在 loginuid 中有记录,所以 audit.log 可以记录哪个用户启动了攻击系统的容器:
# ausearch -k passwd -i
...
type=SYSCALL msg=audit(05/03/2022 08:25:42.466:480) : arch=x86_64
➥ syscall=openat success=no exit=EACCES(Permission denied) a0=AT_FDCWD
➥ a1=0x7fff3d5aef59 a2=O_WRONLY|O_CREAT|O_NOCTTY|O_NONBLOCK a3=0x1b6
➥ items=2 ppid=6978 pid=6986 auid=dwalsh uid=root gid=root euid=root
➥ suid=root fsuid=root egid=root sgid=root fsgid=root tty=(none) ses=1
➥ comm=touch exe=/usr/bin/coreutils
➥ subj=system_u:system_r:container_t:s0:c484,c845 key=passwd
注意:在当前的 Fedora 中,审计子系统已被禁用。您可以通过删除 /etc/audit/rules.d/audit.rules 并使用 augenrules --load 命令重新生成审计规则来启用它。
这就是为什么在 2014 年,我说通过非 root 进程访问 docker.sock 比提供 root 进程或 sudo 访问更危险,因为这两种情况都会记录 loginuid,这意味着您可以跟踪用户在系统上的操作。当您提供对运行 docker.sock 的 root 的访问权限时,您没有任何跟踪数据。让我们在下一节中看看您如何保护内核和文件系统免受容器内进程的影响。
11.2 Podman 密钥处理
在运行容器时,通常需要向容器内运行的服务提供密钥。例如,这是一个需要管理员和密码来控制访问的数据库工具。另一个例子是需要一个密码才能访问另一个服务的服务。
这些应用程序的开发者不希望将密钥信息硬编码到镜像中。容器应用程序的用户必须提供密钥。您只需将密钥通过环境变量提供给应用程序即可,但这意味着如果您提交镜像,密钥也会被提交到镜像中。
Podman 提供了一个密钥机制,podman secret,允许您在不将这些密钥保存在将容器提交到镜像时添加文件或环境变量。首先,让我们看看如何创建一个密钥。
列表 11.1 在 Podman 容器中使用密钥
$ echo "This is my secret" > /tmp/secret ❶
$ podman secret create my_secret /tmp/secret ❷
b5f27b90e9b3486fb5a78d1eb
$ podman run --rm --secret my_secret ubi8 cat
/run/secrets/my_secret ❸
This is my secret
❶ 将您的密钥数据添加到文件中。
❷ 使用 podman secret create 命令根据文件命名一个密钥。
❸ 使用 --secret 选项将密钥泄露到容器中。
您还可以通过添加 --secret my_secret,type=env 标志将密钥泄露到容器中作为环境变量:
$ podman run --secret my_secret,type=env --name secret_ctr ubi8 bash
➥ -c 'echo $my_secret'
This is my secret
如果您要将此容器提交到镜像,密钥将不会保存在镜像内部。
列表 11.2 当容器提交到镜像时,密钥不会被保存。
$ podman commit secret_ctr secret_img ❶
Getting image source signatures
Copying blob a9820c2af00a skipped: already exists
Copying blob 3d5ecee9360e skipped: already exists
Copying blob dc409efbefc4 done
Copying config 501812299f done
Writing manifest to image destination
Storing signatures
501812299f0c0cfbb032d144e6d2c2a41c5eadf229e7b76f6264ab74d9f6c069
$ podman image inspect secret_img --format
➥ '{{ .Config.Env }}' ❷
[TERM=xterm container=oci PATH=/usr/local/sbin:/usr/local/
➥ bin:/usr/sbin:/usr/bin:/sbin:/bin]
❶ 将 secret_ctr 提交到 secret_img 镜像中。
❷ 检查镜像以查看提交的环境变量,并注意 my_secret 环境变量没有被提交。
表 11.1 列出了所有 podman secret 命令。
表 11.1 podman secret 命令
| 命令 | 手册页 | 描述 |
|---|---|---|
create |
podman-secret-create(1) |
创建一个新的密钥。 |
inspect |
podman-secret-inspect(1) |
显示一个或多个密钥的详细信息。 |
ls |
podman-secret-ls(1) |
列出所有可用的密钥。 |
rm |
podman-secret-rm(1) |
删除一个或多个密钥。 |
11.3 Podman 镜像信任
在许多情况下,容器镜像的用户希望指定他们信任哪些容器镜像仓库和镜像。podman image trust 命令允许您指定您信任的仓库。它还允许您指定要阻止的仓库。
信任注册库的位置由图像的传输和注册库主机确定。以使用此容器图像—docker://quay.io/podman/stable—为例,Docker 是传输,quay.io 是注册库主机。
注意:Podman 图像信任在远程模式下不可用,例如,在 Mac 或 Windows 箱子上。您必须在 Linux 箱子上执行此处记录的命令。如果您使用 Podman 机器,请使用 podman machine ssh 命令进入虚拟机。有关更多信息,请参阅附录 E 和 F。
信任策略定义在 /etc/containers/policy.json 中,它描述了信任的注册库范围(注册库和/或存储库)。信任策略可以使用公钥为签名的图像。必须以 root 用户运行 podman image trust 命令。
信任的范围从最具体到最不具体进行评估。换句话说,可以为整个注册库定义一个策略。或者,它可以为该注册库中的特定存储库定义策略,或者定义到注册库中特定签名的图像。在以下示例中,您拒绝从 docker.io 拉取,然后后来仅指定允许拉取 docker.io/library 图像。
以下列表包括在 policy.json 中可以使用的有效范围值,从最具体到最不具体:
docker.io/library/busybox:notlatest
docker.io/library/busybox
docker.io/library
docker.io
如果在这些范围内没有找到任何配置,则使用默认值(使用 default 而不是 REGISTRY[/REPOSITORY] 指定),如下所示。表 11.2 描述了用于注册库的有效信任值。
列表 11.3 告诉 Podman 不要从特定的容器注册库拉取图像
$ sudo podman image trust set -t reject docker.io ❶
$ podman pull alpine ❷
Trying to pull docker.io/library/alpine:latest...
Error: Source image rejected: Running image docker://alpine:latest
➥ is rejected by policy.
$ sudo podman image trust set -t accept
➥ docker.io/library ❸
$ podman pull alpine ❹
Trying to pull docker.io/library/alpine:latest...
Getting image source signatures
Copying blob 59bf1c3509f3 skipped: already exists
Copying config c059bfaa84 done
Writing manifest to image destination
Storing signatures
C059bfaa849c4d8e4aecaeb3a10c2d9b3d85f5165c66ad3a4d937758128c4d18
$ podman pull bitnami/nginx ❺
Resolving "bitnami/nginx" using unqualified-search registries
➥ (/etc/containers/registries.conf.d/999-podman-machine.conf)
Trying to pull docker.io/bitnami/nginx:latest...
Error: Source image rejected: Running image docker://bitnami/nginx:latest
➥ is rejected by policy.
❶ 使用 podman 图像信任拒绝来自 docker.io 容器注册库的所有图像。
❷ 尝试从容器注册库中拉取 Alpine 图像,并查看 Podman 拒绝了该图像。
❸ 使用 Podman 图像信任设置 docker.io/library 的更具体的注册库/存储库。
❹ Podman 可以拉取 docker.io/library/alpine 图像。
❺ 从 docker.io 的其余部分拉取的图像被拒绝。
表 11.2 信任类型告诉容器引擎(如 Podman)信任哪些注册库。
| 类型 | 描述 |
|---|---|
accept |
允许从指定的注册库拉取图像。 |
reject |
不允许从指定的注册库拉取图像。 |
signBy |
来自指定注册库的图像必须由指定的名称签名。 |
如果检查 policy.json 文件,您会看到由 podman image trust 命令添加的条目:
$ cat /etc/containers/policy.json
{
"default": [
{
"type": "insecureAcceptAnything"
}
],
"transports": {
"docker": {
"docker.io": [
{
"type": "reject"
}
],
"docker.io/library": [
{
"type": "insecureAcceptAnything"
}
]
...
您可以使用 podman image trust show 命令以更易于查看的形式显示当前设置:
$ podman image trust show
all default accept
repository docker.io reject
repository docker.io/library accept
repository registry.access.redhat.com signed security@redhat.com
https://access.redhat.com/webassets/docker/content/sigstore
repository registry.redhat.io signed
➥ security@redhat.com https://registry.redhat.io/containers/sigstore
docker-daemon accept
通过 accept 和 reject 标志,您可以设置信任和拒绝哪些注册库。如果您想锁定生产系统中图像的来源,您可以更改系统的 default 策略为 reject 来自任何注册库的图像。您想要允许的所有图像都必须来自特定的注册库:
$ sudo podman image trust set --type=reject default
$ podman image trust show
all default reject
repository docker.io reject
repository docker.io/library accept
repository registry.access.redhat.com signed security@redhat.com
https://access.redhat.com/webassets/docker/content/sigstore
repository registry.redhat.io signed
➥ security@redhat.com https://registry.redhat.io/containers/sigstore
docker-daemon accept
在你的系统上设置这些设置后,Podman 接受来自 docker.io/library 的图像和来自 registry.redhat.io 的签名图像。来自其他注册表的图像都将被拒绝。Podman 还允许直接从 docker-daemon 拉取图像。
不要忘记恢复默认的 policy.json:
$ sudo cp /tmp/policy.json /etc/containers/policy.json
Podman 支持使用来自容器注册表的签名图像。红帽公司签名并分发其图像。让我们看看你如何也能签名图像。
11.3.1 Podman 图像签名
签名图像的一种方式是使用 GNU Privacy Guard (gnupg.org) 密钥。Podman 可以在将图像推送到远程注册表之前对其进行签名,这被称为 简单签名。你可以配置 Podman 和其他容器引擎,要求图像必须使用特定的签名进行签名。所有未签名的图像都将被拒绝。
首先,你需要创建一个 GPG 密钥对或选择一个预制的密钥对。你可以通过运行 gpg --full-gen-key 并遵循交互式对话框来生成新的 GPG 密钥。有关创建密钥的说明,请参阅以下网页:mng.bz/JV9V。
以下是一个使用默认参数创建简单密钥的示例。请确保使用你自己的电子邮件地址:
$ gpg --batch --passphrase '' --quick-gen-key dwalsh@redhat.com default
➥ default
大多数容器注册表都不理解图像签名;它们只是为容器图像提供远程存储。如果你想签名一个图像,你需要自己分发签名,通常使用网络服务器。你可以配置 Podman 和其他容器引擎从该网络服务检索签名。
在以下示例中,你将在本地机器上创建一个运行着的网络服务来演示图像签名。Podman 能够通过单个命令推送和签名图像。Podman 读取注册表配置文件 /etc/containers/registries.d/default.yaml 中的签名位置。
检查 default.yaml 文件以找到 sigstore-staging 标志并查看 Podman 存储签名的默认位置:
sigstore-staging: file:///var/lib/containers/sigstore
sigstore-staging 标志告诉 Podman 将签名存储在 /var/lib/containers/sigstore 目录中。当你想让其他用户使用这些签名来验证你的图像时,你需要将这些图像上传到网络服务器。现在你已经准备好测试简单的签名了,首先签名 ubi8 图像,然后设置 Podman 使用签名来验证拉取的图像。
签名并推送图像
在开始本节之前,你应该备份几个安全文件,以便稍后恢复:
$ sudo cp /etc/containers/registries.d/default.yaml
➥ /etc/containers/policy.json /tmp
让我们从注册表中拉取一个图像并添加一个签名,然后将它推回注册表。请确保使用你自己的注册表账户、图像和之前创建的 GPG 密钥:
$ sudo podman pull quay.io/rhatdan/myimage
Trying to pull quay.io/rhatdan/myimage:latest...
...
2c7e43d880382561ebae3fa06c7a1442d0da2912786d09ea9baaef87f73c29ae
$ podman login quay.io/rhatdan
Username: rhatdan
Password:
Login Succeeded!
$ sudo -E GNUPGHOME=$HOME/.gnupg \
podman push --tls-verify=false --sign-by dwalsh@redhat.com
➥ quay.io/rhatdan/myimage
...
Storing signatures
在 sigstore-staging 目录 /var/lib/containers/sigstore 中查找仓库名称 rhatdan。你会看到有一个新的签名可用,这是由 podman push 命令创建的。请确保使用你自己的注册表账户名:
$ sudo ls /var/lib/containers/sigstore/rhatdan/
'myimage@sha256=0460a9d13a806e124639b23e9d6ffa1e5773f7bef91469bee6ac88
➥ a4be213427'
现在你已经签了镜像,你需要设置一个 Web 服务器来提供签名,并配置 Podman 和其他容器引擎以使用签名和已签名的镜像。
配置 Podman 拉取已签名的镜像
当配置 Podman 使用签名来验证镜像时,你需要配置系统以检索签名。通常,你会在 Web 服务上共享签名。你可以通过在/etc/containers/registries.d/default.yaml文件中配置sigstore标志来识别存储签名的网站。Podman 从该网站下载这些签名。
对于这个例子,你将创建一个在本地主机8000端口上运行的 Web 服务。将sigstore: http://localhost:8000 Web 服务器添加到默认的default.yaml文件中。这将告诉 Podman 在拉取镜像时从该 Web 服务器检索签名。Podman 根据镜像的名称及其摘要查找签名:
$ echo " sigstore: http://localhost:8000" | sudo tee --append
➥ /etc/containers/registries.d/default.yaml
对于这个例子,在本地预演签名存储/var/lib/containers/sigstore中使用python3启动一个新的服务器:
$ cd /var/lib/containers/sigstore && python3 -m http.server
Serving HTTP on 0.0.0.0 port 8000 (http://0.0.0.0:8000/) ...
在另一个窗口中,从本地存储中删除 quay.io/rhatdan/myimage,因为你想要带签名的拉取:
$ podman rmi quay.io/rhatdan/myimage
Untagged: quay.io/rhatdan/myimage:latest
Deleted: 2c7e43d880382561ebae3fa06c7a1442d0da2912786d09ea9baaef87f73c29ae
你需要为 quay.io/rhatdan 存储库设置镜像信任,并将 publickey.gpg 公钥分配给用于验证 dwalsh@redhat.com 签名的镜像:
$ sudo podman image trust set -f /tmp/publickey.gpg quay.io/rhatdan
之前的 Podman 命令将以下段落添加到/etc/containers/policy.json文件中:
...
"transports": {
"docker": {
"quay.io/rhatdan": [
{
"type": "signedBy",
"keyType": "GPGKeys",
"keyPath": "/tmp/publickey.gpg"
}
],
...
你还没有创建keyPath文件/tmp/publickey.gpg。使用以下 GPG 命令创建它:
$ gpg --output /tmp/publickey.gpg --armor --export dwalsh@redhat.com
现在,你可以拉取已签名的镜像:
$ podman pull quay.io/rhatdan/myimage
Trying to pull quay.io/rhatdan/myimage:latest...
...
Writing manifest to image destination
Storing signatures
2c7e43d880382561ebae3fa06c7a1442d0da2912786d09ea9baaef87f73c29ae
这成功了!尽管如此,你仍然不确定它是否使用了签名。通过尝试从没有签名的仓库中拉取另一个镜像来证明给自己,这将失败:
$ podman pull quay.io/rhatdan/podman
Trying to pull quay.io/rhatdan/podman:latest...
Error: Source image rejected: A signature was required,
➥ but no signature exists
确保将所有设置恢复到默认值:
$ sudo cp /tmp/default.yaml /etc/containers/registries.d/default.yaml
$ sudo cp /tmp/policy.json /etc/containers/policy.json
此外,停止在另一个终端中启动的本地主机 Web 服务器。表 11.3 描述了你需要设置的必要基础设施,以允许在环境中使用简单的签名。
表 11.3 简单签名所需的基础设施
| 要求 | 描述 |
|---|---|
| GPG 私钥 | 你需要一个 GPG 密钥对,其中私钥用于签名镜像的服务。 |
| 签名 Web 服务器 | Web 服务器必须运行在可以访问签名存储的地方。 |
一旦你设置了使用简单签名的必要基础设施,你将需要了解每个使用和验证签名的客户端的要求。表 11.4 列出了这些要求。
表 11.4 简单签名所需客户端配置
| 要求 | 描述 |
|---|---|
| GPG 公钥(/tmp/publickey.gpg) | 用于签名的公钥必须在拉取已签名镜像的任何机器上存在。 |
| 客户端 sigstore 配置 | 签名 Web 服务器必须在所有需要拉取已签名镜像的系统的/etc/containers/registries.d/*.yaml文件中配置为 sigstore。 |
| 客户端图像信任配置 | 图像信任必须在使用这些图像的每个容器引擎系统上进行配置。 |
11.4 Podman 图像扫描
Podman 不是一个图像扫描器;它将这项任务留给了其他工具。但 Podman 确实有一个很好的功能,使得扫描器扫描图像变得更加容易。Podman 可以直接挂载可扫描的图像。扫描器查看图像挂载的内容,而无需执行图像中的任何代码。回想一下,您不能在没有首先进入用户命名空间的情况下以 rootless 模式挂载容器或图像。执行 podman image mount 命令以显示错误:
$ podman image mount ubi8
Error: cannot run command "podman image mount" in rootless mode, must
➥ execute `podman unshare` first
在下一个示例中,您首先使用 podman unshare 进入用户命名空间,然后挂载 ubi8 图像。最后,更改目录到挂载目录,并运行一个 find 命令以定位图像中的所有 setuid 二进制文件。注意,您使用来自主机操作系统的工具来扫描图像:
$ podman unshare
# podman image mount
# mnt=$(podman image mount ubi8)
# echo $mnt
/home/dwalsh/.local/share/containers/storage/overlay/05ddfb76c5eb2146646c70
➥ e20db21a35dfec2215f130ce8bd04fce530142cfbd/merged
# cd $mnt
# /usr/bin/find . -user root -perm -4000
./usr/libexec/dbus-1/dbus-daemon-launch-helper
./usr/bin/chage
./usr/bin/mount
./usr/bin/umount
./usr/bin/newgrp
./usr/bin/gpasswd
./usr/bin/passwd
./usr/bin/su
./usr/sbin/userhelper
./usr/sbin/unix_chkpwd
./usr/sbin/pam_timestamp_check
使用图像内部工具扫描图像是不安全的,因为图像的攻击者可以修改扫描工具。Podman 使得扫描器更容易完成他们的工作。
11.5.1 只读容器
我经常谈论生产环境中的容器与开发环境中的容器。当容器化应用程序处于开发状态时,能够写入容器图像并在以后提交该图像是有用的。尽管这很常见,但大多数人会在实际构建图像时切换到使用 Containerfiles。底线是,一旦开发人员将软件移交给质量工程团队,他们期望内容被视为只读。
当在生产环境中运行容器时,我认为以只读模式运行图像是有意义的。想象一下,您正在运行一个被黑客攻击的应用程序。黑客首先想要做的是将后门写入应用程序;然后,下一次容器或应用程序启动时,容器已经有了漏洞。如果图像是只读的,黑客将无法留下后门,并被迫从头开始循环。
--read-only 选项阻止应用程序将内容写入图像,并强制应用程序只将内容写入 tmpfs 文件系统或添加到容器中的卷。有时您可能想要阻止容器在您的系统上的任何位置写入,并且只读取或执行容器内的代码。以只读模式运行容器的另一个好处是,您可以捕获到您不知道容器正在写入图像的错误。最后,在像 overlayfs 这样的写时复制文件系统上写入,几乎总是比写入卷或 tmpfs 慢:
$ podman run --read-only ubi8 touch /foo
touch: cannot touch '/foo': Read-only file system
以 rootless 模式运行的一个问题是,应用程序通常期望写入 /run、/tmp 和 /var/tmp。Podman 通过在这些位置自动挂载 tmpfs 文件系统来管理这个问题:
$ podman run --read-only ubi8 touch /run/foo
由于一些用户认为允许容器化应用程序在任何地方写入,即使在 tmpfs 挂载上,也太不安全了,Podman 添加了--read-only-tmpfs选项。--read-only-tmpfs选项在--read-only模式下运行时添加了/run、/tmp 和/var/tmp tmpfs。如果您想禁用此功能,可以使用–-read-only-tmpfs=false标志:
$ podman run --read-only-tmpfs=false --read-only ubi8 touch /run/foo
touch: cannot touch '/run/foo': Read-only file system
11.5 深度安全
在安全领域,有一个常见的想法,即深度安全。根据这一理念,应使用多层或工具来保护资产。这一理念的典型类比是古代城堡的安全,通常建在山顶上,有多个城墙,有护城河,甚至还有更多的安全功能。攻击者需要突破所有这些层才能到达统治者。
容器安全的工作方式与此类似。Podman 使用 Linux 提供的所有安全机制,为您提供深度安全。
11.5.1 Podman 同时使用所有安全机制
Podman 容器可以运行本章提到的所有安全机制。这意味着被黑客攻击的容器需要找到一种方法来逃离只读文件系统、命名空间、丢弃的能力、SELinux、seccomp 等等,以获得对您系统的访问权限。
在某些情况下,您可能需要放宽一些安全机制,以便容器可以运行。理解如何处理本章讨论的安全功能总是比仅仅使用--privileged标志运行容器要好,这个标志会关闭您所有的防御。
Podman 旨在为容器提供合理的安全包装,但它需要允许通用容器成功。了解您的容器应用程序的安全需求和 Podman 的安全功能,可以使您提高容器安全包装。如果您知道您的容器不需要以 root 身份运行,就不要以 root 身份启动它。如果您的容器不需要任何 Linux 能力,就丢弃它们。无 root 容器比有 root 容器更好。您还可以考虑以只读模式或在内部分离的用户命名空间中运行容器。通过简单地采用这些措施,您就有能力使您的容器化应用程序的城堡墙壁更加坚固。
11.5.2 您应该在何处运行您的容器?
我将留给您最后一个想法。在本章的开头,我谈到了住在不同类型庇护所中的三只猪——独立房屋、联排别墅和公寓楼——每个都比上一个稍微不安全一些。容器安全可以做得比住在单独住宅单元的猪更好,因为单元可以堆叠在一起。
假设你拥有两个不同的容器:一个用于网页前端的容器和一个包含信用卡数据的数据库容器。如果你想确保它们是隔离的,你可以在系统内部将它们放在同一个容器中,或者更好的做法是将它们放入容器中,但分别放入不同的虚拟机中,最后再将这些虚拟机放在不同的机器上。你将能够将你的网页前端放入一个运行在容器内部的虚拟机中,该虚拟机位于你的 DMZ 内部,面向互联网。你可以在将你的数据库放在你的私有网络内部的同时完成所有这些操作,而不会限制你的网页前端对网络的访问。可能性几乎是无限的。
摘要
-
容器安全有许多不同的方面,包括运行容器的隔离、信任镜像和注册表、扫描镜像等。
-
深度防御意味着你的容器工具利用尽可能多的安全机制。如果某个安全机制失效,其他机制可能仍然能保护你的系统。
-
容器安全全部关乎保护 Linux 内核和宿主文件系统免受恶意容器进程的侵害。
-
设置和控制你在系统上运行的容器镜像至关重要。不要允许你的用户从互联网上运行随机应用程序。
附录 A. Podman 相关的容器工具
本附录描述了使用 containers/storage 和 containers/image 库的三个工具。这些工具解决了以下功能:
-
在不同的容器注册表和存储之间移动容器镜像
-
构建容器镜像
-
在单个节点上测试、开发和在生产环境中运行容器
-
在生产环境中大规模运行容器
作为 Podman 的原始创建者,我认识到需要专门的工具,每个工具都执行特定的功能,而不是一刀切的单体解决方案。
从安全角度来看,这四个类别需要不同的安全约束。在生产环境中运行的容器需要在一个比开发和测试环境中运行得更安全的环境中运行。在注册表中移动容器镜像不需要对您运行命令的主机具有特权访问权限——只需要对注册表的远程访问。如果您在构建过程中需要更多访问权限,那么在生产环境中,它们将获得与构建过程中相同的访问权限。
单体守护进程的另一个关键问题是它阻止了对工具的实验,并且不允许它们独立发展。一个例子是我们提出更改 Docker 守护进程,允许用户从容器注册表中拉取不同类型的 OCI 内容。这个更改被拒绝了,因为它与 Docker 容器几乎没有关系。
同样,当单体守护进程为某个产品修改时,它可能会对使用该守护进程的另一个产品的功能产生负面影响。它可能导致性能下降或完全损坏。这发生在 Kubernetes 开发期间,因为它依赖于 Docker 守护进程作为容器引擎。但由于 Docker 是单体的,并且为许多其他项目开发,其中许多更改影响了 Kubernetes,导致不稳定。很明显,Kubernetes 需要为其工作负载提供一个专门的容器引擎,2020 年 12 月宣布 Kubernetes 最终将使用新开发的标准化接口,即容器运行时接口(CRI;见mng.bz/yaDq),以改善编排程序与不同容器运行时之间的交互。我编写了一本彩色书,《容器指挥官》(图 A.1;red.ht/3gfVlHF),由 Máirín Duffy (@marin) 插图,描述了本附录中讨论的容器工具,基于超级英雄。

图 A.1 容器彩色书 (red.ht/3gfVlHF)
最后,有时存在相互冲突的利益或发布计划。拥有独立的独立工具允许以它们自己的速度独立部署,从而确保向客户保证新功能。为表 A.1 中描述的独立功能创建了四个项目。
表 A.1 基于 containers/storage 和 containers/image 的主要容器工具。
| 工具 | 描述 |
|---|---|
| Skopeo | 对容器镜像和镜像仓库执行各种操作 (github.com/containers/skopeo) |
| Buildah | 促进对容器镜像执行广泛的操作 (github.com/containers/buildah) |
| Podman | Pod、容器和镜像的全能管理工具 (github.com/containers/podman) |
| CRI-O | 基于 OCI 的 Kubernetes 容器运行时接口实现 (github.com/cri-o/cri-o) |
由于你已经学到了很多关于 Podman 的知识,你现在知道为什么它被包含在这个列表中。Podman 是一个理解和发展容器、Pod 和镜像的优秀工具。它封装了 Docker CLI 所做的所有事情,但不需要将所有内容锁定在一个中央守护进程下。因为 Podman 无守护进程工作并使用操作系统共享数据,其他工具可以与相同的数据存储和库一起工作。本附录的其余部分描述了其余的工具,从 Skopeo(图 A.2)开始。

图 A.2 Skopeo、Buildah 和 Podman 通过共享相同的容器/存储镜像和容器/镜像库来协同工作,以拉取和推送镜像。
A.1 Skopeo
当使用 Docker 或 Podman 等容器引擎时,如果你想检查注册表中的容器镜像,你必须从注册表将其拉取到本地存储。只有在这种情况下,你才能检查它。问题是这个镜像可能非常大,在检查后,你可能会意识到它不是你预期的,你浪费了时间拉取它。因为用于拉取和检查镜像的协议只是一个网络协议,所以创建了一个简单的工具 Skopeo 来拉取镜像的详细信息并在屏幕上显示。Skopeo 是希腊语中 远程查看 的意思。
![图片 A-UN01.png]
执行以下 skopeo inspect 命令以以 JSON 格式检查镜像的详细信息:
$ skopeo inspect docker:/ /quay.io/rhatdan/myimage
{
"Name": "quay.io/rhatdan/myimage",
"Digest":
"sha256:fe798c1576dc7b70d7de3b3ab7c72cd22300b061921f052279d88729708092d8",
"RepoTags": [
"Latest",
"1.0"
],
...
Skopeo 被扩展以也能从注册表中复制镜像。最终,Skopeo 成为了在不同类型存储(传输)之间复制镜像的工具。这些存储类型成为了表 A.2 中定义的传输。
表 A.2 Podman 支持的传输方式
| 传输 | 描述 |
|---|---|
容器注册表 (docker) |
这是默认的传输方式。它引用存储在远程容器镜像注册网站中的容器镜像。注册表存储和共享容器镜像(例如,docker.io 和 quay.io)。 |
oci |
引用容器镜像;符合 Open Container Initiative 格式规范。清单和层 tarball 位于本地目录中作为单独的文件。 |
dir |
引用符合 Docker 图像布局的容器图像。它与 oci 传输方式非常相似,但使用传统的 Docker 格式存储文件。作为一个非标准化格式,它主要用于调试或非侵入式容器检查。 |
docker-archive |
引用打包在 TAR 归档中的 Docker 图像布局中的容器图像。 |
oci-archive |
引用符合 Open Container Initiative 格式规范的图像,该图像打包在 TAR 归档中。它与 docker-archive 传输方式非常相似,但以 OCI 格式存储图像。 |
docker-daemon |
引用存储在 Docker 守护进程内部存储中的图像。由于 Docker 守护进程需要 root 权限,Podman 必须由 root 用户运行。 |
container-storage |
引用位于本地容器存储中的图像。它不是一个传输方式,而是一种存储图像的机制。它可以用来将其他传输方式转换为 container-storage。Podman 默认使用 container-storage 来存储本地图像。 |
其他容器引擎和工具希望使用 Skopeo 中开发的复制图像的功能,因此 Skopeo 被拆分为两部分:命令行 Skopeo 和底层库 containers/image。将功能拆分为单独的库使得构建其他容器工具成为可能,包括 Podman。
skopeo copy 命令在在不同类型的容器存储之间复制图像时非常受欢迎。与 Podman 和 Buildah 相比,一个不同之处在于,正如你在 A.2 节中看到的,Skopeo 强制用户指定源和目的地的传输方式。Podman 和 Buildah 默认根据上下文和命令使用 docker 或 containers-storage 传输方式。在以下示例中,你将使用 docker 传输方式从一个容器注册库复制图像,并使用 container-storage 传输方式将图像本地存储:
$ skopeo copy docker:/ /quay.io/rhatdan/myimage containers-storage:quay.io/rhatdan/myimage
Getting image source signatures
Copying blob dfd8c625d022 done
Copying blob 68e8857e6dcb done
Copying blob e21480a19686 done
Copying blob fbfcc23454c6 done
Copying blob 3f412c5136dd done
Copying config 2c7e43d880 done
Writing manifest to image destination
Storing signatures
另一个许多 Skopeo 用户使用的命令是 skopeo sync,它允许你在容器注册库和本地存储之间同步图像。
Skopeo 主要用于基础设施项目,以帮助配置多个容器注册库——例如,将公共注册库中的图像复制到私有注册库中。表 A.3 描述了与 Skopeo 一起使用的最常用命令。第一个利用 containers/image 库的工具是 Buildah。
表 A.3 Skopeo 主要命令及其描述
| 命令 | 描述 |
|---|---|
skopeo copy |
从一个位置复制图像(清单、文件系统层或签名)到另一个位置。 |
skopeo delete |
标记图像名称,以便由注册库的垃圾收集器稍后删除。 |
skopeo inspect |
返回关于注册库中图像名称的低级信息。 |
skopeo list-tags |
列出特定传输图像存储库中的标签。 |
skopeo login |
登录到容器注册库(与 podman login 相同)。 |
skopeo logout |
从容器注册库登出(与 podman logout 相同)。 |
skopeo manifest digest |
计算一个清单文件的清单摘要,并将其写入标准输出。 |
skopeo sync |
在容器注册库和本地目录之间同步镜像。 |
A.2 Buildah
正如你在 1.1.2 节中学到的,创建容器镜像意味着在磁盘上创建一个目录并向其中添加内容,使其看起来像 Linux 机器上的根目录 /,称为 rootfs。最初,完成这一点的唯一方法是通过 docker build 使用 Dockerfile。虽然 Dockerfile 和 Containerfile 是创建容器镜像配方的好方法,但还需要一个低级别的构建块工具,允许以其他方式构建容器镜像——允许将镜像构建过程分解成单独的命令,让你可以使用比 Containerfile 更强大的脚本工具和语言来构建镜像。我们创建了一个名为 Buildah 的工具 (buildah.io) 来满足这个目的。

Buildah 被设计成构建容器镜像的简单工具。它建立在容器存储和容器图像库之上,就像 Podman 和 Skopeo 一样。它具有许多与 Podman 相似的功能。你可以拉取镜像、推送镜像、提交镜像,甚至可以在镜像上运行容器。Podman 与 Buildah 主要的区别在于其底层的 容器 概念。Podman 容器是一个长期运行的容器,一个 运行 的容器,而 Buildah 容器只是一个临时的容器,一个 工作 的容器,它将被用来创建一个 OCI 镜像。
注意:Buildah 是一个仅适用于 Linux 的工具,在 Mac 或 Windows 上不可用。然而,Podman 在 podman build 命令中集成了 Buildah。Mac 和 Windows 上的 Podman 使用服务器端的 Buildah 代码,允许这些平台使用 Containerfiles 和 Dockerfiles 进行构建。有关更多信息,请参阅附录 E 和 F。
Buildah 是为了将 Dockerfile 中定义的步骤在命令行中可用而设计的。Buildah 希望通过允许你使用操作系统内所有可用的工具来填充镜像,从而简化容器镜像的构建过程。你可以通过标准 Linux 工具,如 cp、make、yum install 等将数据添加到这个目录中。然后提交 rootfs 到一个 tarball 中,添加一些 JSON 来描述镜像创建者希望镜像执行的操作,最后将这个 tarball 推送到容器注册库。基本上,Buildah 将你在 Containerfile 中学到的步骤分解成可以在 shell 中执行的单独命令。
注意:名称 Buildah 是我对 builder 这个词发音的戏谑。如果你曾经听过我说话,你会注意到我有一个强烈的波士顿口音。当核心团队问我想要给这个工具起什么名字时,我说:“我不在乎,就叫它 builder 吧。” 他们听成了 Buildah。
构建新的容器镜像的第一步是拉取基础镜像。在 Containerfile 中,这是通过 FROM 指令完成的。
A.2.1 从基础镜像创建工作容器
首先要查看的命令是 buildah from。它等同于 Containerfile 的 FROM 指令。当执行 buildah from IMAGE 时,它会从容器注册库中拉取指定的镜像,将其保存在本地容器存储中,并基于此镜像创建一个工作容器。如前所述,这个容器类似于 Podman 容器,但它只临时存在,以成为容器镜像。在以下示例中,基于 ubi8-init 镜像创建了一个工作容器。
附录 A.1 Buildah 拉取镜像并创建 Buildah 容器
$ buildah from ubi8-init
Resolved "ubi8-init" as an alias (/etc/containers/registries.conf.d/
➥ 000-shortnames.conf)
Trying to pull registry.access.redhat.com/
➥ ubi8-init:latest... ❶
Getting image source signatures
Checking if image destination supports signatures
Copying blob adffa6963146 done
Copying blob 29250971c1d2 done
Copying blob 26f1167feaf7 done
Copying config 4b85030f92 done
Writing manifest to image destination
Storing signatures
ubi8-init-working-container ❷
❶ 从容器注册库拉取镜像
❷ 输出新的容器名称
注意到 buildah from 的输出看起来与 podman pull 的输出相同,除了最后一行,它输出了容器名称:ubi8-init-working-container。如果您再次运行 buildah from 命令,您将得到第二个容器名称:
$ buildah from ubi8-init
ubi8-init-working-container-1
Buildah 会跟踪其容器,并通过递增计数器来生成每个容器。当然,您可以使用 --name 选项覆盖容器名称。接下来,您将向这个容器镜像添加内容。
A.2.2 向工作容器添加数据
Buildah 有两个命令,buildah copy 和 buildah add,用于将文件、URL 或目录的内容复制到容器的当前工作目录。它们映射到 Containerfile 的 COPY 和 ADD 指令的功能。
注意:有两个命令几乎做同样的事情,这可能会有些令人困惑。在大多数情况下,我建议您只使用 Containerfile 中的 buildah copy 和 COPY 命令。这两个命令的主要区别在于 COPY 命令只将主机上的本地文件和目录复制到容器镜像中。add 命令支持使用 URL 拉取远程内容并将其插入到您的容器中。ADD 命令还支持将 TAR 和 ZIP 文件复制到容器镜像中并展开它们。
buildah copy 命令的语法要求您指定由 buildah from 命令先前创建的容器名称,然后是源和(可选的)目标。如果没有提供目标,源数据将被复制到容器的当前工作目录。如果目标目录不存在,将会创建它。
以下示例将本地 html/index.xhtml 文件(在 3.1 节中创建)复制到容器中的 /var/lib/www/html 目录:
$ buildah copy ubi8-init-working-container html/index.xhtml
➥ /var/lib/www/html/
如果您想使用更高级的工具,如包管理器,向容器添加内容,Buildah 支持在容器内运行命令。
A.2.3 在工作容器中运行命令
要在运行中的容器内运行命令,你需要执行buildah run。在底层,这个命令与RUN指令的工作方式完全相同;它会在当前容器之上启动一个新的容器,执行指定的命令,并将结果提交回运行中的容器。buildah run的语法要求你指定运行中的容器名称,然后跟随着命令。在下面的示例中,你将在容器内安装httpd服务:
$ buildah run ubi8-init-working-container dnf -y install httpd
Updating Subscription Management repositories.
Unable to read consumer identity
This system is not registered with an entitlement server. You can use
➥ subscription-manager to register.
...
Complete!
为了确保在创建运行中的容器后,你将有一个正在运行的 Web 服务器,下一个命令将启用 Apache HTTP 服务器服务:
$ buildah run ubi8-init-working-container systemctl enable httpd.service
Created symlink /etc/systemd/system/multi-user.target.wants/httpd.service →
➥ /usr/lib/systemd/system/httpd.service.
表 A.4 展示了 Containerfile 指令与 Buildah 命令之间的关系。
表 A.4 将 Containerfile 指令映射到 Buildah 命令
| 指令 | 命令 | 描述 |
|---|---|---|
ADD |
buildah add |
将文件、URL 或目录的内容添加到容器中。 |
COPY |
buildah copy |
将文件、URL 或目录的内容复制到容器的运行目录中。 |
FROM |
buildah from |
创建一个新的运行中的容器,要么从头开始,要么使用指定的镜像作为起点。 |
RUN |
buildah run |
在容器内运行命令。 |
A.2.4 直接从宿主机向运行中的容器添加内容
到目前为止,你已经看到了 Buildah 如何执行与你在 Containerfile 中执行的相同命令,但 Buildah 的一个目标是将容器镜像的 rootfs 直接暴露给宿主机。这允许你使用宿主机上可用的命令向容器镜像添加内容,而无需在容器镜像内部存在这些命令。
buildah mount命令允许你将运行中容器的根文件系统直接挂载到你的系统上,然后使用cp、make、dnf或甚至是一个编辑器来操作容器根文件系统的内容。
如果你以 root 用户运行 Buildah,你可以简单地执行buildah mount命令。但在无 root 模式下,这是不允许的。回想一下第 2.2.10 节,你学习了podman mount命令,你必须首先进入用户命名空间。同样,buildah unshare命令创建一个在用户命名空间中运行的 shell。一旦你进入用户命名空间,你就可以挂载容器。在下面的示例中,使用你迄今为止所学的内容,你将使用宿主机的操作系统grep命令向容器添加内容:
$ buildah unshare
# mnt=$(buildah mount ubi8-init-working-container)
# echo $mnt
/home/dwalsh/.local/share/containers/storage/overlay/133e1728eac26589b07984
➥ e3bdf31b5e318159940c866d9e0493a1d08e1d2f6a/merged
# grep dwalsh /etc/passwd >> $mnt/etc/passwd
# exit
现在你可以检查你的更改是否实际上已应用于运行中的容器内部:
$ buildah run ubi8-init-working-container grep dwalsh /etc/passwd
dwalsh:x:3267:3267:Daniel J Walsh:/home/dwalsh:/bin/bash
在填充运行中容器的内容完成后,现在是时候指定 Containerfile 中的其他指令了。这些指令将描述你作为容器镜像创建者的意图。
A.2.5 配置运行中的容器
您可能在表 A.3 中注意到有很多 Containerfile 指令缺失。LABEL、EXPOSE、WORKDIR、CMD 和 ENTRYPOINT 等 Containerfile 指令用于填充 OCI 镜像规范。
现在,使用 buildah config 命令,您可以添加一个端口以暴露(EXPOSE)并标记容器根文件系统内的一个位置作为卷(VOLUME),该卷将用作网站根目录:
$ buildah config --port=80 --volume=/var/lib/www/html
➥ ubi8-init-working-container
您可以使用 buildah inspect 命令检查相应的 OCI 镜像规范字段:
$ buildah inspect --format '{{ .OCIv1.Config.ExposedPorts }} {{
➥ .OCIv1.Config.Volumes }}' ubi8-init-working-container
map[80:{}] map[/var/lib/www/html:{}]
表 A.4 显示了 Containerfile 指令与 Buildah 配置选项之间的关系。您还可以参考表 A.5 以获取有关这些指令的更多信息。
表 A.5 将 Containerfile 指令映射到 Buildah 配置选项
| 指令 | 选项 | 描述 |
|---|---|---|
MAINTAINER |
--author |
设置镜像作者的联系方式 |
CMD |
--cmd |
设置在容器内运行的默认命令 |
ENTRYPOINT |
--entrypoint |
为将在容器中运行的可执行文件设置命令 |
ENV |
--env |
为所有后续指令设置环境变量 |
HEALTHCHECK |
--healthcheck |
指定一个命令以检查容器是否仍在运行 |
LABEL |
--label |
添加键值元数据 |
ONBUILD |
--onbuild |
设置当镜像用作其他镜像的基础时运行的命令 |
EXPOSE |
--port |
指定容器在运行时将监听的端口 |
STOPSIGNAL |
--stop-signal |
设置在容器停止时发送的停止信号 |
USER |
--user |
设置运行容器时以及所有后续的 RUN、CMD 和 ENTRYPOINT 指令所使用的用户 |
VOLUME |
--volume |
为外部数据添加挂载点并将其标记为卷 |
WORKDIR |
--workingdir |
为所有后续的 RUN、CMD、ENTRYPOINT、COPY 和 ADD 指令设置工作目录 |
完成向 Buildah 容器镜像添加内容以及向 OCI 镜像规范添加配置后,您需要从工作容器创建镜像。
A.2.6 从工作容器创建镜像
您到目前为止一直在构建的工作容器可以使用 buildah commit 命令创建符合 OCI 规范的镜像。此命令与您在 2.1.9 节中学习的 podman commit 命令的工作方式相同。此命令的输入是工作容器名称和一个可选的镜像标签;如果没有指定标签,则镜像将没有名称:
$ buildah commit ubi8-init-working-container quay.io/rhatdan/myimage2
Getting image source signatures
Copying blob 352ba846236b skipped: already exists
Copying blob 3ba8c926eef9 skipped: already exists
Copying blob 421971707f97 skipped: already exists
Copying blob 9ff25f020d5a done
Copying config 5e47dbd9b7 done
Writing manifest to image destination
Storing signatures
5e47dbd9b7b7a43dd29f3e8a477cce355e42c019bb63626c0a8feffae56fcbf9
您可以使用 buildah images 查看镜像:
$ buildah images
REPOSITORY TAG IMAGE ID CREATED SIZE
quay.io/rhatdan/myimage2 latest 5e47dbd9b7b7 2 minutes ago 293 MB
registry.access.redhat
➥ .com/ubi8-init latest 4b85030f924b 5 weeks ago 253 MB
由于 Podman 和 Buildah 共享相同的容器镜像存储,您可以使用 podman images 看到相同的镜像:
$ podman images
REPOSITORY TAG IMAGE ID CREATED SIZE
quay.io/rhatdan/myimage2 latest 5e47dbd9b7b7 4 minutes ago 293 MB
registry.access.redhat
➥ .com/ubi8-init latest 4b85030f924b 5 weeks ago 253 MB
您甚至可以在镜像上运行 Podman 容器:
$ podman run quay.io/rhatdan/myimage2 grep dwalsh /etc/passwd
dwalsh:x:3267:3267:Daniel J Walsh:/home/dwalsh:/bin/bash
A.2.7 将镜像推送到容器注册库
与 Podman 类似,Buildah 有 buildah login 和 buildah push 命令,这些命令允许您将镜像推送到容器注册库,如下面的示例所示:
$ buildah login quay.io
Username: rhatdan
Password:
Login Succeeded!
$ buildah push quay.io/rhatdan/myimage2
Getting image source signatures
Copying blob 3ba8c926eef9 done
Copying blob 421971707f97 done
Copying blob 9ff25f020d5a done
Copying blob 352ba846236b done
Copying config 5e47dbd9b7 done
Writing manifest to image destination
Copying config 5e47dbd9b7 done
Writing manifest to image destination
Storing signatures
注意:您也可以使用 podman login 和 podman push 或甚至 skopeo login 和 skopeo copy 来完成相同任务。
恭喜!您已成功手动构建了一个符合 OCI 标准的容器镜像,而不是使用 Containerfile。此外,如果您想使用现有的 Containerfile 或 Dockerfile 创建镜像,可以使用 buildah build 命令。
A.2.8 从 Containerfiles 构建镜像
您可以使用 buildah build 命令从 Containerfile 或 Dockerfile 构建一个符合 OCI 标准的镜像。Buildah 包含一个解析器,它理解 Containerfile 格式,并可以使用之前描述的命令自动执行所有任务。在下一个示例中,使用第 2.3.2 节中的 Containerfile:
$ cat myapp/Containerfile
FROM ubi8/httpd-24
COPY index.xhtml /var/www/html/index.xhtml
您可以通过执行以下命令使用此 Containerfile 构建您的容器镜像:
$ buildah build ./myapp
STEP 1/2: FROM ubi8/httpd-24
Resolved "ubi8/httpd-24" as an alias (/home/dwalsh/.cache/containers/
➥ short-name-aliases.conf)
Trying to pull registry.access.redhat.com/ubi8/httpd-24:latest
...
Getting image source signatures
Checking if image destination supports signatures
Copying blob adffa6963146 skipped: already exists
...
STEP 2/2: COPY html/index.xhtml /var/www/html/index.xhtml
COMMIT
Getting image source signatures
Copying blob 352ba846236b skipped: already exists
...
bbfcf76c994c738f8496c1f274bd009ddbc960334b59a74953691fff00442417
您可能已经注意到,这个输出与 podman build 命令的输出完全匹配。这是因为 podman build 命令使用了 Buildah。
A.2.9 Buildah 作为库
Buildah 被设计成不仅可以用作命令行工具,还可以用作基于 Golang 的库。Buildah 被用于几个不同的工具中,例如 Podman 和 OpenShift 镜像构建器。Buildah 允许这些工具内部构建 OCI 镜像。每次您执行 podman build 时,您都在执行 Buildah 库代码。在学会了如何使用 Buildah 构建容器镜像、使用 Skopeo 在容器存储之间复制镜像以及使用 Podman 在主机上管理和运行容器之后,让我们来谈谈这些工具如何在 Kubernetes 生态系统中使用。
A.3 CRI-O:OCI 容器的容器运行时接口
当 Kubernetes 正在开发时,它使用 Docker API 内部运行容器。Kubernetes 依赖于 Docker 从一个版本到另一个版本变化的特性,有时会破坏 Kubernetes。同时,CoreOS 想要他们的替代容器引擎,称为 RKT (github.com/rkt/rkt),与 Kubernetes 一起工作。Kubernetes 开发者决定,然后,将 Docker 功能拆分出来并使用一个新的 API,称为容器运行时接口 (CRI;mng.bz/yaDq)。此接口允许 Kubernetes 使用除了 Docker 之外的其他容器引擎。
当 Kubernetes 想要拉取一个容器镜像时,它会通过 CRI 调用一个远程套接字,并要求监听器为它拉取一个 OCI 镜像。当它想要启动一个 Pod/容器时,它会调用套接字并要求启动容器。
注意:CoreOS 最终被 Red Hat 收购,RKT 项目已结束。Kubernetes 已弃用 Docker 作为容器运行时。
Red Hat 将 CRI 视为开发新容器引擎的机会,他们最终将其称为 OCI 容器容器运行时接口(CRI-O;cri-o.io/)。CRI-O 基于与 Skopeo、Buildah 和 Podman 相同的容器/storage 和容器/image 库,可以与这些工具一起使用。CRI-O 的主要目标是取代 Docker 服务作为 Kubernetes 的容器引擎。

CRI-O 与 Kubernetes 版本绑定。当发布新的 Kubernetes 版本时,版本号会同步。CRI-O 针对 Kubernetes 工作负载进行了优化;从事该工作的工程师了解 Kubernetes 试图做什么,并确保 CRI-O 以最有效的方式完成。由于 CRI-O 没有其他用户,Kubernetes 不必担心 CRI-O 中的破坏性更改。
注意,CRI-O 是 Red Hat 的基于 OpenShift Kubernetes 产品的核心技术。OpenShift 在 Kubernetes 开始运行之前使用 Podman 来安装和配置 CRI-O。OpenShift 镜像构建器集成了 Buildah 功能,使用户能够在他们的 OpenShift 集群内构建镜像。
附录 B. OCI 运行时
本附录描述了与 Podman 等容器引擎一起使用的主要 OCI 运行时。如第一章所述,OCI 运行时 (opencontainers.org) 是由容器引擎(包括 Podman)启动的可执行文件,用于配置 Linux 内核和子系统以运行内核;其最后一步是启动容器。OCI 运行时读取 OCI 运行时规范 JSON 文件,然后配置命名空间、安全控制和 cgroups,最终启动容器进程(图 B.1)。

图 B.1 Podman 执行 OCI 运行时来启动容器。
在本附录中,您将了解正在使用的四个主要 OCI 运行时。--runtime 选项允许您在不同的 OCI 运行时之间切换。在下一个示例中,您将运行相同的容器命令两次,每次使用不同的运行时。在第一个命令中,您使用在 containers.conf 中定义的运行时 crun 来运行容器,因此您不需要指定运行时的路径。
列表 B.1 Podman 使用备用 OCI 运行时 crun
$ podman --runtime crun run --rm ubi8 echo hi ❶
hi
❶ --runtime 选项告诉 Podman 使用 crun OCI 运行时,而不是默认的运行时。
默认运行时在 Linux 机器上 containers.conf 文件中的 [containers] 表下定义。
列表 B.2 修改默认 OCI 运行时
$ grep -iA 3 "Default OCI Runtime" /usr/share/containers/containers.conf
# Default OCI runtime
#
#runtime = "crun" ❶
❶ Podman 在大多数系统上默认使用 crun;在一些较旧的发行版,如 Red Hat Enterprise Linux 上,Podman 默认使用 runc。
在第二个示例中,您使用 OCI 运行时的完整路径,/usr/bin/runc:
$ podman --runtime /usr/bin/runc run –rm ubi8 echo hi
hi
如果您想永久更改默认的 OCI 运行时,您可以在家目录中的 containers.conf 文件中的 [engine] 表中设置运行时选项:
$ cat > ~/.config/containers/containers.conf << EOF
[engine]
runtime="runc"
EOF
$ podman --help | grep -- runc
--runtime string Path to the OCI-compatible binary used to run containers. (default "runc")`
注意:--runtime 选项仅在 Linux 上可用。podman --remote,因此 Podman 在 Mac 和 Windows 上不支持 --runtime 选项,所以您需要在服务器端设置 containers.conf 文件。
查看更多关于 podman(1) 的信息:man podman.。
OCI 运行时正在不断开发和实验中。您可以期待未来在这个领域发生创新。第一个开发的容器运行时,以及事实上的标准,是 runc。
B.1 runc
runc 是原始的 OCI 运行时 (github.com/opencontainers/runc). 当 OCI 最初形成时,Docker 将 runc 捐赠给 OCI,作为 OCI 运行时的默认实现。OCI 继续支持和开发 runc。它用 Golang 编写,还包括 libcontainer 库,该库被许多容器引擎和 Kubernetes 使用。
runc 网站声明,runc 以及所有 OCI 运行时,是一个低级工具,不建议直接由最终用户使用。建议由容器引擎如 Podman 或 Docker 启动。
请记住,容器引擎的工作是拉取容器镜像到主机,配置并挂载根文件系统(rootfs)以在容器内使用,最后在启动 OCI 运行时之前写入 OCI 运行时 JSON 文件。
OCI 运行时规范仅描述了 OCI 运行时使用的 JSON 文件的内容。因为每个 OCI 引擎都支持 runc 命令行,其他 OCI 运行时也采用了相同的 CLI 命令和选项。这使得当一个运行时被容器引擎启动时,替换另一个运行时变得更加容易。表 B.1 显示了 runc 支持的命令,因此所有 OCI 运行时都支持这些命令。|
表 B.1 runc 命令
| 命令 | 描述 |
|---|---|
checkpoint |
检查点一个正在运行的容器 |
create |
创建一个容器 |
delete |
删除容器持有的任何资源,通常与分离容器一起使用 |
events |
显示容器事件,例如 OOM 通知、CPU、内存和 IO 使用统计信息 |
init |
初始化命名空间并启动进程 |
kill |
向容器的 init 进程发送指定的信号(默认:SIGTERM) |
List |
列出由 runc 启动的、给定根目录下的容器 |
pause |
暂停容器内的所有进程 |
ps |
显示容器内运行的进程 |
restore |
从之前的检查点恢复容器 |
resume |
恢复之前暂停的所有进程 |
run |
创建并运行一个容器 |
spec |
创建一个新的规范文件 |
start |
在创建的容器中执行用户定义的进程 |
state |
输出容器的状态 |
update |
更新容器资源限制 |
runc 仍在不断发展,并拥有一个非常活跃的社区。runc 的问题在于它是用 Golang 编写的。Golang 并非为小型、经常执行的应用程序而设计,这种应用需要快速启动、执行 fork/exec 命令并快速退出。在 Golang 中,fork/exec 是一个重量级的操作,尽管 runc 尝试解决这个问题,但最终还是牺牲了一部分性能。然而,“一点”性能损失可能会随着时间的推移而累积,因此 crun 在大规模应用中表现更佳。
B.2 crun
runc 是用 Golang 编写的,是一个非常庞大的可执行文件——大小为 12 兆字节。Golang 是一种非常好的语言,但它没有充分利用共享库。由于这个原因,Golang 可执行文件会占用更多的内存。runc 的大小导致它在容器启动时加载速度较慢。Golang 的另一个问题是它不支持 fork/exec 模型,它在其他语言(例如 C)中的 fork/exec 模型要慢得多。当你启动和停止数百或数千个容器时(例如,在 Kubernetes 集群中),这种速度的缺乏更为重要。像 Podman 这样的容器引擎,也是用 Go 编写的,通常运行时间更长,因此启动时间并不那么重要。像 runc 这样的 OCI 运行时执行时间非常短,并且快速退出。
Giuseppe Scrivano,runc 和 Podman 的贡献者,理解了 runc 中的这些不足,并希望用 C 语言编写一个兼容的 OCI 运行时。他创建了一个非常轻量级的 OCI 运行时,称为 crun。
crun 将自己描述为“*一个快速且轻量级的 OCI 运行时。” (github.com/containers/crun) 它支持与 runc 相同的所有命令和选项,而 crun 可执行文件的大小比 runc 小得多。执行 du -s 命令以比较大小:
$ du -s /usr/bin/runc /usr/bin/crun
14640 /usr/bin/runc
392 /usr/bin/crun
crun 是用 C 编写的,比 Golang 更好地支持 fork 和 exec,因此在启动容器时速度更快。
这也使得它能够轻松地集成到系统上的其他库中,并且有一些实验正在使用 crun 作为处理 OCI 运行时 JSON 文件和启动不同类型的容器(例如,Linux 上的 WASM 和 Windows 容器)的库。crun 还基于 libkrun 有潜力启动基于 KVM 分离的容器。
crun 现在是 Podman 在 Fedora 和 Red Hat Enterprise Linux 9 中使用的默认 OCI 运行时。runc 继续得到支持,并在 Red Hat Enterprise Linux 8 中是默认的 OCI 运行时。
crun 和 runc 是管理使用命名空间分离的传统容器的两个主要 OCI 运行时。这两个项目工作得相当紧密。当在任一 OCI 运行时中发现错误或问题时,它们会迅速在两个项目中修复。有关更多信息,请参阅 crun(1) 手册页:man crun。
B.3 Kata

OCI 运行时也被编写为使用虚拟机隔离,其中主要的例子是 Kata Containers。Kata Container 项目(katacontainers.io)自我宣传如下:“拥有容器的速度,虚拟机的安全性。Kata Containers 是一个开源容器运行时,构建轻量级的虚拟机,可以无缝地集成到容器的生态系统中。”

图 B.2 Kata containers 启动一个轻量级虚拟机,该虚拟机仅运行容器。
Kata 容器使用虚拟机技术来启动每个容器,这与在虚拟机内部启动 VM 和运行 Podman 的方式非常不同。一个标准的虚拟机有一个初始化系统,它会启动各种服务,如日志系统、cron 等。另一方面,Kata 容器启动一个微操作系统,它只运行容器及其支持服务(图 B.2)。由于其唯一目的是启动容器,当容器退出时,这个虚拟机就会消失。
我认为在 VM/虚拟机分离中运行容器比传统的容器分离提供了更好的安全隔离,在传统的容器分离中,容器直接与主机内核通信。VM 分离容器必须首先在虚拟机内部突破隔离,然后找到一种方法突破虚拟机管理程序——只有在这种情况下才会面临攻击主机内核。
虽然 VM 分离容器更安全,但这确实带来了一些缺点。启动 Kata 容器、配置虚拟机管理程序、在虚拟机内部启动内核和其他进程,以及最终启动容器,都需要相当大的开销。虚拟机的内存、CPU 等资源必须预先分配,并且难以更改。在云中在虚拟机内部运行 Kata 通常是不允许的,或者至少更昂贵,因为大多数云服务提供商都不赞成嵌套虚拟化。
最后,也是最重要的,VM 分离容器由于其本质属性,在与其他容器和主机操作系统共享内容方面存在困难。最大的问题是卷。
在传统容器中与主机机器共享内容只是一个绑定挂载,而在 VM 分离容器中,绑定挂载不起作用。由于主机和容器中的进程运行在两个不同的内核上,你需要一个网络协议来共享内容。Kata 容器最初使用 NFS 和 Plan 9 网络文件系统。在这些网络文件系统上读写数据比使用绑定挂载获得的本地文件系统读写要慢得多。
Virtiofs 是一种新的文件系统,它具有网络文件系统的属性,但允许虚拟机访问主机上的文件。它能够在速度上对基于网络的文件系统有显著的提升,同时仍然处于高度开发中。
Kata 容器有两种启动方式。Kata 传统上有一个基于 Podman 支持的runc命令的 OCI 命令行,kata-runtime。你可以在 Linux 机器上通过搜索#kata来查看在containers.conf中定义的路径:
$ grep -A 9 '^#kata' /usr/share/containers/containers.conf
#kata = [
# "/usr/bin/kata-runtime",
# "/usr/sbin/kata-runtime",
# "/usr/local/bin/kata-runtime",
# "/usr/local/sbin/kata-runtime",
# "/sbin/kata-runtime",
# "/bin/kata-runtime",
# "/usr/bin/kata-qemu",
# "/usr/bin/kata-fc",
#]
关于 Kata 容器的底线是,你可以在性能开销的情况下获得更好的安全性。你可以根据工作负载的需求在这些 OCI 运行时之间进行选择。
B.4 gVisor

在这个附录中,我最后要介绍的是 gVisor (gvisor.dev/)。gVisor 网站将自己宣传为“为容器提供高效深度防御的应用内核。”
gVisor 包含一个名为 runsc 的 OCI 运行时,并与 Podman 以及其他容器引擎协同工作。gVisor 项目将自己称为一个应用内核,使用 Golang 编写,实现了 Linux 系统调用接口的大部分功能。它为运行中的应用程序和宿主操作系统之间提供了一个额外的隔离层。Google 工程师编写了 gVisor 的原始版本,并声称 Google Cloud 运行的容器中大部分都使用了 gVisor OCI 运行时。
gVisor 在某种程度上类似于 VM 隔离容器,因为 gVisor 会拦截容器内部几乎所有的系统调用,然后对其进行处理。gVisor 将自己描述为使用 Golang 编写的容器应用内核,限制了宿主内核的访问。同时,它没有像 Kata 那样的嵌套虚拟化问题。
然而,gVisor 引入了额外的 CPU 周期和更高的内存使用,从而带来性能损失。这可能会导致延迟增加、吞吐量减少,或者两者兼而有之。gVisor 还是对系统调用表面的一种独立实现,这意味着许多子系统或特定调用没有像更成熟的实现那样进行优化。
附录 C. 获取 Podman
Podman 是一个用于处理容器的优秀工具,但如何在您的系统上安装它呢?需要哪些软件包才能使其正常工作?本附录涵盖了在您的系统上安装或构建 Podman 的方法。
C.1 安装 Podman
Podman 几乎可以通过所有 Linux 发行版的软件包管理器获得。它也适用于 Mac、Windows 和 FreeBSD 平台。官方 podman.io 网站提供了如何为不同发行版安装 Podman 的新指令,podman.io/getting-started/installation 网站会定期更新。本附录中的大部分内容源自 podman.io 网站,如图 C.1 所示。

图 C.1 Podman 安装说明网站
C.1.1 macOS
由于 Podman 是一个运行 Linux 容器的工具,因此您只能在访问到本地或远程运行的 Linux 机器的情况下在 macOS 桌面上使用它。为了使这个过程变得更容易一些,Podman 包含一个 podman machine 命令,可以自动管理虚拟机。
Homebrew
Mac 客户端可通过 Homebrew 获取 (brew.sh/):
$ brew install podman
Podman 有能力使用 podman machine 命令在您的机器上安装虚拟机并运行 Linux 实例。在 Mac 上,您必须执行以下命令来安装和启动 Linux 虚拟机,才能成功在本地运行容器:
$ podman machine init
$ podman machine start
可选地,您可以使用 podman system connection 命令来设置 SSH 连接到运行 Podman 服务的远程 Linux 机器。
您可以使用以下命令验证安装信息:
$ podman info
Podman 命令在 Mac 上以原生方式运行,但与在虚拟机中运行的 Podman 实例进行通信。
C.1.2 Windows
由于 Podman 是一个运行 Linux 容器的工具,因此您只能在访问到本地或远程运行的 Linux 机器的情况下在 Windows 桌面上使用它。在 Windows 上,Podman 还可以利用 Windows Subsystem for Linux 系统。
Windows 远程客户端
您可以在 github.com/containers/podman/releases 网站上获取最新的 Windows 远程客户端。
安装完成后,您可以使用 podman system connection 命令配置 Windows 远程客户端以连接到 Linux 服务器。您可以在 mng.bz/M0Kn 上了解更多关于此过程的信息。
Windows Subsystem for Linux (WSL) 2.0
请参阅有关安装 WSL 2.0 的 Windows 文档,然后选择包含 Podman 的发行版,其中许多在本节中已描述。或者,podman machine init 命令可以为您自动安装和配置 WSL,在它上面下载和配置 Fedora Core 虚拟机,并为 Podman 远程客户端创建相应的 SSH 连接。
注意:WSL 1.0 不受支持。
C.1.3 Arch Linux 和 Manjaro Linux
Arch Linux 和 Manjaro Linux 使用 pacman 工具安装软件:
$ sudo pacman -S podman
C.1.4 CentOS
Podman 可在 CentOS 7 的默认 Extras 仓库中找到,在 CentOS 8 和 Stream 的 AppStream 仓库中可用:
$ sudo yum -y install podman
C.1.5 Debian
podman 软件包可在 Debian 11(bullseye)仓库及其以后的版本中找到:
$ sudo apt-get -y install podman
C.1.6 Fedora
$ sudo dnf -y install podman
C.1.7 Fedora-CoreOS, Fedora Silverblue
Podman 在这些发行版中预装。无需安装。
C.1.8 Gentoo
$ sudo emerge app-emulation/podman
C.1.9 OpenEmbedded
Podman 及其依赖项的 BitBake 脚本在 meta-virtualization 层中可用 (mng.bz/aPzB)。将层添加到您的 OpenEmbedded 构建环境,并使用以下命令构建 Podman:
$ bitbake podman
C.1.10 openSUSE
sudo zypper install podman
C.1.11 openSUSE Kubic
openSUSE Kubic 发行版内置了 Podman。无需安装。
C.1.12 Raspberry Pi OS arm64
Raspberry Pi OS 使用标准的 Debian 仓库,因此它与 Debian 的 arm64 仓库完全兼容:
$ sudo apt-get -y install podman
C.1.13 Red Hat Enterprise Linux
RHEL7
确保您拥有 RHEL7 订阅,然后启用 extras 频道并安装 Podman:
$ sudo subscription-manager repos --enable=rhel-7-server-extras-rpms
$ sudo yum -y install podman
注意:RHEL7 已不再接收 Podman 软件包的更新,除非是安全修复。
RHEL8
Podman 包含在 container-tools 模块中,与 Buildah 和 Skopeo 一起:
$ sudo yum module enable -y container-tools:rhel8
$ sudo yum module install -y container-tools:rhel8
RHEL9(及其以后版本)
$ sudo yum install podman
C.1.14 Ubuntu
podman 软件包可在 Ubuntu 20.10 及更高版本的官方仓库中找到:
$ sudo apt-get -y update
$ sudo apt-get -y install podman
C.2 从源代码构建
我通常建议人们获取 Podman 的打包版本,因为成功在 Linux 上运行 Podman 需要安装额外的工具,例如 conmon(容器监控器)、containernetworking-plugins(网络配置)和 containers-common(通用配置)。虽然从源代码构建 Podman 的过程并不复杂,但依赖项列表因 Linux 发行版而异。您始终可以在以下 Podman 页面上找到最新说明:mng.bz/gRDE。
C.3 Podman Desktop
此外,还有一个用于浏览、管理和检查来自不同容器引擎的容器和镜像的 GUI,Podman Desktop,可在 github.com/containers/podman-desktop 获取。Podman Desktop 提供了同时连接到多个引擎的能力,并提供了一个统一的界面。这是一个相对较新的项目,正处于快速发展中,因此请期待一些粗糙的边缘。
为了提供一些背景信息,2021 年 9 月,Docker Inc. 宣布他们将开始对之前免费的 macOS Docker Desktop 版本收费。Docker 的公告导致许多人转向并寻找替代品。
摘要
-
Podman 是一个运行 Linux 容器的工具,因此它仅在 Linux 上运行。
-
Podman 可在大多数主要 Linux 发行版的默认软件包仓库中找到。
-
Podman 可作为 Mac 和 Windows 上的远程客户端使用,连接到本地或远程 Linux 服务器。
-
Podman 为 macOS 和 Windows 上的 Linux VM 管理提供了一个特殊的命令。
-
Podman 可以从源代码构建,但需要许多其他工具才能成功运行。
-
Podman Desktop 是 Docker Desktop 的一个替代方案。
附录 D. 为 Podman 做出贡献
我最喜欢的开源事情是社区的努力。能够为项目做出贡献,并且更好地,能够让人们为你的项目做出贡献,这是很棒的。我喜欢用的类比来自《格林童话》中的故事“精灵”(sites.pitt.edu/~dash/grimm039.xhtml):
一个鞋匠,没有任何过错,变得如此贫穷,以至于他只有足够的皮革做一双鞋。他一个晚上剪出了鞋样,然后上床睡觉,打算第二天早上完成。他心地善良,睡得很平静,向上帝祈祷,然后入睡。第二天早上,在祈祷之后,他正准备回到工作中,却发现他的工作台上鞋子已经完全做好了。
这个故事继续讲述了一对每晚来访并完成鞋子的精灵。我认为这就是开源工作的方式。基本上,进行小贡献、错误报告、错误修复、文档修复、功能请求以及宣传项目的人都是精灵。有时我甚至睡觉,当我醒来时,我会发现有人已经修复了我前一天晚上试图解决的问题!有时这些精灵会成长为维护者。一些小的贡献随着时间的推移而增长,这些开发者最终成为 Podman 团队的核心成员。我们甚至雇佣了一些人。
D.1 加入社区
每一点小的改变都有助于使项目变得更好。当我与大学生谈论开源时,我会告诉他们他们作为学生时所没有的独特机会。他们可以为软件项目或产品做出贡献,并将其列在简历上。当面试实习生或工作时,简历上有几个 github.com 的贡献是非常令人印象深刻的。
Podman 及其底层技术一直在寻找新的贡献(图 D.1)。任何大小的贡献都是受欢迎的——从手册中的拼写错误到完整的功能。你不需要是软件开发者才能做出贡献。我们总是在寻找帮助文档、podman.io 的网页设计以及软件方面的帮助。许多伟大的想法都来自产品的用户。仅仅报告一个错误或指出你不满意的地方,都可能带来改进项目的全新想法。我经常要求那些使用 Podman 搭建复杂环境的人撰写博客,这样其他人也可以从中学习。

图 D.1 Podman 社区页面 (podman.io/community)
Podman 是一个包容性的社区,就像 github.com/containers 的所有项目一样。容器项目的行为准则声明位于mng.bz/5mEB,内容如下:
作为github.com/containers 仓库下项目的贡献者和维护者,为了促进一个开放和欢迎的社区,我们承诺尊重通过报告问题、发布功能请求、更新文档、提交拉取请求或补丁以及其他活动向任何容器项目做出贡献的所有人。
D.2 Podman 在 github.com
问题、讨论和拉取请求都位于github.com/containers/podman 仓库中(图 D.2)。截至本文撰写时,该项目已有超过 1,200 个分支和 12,000 个星标。总的来说,这是一个非常活跃的项目。

图 D.2 Podman 的 github 页面 (github.com/containers/podman)
您还可以在 libera.chat 上的#podman 频道直接与核心维护者进行交流。该 IRC 频道也链接到 Matrix 上的#podman:matrix.org (https://matrix.to/#/#podman:matrix.org) 和 Podman Discord (discord.com/invite/x5GzFF6QH4) 以便进行网络访问。
您还可以通过发送电子邮件到 podman-join@lists.podman.io 加入一个低流量的邮件列表。最后,您可以在 Twitter 上关注@podman_io 或关注我@rhatdan。
附录 E. macOS 上的 Podman
本附录涵盖
-
在 macOS 上安装 Podman
-
使用
podman machine init命令下载已安装 Podman 服务的虚拟机 -
使用
podman命令与虚拟机中运行的 Podman 服务进行通信 -
使用
podman machinestart/stop 命令启动或停止虚拟机
Podman 是一个用于启动 Linux 容器的工具。Linux 容器需要一个 Linux 内核。尽管我很想说服全世界像我用一样迁移到 Linux 桌面,但大多数用户在 macOS 和 Windows 操作系统上工作——也许甚至包括您。如果您使用 Linux 桌面,太好了!如果您不使用 macOS 机器,请随意跳过本附录。
由于您没有跳过本附录,我将假设您希望在无需 ssh 登录 Linux 机器的情况下创建 Linux 容器。您可能希望使用本地的软件开发工具,并保持开发本地化。
实现这一目标的一种方法是在 Linux 机器上作为服务运行 Podman,并使用 podman --remote 命令与该服务进行通信。Podman 提供了 podman system connection 命令来配置 Podman 如何与 Linux 机器通信。然而,这种方法的问题是一个繁琐的过程,需要许多手动步骤。请参阅此网页以获取有关此过程的最新教程:mng.bz/69ro。
更好的方法是用一个新的命令 podman machine,它封装了所有这些步骤,并改善了您管理用于 podman-remote 的 Linux 机器的体验。在本附录中,您将学习如何在 macOS 上安装 Podman,然后使用 podman machine 命令安装、配置和管理虚拟机,以便您可以使用本地的 Podman 客户端启动容器。
在 macOS 上启动 Podman 的第一步是安装它。macOS 客户端可通过 Homebrew 获取(brew.sh/)。
注意:Homebrew 自称是“...安装 UNIX 工具最容易且最灵活的方式,Apple 没有将其包含在 macOS 中” (docs.brew.sh/Manpage)。
Homebrew 是在您的 macOS 上安装开源软件的最佳方式。如果您目前尚未在 macOS 上安装 Homebrew,请在终端中打开它,并在提示符下使用以下命令进行安装:
$ /bin/bash -c "$(curl -fsSL
➥ https:/ /raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
现在运行以下 brew 命令,将仅支持 --remote 的 Podman 剪减版安装到 /opt/homebrew/bin 目录:
$ brew install podman
如果您无法访问 Linux 虚拟机或远程 Linux 服务器,Podman 允许您使用 podman machine 命令创建一个本地运行的虚拟机。它通过创建和配置一个启用了 Podman 服务的虚拟机来简化这一过程。
注意:如果您已经有一个现有的 Linux 机器,您可以使用 Podman 系统连接命令设置与这些机器的连接。
E.1 使用 podman machine
podman machine 命令允许您从互联网上拉取虚拟机并启动它、停止它或删除它。此虚拟机已预配置了 Podman 服务。此外,此命令创建 SSH 连接并将此信息添加到 podman system connection 数据存储中,极大地简化了设置 podman-remote 环境的过程。表 E.1 列出了用于管理 Podman 虚拟机生命周期的所有 podman machine 子命令。第一步是使用 podman machine init 命令在您的系统上初始化一个新的虚拟机,以下章节将描述此命令。
表 E.1 Podman machine 命令
| 命令 | 描述 |
|---|---|
init |
初始化一个新的虚拟机。 |
list |
列出虚拟机。 |
rm |
删除虚拟机。 |
ssh |
通过 ssh 登录到虚拟机。这对于进入虚拟机并运行原生 Podman 命令很有用。一些 Podman 命令不支持远程操作,并且您可能希望在虚拟机内部更改一些配置。 |
start |
启动虚拟机。 |
stop |
停止虚拟机。如果您没有运行容器,您可能想关闭虚拟机以节省系统资源。 |
E.1.1 podman machine init
podman machine init 命令在您的 macOS 系统上下载和配置虚拟机(图 E.1)。默认情况下,如果之前未下载,它将下载最新的发布版 fedora-coreos 镜像(getfedora.org/en/coreos)。Fedora CoreOS 是一个专为运行容器而设计的最小化操作系统。
注意:虚拟机相对较大,下载可能需要几分钟。

图 E.1 podman machine init 命令拉取虚拟机并配置 SSH
列表 E.1 Podman 在 Mac 上下载虚拟机并准备执行
$ podman machine init
Downloading VM image: fedora-coreos-35.20211215.2.
➥ 0-qemu.x86_64.qcow2.xz ❶
[=========>------------------------------------------------] 111.0MiB /
➥ 620.7MiB
Downloading VM image: fedora-coreos-35.20211215.2.0-qemu.x86_64.qcow2.xz: done
Extracting compressed file ❷
❶ Podman 在您的系统上查找并下载最新的 fedora-coreos qcow 镜像。
❷ 下载镜像后,Podman 解压缩镜像并配置 qemu 以准备执行。它还配置了到 Podman 系统连接数据存储的 SSH 连接。
Podman 预配置了虚拟机使用的内存、磁盘大小和 CPU 数量。这些值可以使用 init 子命令选项进行配置。表 E.2 描述了这些选项。
表 E.2 Podman machine init 命令选项
| 选项 | 描述 |
|---|---|
--cpus uint |
CPU 数量(默认为 1) |
--disk-size uint |
磁盘大小(以 GB 为单位,默认为 10)。这是一个重要的设置,因为它限制了虚拟机内部可以使用的容器和镜像数量。如果您有空间,我建议增加该字段。 |
--image-path string |
qcow 图像的路径(默认为 testing)。Podman 有两个内置的 Fedora CoreOS 图像可以拉取:testing 和 stable。您还可以选择其他操作系统和虚拟机进行下载,但虚拟机必须支持 CoreOS/Ignition 文件 (coreos.github.io/ignition/))。 |
--memory integer |
以 MB 为单位的内存(默认为 2048)。虚拟机运行需要一定量的内存,并且根据您在虚拟机内想要运行的容器,可能需要更多内存。 |
一旦 podman machine init 完成下载和安装虚拟机,您可以使用 podman machine list 命令查看虚拟机。注意 * 表示默认要使用的虚拟机。podman machine 命令目前一次只能运行一个虚拟机:
$ podman machine list
NAME VM TYPE CREATED LAST UP CPUS
➥ MEMORY DISK SIZE
podman-machine-default qemu 2 minutes ago 2 minutes ago 1
➥ 2.147GB 10.74GB
在下一节中,您将检查自动创建的 SSH 连接。
E.1.2 Podman machine SSH 配置
podman machine init 命令为操作系统提供 Ignition 配置,其中包含 core 用户的 SSH 密钥。然后 Podman 在客户端机器上为无根和根模式添加 SSH 连接,配置用户账户,并在虚拟机内添加所需的软件包和配置。SSH 配置允许从客户端对 core 和 root 账户执行无密码 SSH 命令。podman machine init 命令还配置了 Podman 系统连接信息(见第 9.5.4 节)。系统连接数据库在虚拟机内的根用户和无根用户中进行了配置。如果没有现有的连接,podman machine init 命令将新创建的连接设置为默认连接。
您可以使用 podman system connection list 命令检查所有连接。默认连接 podman-machine-default 是无根连接:
$ podman system connection list
Name URI
Identity Default
podman-machine-default
➥ ssh:/ /core@localhost:50107/run/user/501/podman/podman.sock
➥ /Users/danwalsh/.ssh/podman-machine-default true
podman-machine-default-root
➥ ssh:/ /root@localhost:50107/run/podman/podman.sock
➥ /Users/danwalsh/.ssh/podman-machine-default false
有时您想要执行的容器需要 root 权限,并且不能在无根模式下运行。为此,您可以使用 podman system connection default 命令修改系统连接,使其默认为根模式服务:
$ podman system connection default podman-machine-default-root
再次查看连接以确认默认连接现在是 podman-machine-default-root:
$ $ podman system connection list
Name URI
➥ Identity Default
podman-machine-default
➥ ssh:/ /core@localhost:50107/run/user/501/podman/podman.sock
➥ /Users/danwalsh/.ssh/podman-machine-default false
podman-machine-default-root
➥ ssh:/ /root@localhost:50107/run/podman/podman.sock
➥ /Users/danwalsh/.ssh/podman-machine-default true
n-machine-default ssh:/ /root@localhost:38243/run/podman/podman.sock
现在,所有 Podman 命令都直接连接到在 root 账户内运行的 Podman 服务。再次使用 Podman 系统连接默认命令将默认连接更改为无根用户:
$ podman system connection default podman-machine-default
如果您此时尝试运行 Podman 容器,它将失败,因为虚拟机实际上并没有运行。您需要启动虚拟机。
E.1.3 启动虚拟机
在添加虚拟机并将特定连接设置为默认连接后,尝试运行一个 podman 命令:
$ podman version
Cannot connect to Podman. Please verify your connection to the Linux system
using `podman system connection list`, or try `podman machine init` and
`podman machine start` to manage a new Linux VM
Error: unable to connect to Podman. failed to create sshClient: Connection
to bastion host (ssh:/ /root@localhost:38243/run/podman/podman.sock)
failed.: dial tcp [::1]:38243: connect: connection refused
正如错误信息所指出的,虚拟机没有运行,必须启动。
您可以使用 podman machine start 命令启动单个虚拟机。Podman 一次只能运行一个虚拟机。默认情况下,启动命令启动默认虚拟机。如果您有多个虚拟机并想启动不同的虚拟机,您可以指定可选的机器名称:
$ podman machine start
INFO[0000] waiting for clients...
INFO[0000] listening tcp:/ /127.0.0.1:7777
INFO[0000] new connection from @ to /run/user/3267/podman/
➥ qemu_podman-machine-default.sock
Waiting for VM ...
macOShine "podman-machine-default" started successfully
您现在可以开始在运行 Podman 服务的 Linux 箱子上运行 Podman 命令了。运行 podman version 命令以确认客户端和服务器配置正确。如果不正确,Podman 命令应指导您配置系统:
$ podman version
Client:
Version: 4.1.0
API Version: 4.1.0
Go Version: go1.18.1
Built: Thu May 5 16:07:47 2022
OS/Arch: darwin/arm64
Server:
Version: 4.1.0
API Version: 4.1.0
Go Version: go1.18
Built: Fri May 6 12:16:38 2022
OS/Arch: linux/arm64
现在,您可以直接在 macOS 上使用之前章节中学到的 Podman 命令。当您完成在 VM 中与容器的操作后,您可能应该关闭它以节省资源。
注意:Podman 也在 M1 arm64 机器以及 x86 平台上得到支持。podman machine init 下载匹配架构的 VM,允许您为该架构构建镜像。关于在其他架构上构建镜像的支持正在写作时进行中。
E.1.4 停止 VM
podman machine stop 命令允许您关闭 VM 内的所有容器以及 VM 本身:
$ podman machine stop
当您需要再次开始使用容器时,使用 podman machine start 命令启动 VM。
注意:所有的 podman machine 命令在 Linux 上同样有效,并允许您同时测试 Podman 的不同版本。Linux 上的 Podman 是完整的命令;因此,您需要使用 --remote 选项与由 Podman machine 启动的 VM 内运行的 Podman 服务进行通信。在非 Linux 平台上,不需要 --remote 选项,因为客户端已经预先配置为 --remote 模式。
摘要
-
Linux 容器需要 Linux 内核,这意味着在 macOS 上运行容器需要运行 Linux 的虚拟机。
-
在 macOS 上运行的 Podman 不在本地运行容器。实际上,Podman 命令是与运行在 Linux 机器上的 Podman 服务进行通信。
-
podmanmachineinit命令将 Fedora CoreOS VM 下载并安装到您的平台上,该 VM 正在运行 Podman 服务。 -
podmanmachineinit命令还设置了允许 Podman 远程客户端与 VM 内的 Podman 服务器通信所需的 SSH 环境。
附录 F. Windows 上的 Podman
本附录涵盖
-
在 Windows 上安装 Podman
-
使用
podmanmachineinit命令创建运行 Podman 的基于 Fedora 的 WSL 2 发行版 -
在 Windows 上使用
podman命令与在 WSL 2 实例中运行的 Podman 服务进行通信 -
使用
podmanmachinestart/stop命令启动或停止 WSL 2 实例
Podman 是一个用于启动 Linux 容器的工具。Linux 容器需要一个 Linux 内核。尽管我很想说服全世界像我自己一样迁移到 Linux 桌面,但大多数用户在 Mac 和 Windows 操作系统上工作——也许甚至是你。如果你使用 Linux 桌面,太好了!如果你不使用 Windows 机器,请随意跳过本附录。
由于你没有跳过这个附录,我假设你想要创建 Linux 容器,而不必通过 ssh 登录到 Linux 机器并在那里创建容器。你很可能想使用本地的软件开发工具,并将它们的软件保留在本机上。
在 Linux 上,Podman 可以作为服务运行,允许远程连接来启动容器。然后,从另一台系统,可以使用 podman --remote 命令与远程 Podman 服务进行通信以启动容器。
此外,你可以使用 podman system connection 来配置 podman --remote 以通过 SSH 与运行 Podman 服务的远程 Linux 机器进行通信,而不需要在每个命令中提供 URL。所有这些的问题在于,有人必须配置远程机器以使用正确的 Podman 服务版本,然后你必须配置 SSH 会话。
认识到这种体验对于 Windows 桌面上的 Podman 新用户来说并不理想,Podman 添加了一个新的命令:podman machine。podman machine 命令使得创建和管理基于 WSL 2 的 Linux 环境变得容易,Podman 预先安装并配置好。Windows 上的 Podman 命令实际上是一个精简版的 Podman 命令,只支持 podman --remote。在本附录中,你将学习如何在 Windows 机器上安装 Podman,然后使用 podman machine 命令来安装、配置和管理 WSL 2 实例。
F.1 第一步
Windows 上的 podman machine 命令接受与 Linux 和 Mac 上使用的所有相同命令,具有非常相似的行为。尽管如此,由于 Windows 的底层后端基于 Windows Subsystem for Linux (docs.microsoft.com/en-us/windows/wsl/) 而不是 VM,因此在其他操作系统中有一些差异。
WSL 2 涉及使用 Windows Hyper-V 虚拟机管理程序;然而,与基于标准 VM 的方法不同,WSL 2 在用户安装的每个 Linux 发行版实例之间共享相同的 VM 和 Linux 内核实例。例如,如果你创建了两个 WSL 2 发行版,并且在每个实例上运行 dmesg,你会看到相同的输出,因为相同的内核在托管这两个实例。
注意:WSL 1 与 Podman 不兼容;您必须将您的 Windows 机器升级到支持 WSL 2 的操作系统版本。对于 x64 系统,您需要 Windows 版本 1,903 或更高版本,构建号 18,362 或更高。对于 arm64 系统,您需要 Windows 版本 2004 或更高版本,构建号 19,041 或更高。
使用 WSL 2 运行 Podman 可以在主机和所有运行实例之间实现高效的资源共享,但牺牲了较低的隔离性。请注意,podman machine 命令与您运行的任何其他发行版共享相同的内核,因此在任何发行版中操作任何内核级设置(例如,网络接口和 netfilter 策略)时请谨慎,因为您可能会无意中影响 Podman 执行的容器。
F.1.1 前提条件
Windows 版本的 Podman 需要 Windows 10(构建号 19,041 或更高)或 Windows 11。由于 WSL 2 使用虚拟机管理程序,您的计算机必须启用虚拟化指令(例如,Intel VT-x 或 AMD-V)。此外,虚拟机管理程序需要二级地址转换(SLAT)支持。最后,您的系统必须具有互联网连接或所有要由 podman machine 获取的软件的离线副本。
注意:如果您在任何时候遇到错误 0x80070003 或 0x80370102(或任何表示虚拟机无法启动的错误),您很可能已禁用虚拟化。检查您的 BIOS(或 WSL 2 实例)设置以验证 VT-x/AMD-V/WSL 2 实例和 SLAT 是否已启用。
虽然不是必需的,但强烈建议安装 Windows Terminal(与标准 CMD 命令应用或 PowerShell 相比),因为 Windows 11 的未来版本默认包含它。除了拥有现代终端功能,如透明剪切和粘贴以及分格屏幕外,它还提供了直接 WSL 和 PowerShell 集成,使得在不同环境之间切换变得容易。您可以通过 Windows 商店或 winget 来安装它:
PS C:\Users\User> winget install Microsoft.WindowsTerminal
F.1.2 安装 Podman
安装 Podman 很简单。访问 Podman 网站或 Podman GitHub 仓库,然后在“发布”部分下载最新的 Podman MSI Windows 安装程序(图 F.1;github.com/containers/podman/releases)。

图 F.1 下载和运行 Podman 安装程序
运行安装程序后,打开一个终端(如果您按照建议安装了 Windows Terminal,请使用 wt 命令),并执行您的第一个 podman 命令(图 F.2)。

图 F.2 在 Windows Terminal 中运行的 Podman 命令
自动 WSL 安装
如果您的 Windows 系统上没有安装 WSL,Podman 会为您安装它。只需执行podman machine init命令(如图 F.3 所示)来创建您的第一个机器实例,然后 Podman 会提示您安装 WSL 的权限。WSL 安装过程需要重启,但会继续执行机器创建过程。(请确保等待几分钟,以便终端重新启动并安装。)如果您更喜欢手动安装,请参阅 WSL 安装指南:docs.microsoft.com/en-us/windows/wsl/install。

图 F.3 podman machine init 启动 WSL 安装。
F.2 使用 podman machine
通过使用podman machine命令,可以轻松设置和使用 Linux 环境。在 Windows 上,这些命令创建和管理 WSL 2 发行版,包括从互联网下载基本 Linux 镜像和软件包,并为您设置一切。WSL 2 发行版预先配置了 Podman 服务,并将 SSH 连接配置自动添加到podman system connection数据存储中。最终结果是您可以在 Windows 桌面上轻松运行 Podman 命令,就像在 Linux 系统上一样。表 F.1 列出了用于管理 WSL 2 支持的 Linux 环境生命周期的所有podman machine命令。
表 F.1 podman machine命令
| 命令 | 描述 |
|---|---|
init |
初始化一个新的基于 WSL 2 的机器实例。 |
list |
列出 WSL 2 机器。 |
rm |
删除一个 WSL 2 机器实例。 |
set |
设置可更新的 WSL 机器设置。 |
ssh |
通过ssh连接到 WSL 2 机器实例。这对于进入 WSL 2 实例并运行原生 Podman 命令很有用。一些 Podman 命令不支持远程执行,您可能需要在 WSL 2 实例内部更改一些配置。 |
start |
启动一个 WSL 2 机器实例。 |
stop |
停止一个 WSL 2 机器实例。如果您没有运行容器,您可能想要停止以节省系统资源。 |
在安装 Podman(见 F.1.2 节)后,第一步是在您的系统上创建一个 WSL 2 机器实例。您将使用以下部分中描述的podman machine init命令。
F.2.1 podman machine init
如图 F.4 所示,您可以使用podman machine init命令来自动安装一个基于 WSL 2 的 Linux 环境,该环境托管 Podman 服务以运行容器。默认情况下,podman machine init会下载一个已知的兼容版本的 Fedora 来创建 WSL 2 实例(getfedora.org)。使用 Fedora 是因为它与 Podman 集成良好,并且是大多数 Podman 核心开发者使用的操作系统。

图 F.4 podman machine init 命令创建 WSL 2 发行版并配置 SSH 连接。
注意:除了基本镜像外,还需要下载和安装一些软件包,这可能需要几分钟才能完成。
以下是从运行 podman machine init 命令中得到的简化输出:
PS C:\Users\User> podman machine init
Downloading VM image: fedora-35.20211125-x86_64.tar.xz: done
Extracting compressed file
Importing operating system into WSL (this may take 5+ minutes on a new WSL
➥ install)...
Installing packages (this will take awhile)...
Fedora 35 - x86_64 5.5 MB/s | 79 MB 00:14
Complete!
Configuring system...
Generating public/private ed25519 key pair.
Machine init complete
To start your machine run:
podman machine start
表 F.2 解释了允许你自定义默认设置的 init 选项。
表 F.2 podman machine init 命令选项
| 选项 | 描述 |
|---|---|
--cpus uint |
未使用 |
--disk-size uint |
未使用 |
--image-path string |
在 Windows 上,此选项指的是 Fedora 发行版编号(例如,35)。与 Linux 和 Mac 一样,你也可以指定一个任意 URL 或文件系统位置,使用自定义镜像,但 Podman 预期的是 Fedora 衍生的布局。 |
--memory integer |
未使用 |
--rootful |
确定此机器实例是否应该是 rootful 或 rootless |
注意:表 F.2 中指定的物理限制(例如,CPU、内存和磁盘)目前在 Windows 上被忽略,因为 Windows Subsystem for Linux (WSL) 后端会根据不同的发行版动态调整和共享资源。如果你需要限制资源,你可以在你的用户 .wslconfig 文件中配置这些限制。然而,由于它们共享相同的底层虚拟机,因此它们适用于所有 WSL 2 发行版。
F.2.2 Podman machine SSH 配置
podman machine init 命令在 WSL 2 实例中创建一个账户。默认情况下,Fedora 中的用户是 user@localhost。Podman 在客户端机器上配置 SSH,以及 WSL 2 实例中的新用户账户和 root。SSH 配置允许从客户端对 user 和 root 账户执行无密码 SSH 命令。podman machine init 命令还配置了 Podman 系统连接信息(见第 9.5.4 节)。系统连接数据库为 WSL 2 实例中的 rootful 用户和 rootless 用户配置。如果你没有任何现有的连接,podman machine init 命令会创建并设置一个默认的 rootless 用户连接到你的 WSL 2 实例。
你可以使用 podman system connection list 命令检查所有连接。默认连接 podman-machine-default 是 rootless 连接:
PS C:\Users\User> podman system connection ls
Name URI Identity
➥ Default
podman-machine-default ssh:/ /user@localhost:57051.. podman-machine-
➥ default true
podman-machine-default-root ssh:/ /root@localhost:57051.. podman-machine-
➥ default false
有时,你想要执行的容器需要 root 权限,并且不能在 rootless 模式下运行。你可以通过切换创建的机器实例的默认模式来将默认连接更改为 rootful。使用 podman machine set 命令将默认服务修改为 rootful:
PS C:\Users\User> podman machine set --rootful
再次查看连接以确认默认连接现在是 podman-machine-default-root:
PS C:\Users\User> podman system connection ls
Name URI Identity
➥ Default
podman-machine-default ssh:/ /user@localhost:57051..
➥ podman-machine-default false
podman-machine-default-root ssh:/ /root@localhost:57051..
➥ podman-machine-default true
现在,所有 Podman 命令都直接连接到在 root 账户中运行的 Podman 服务。再次使用 podman machine set 命令将默认连接更改为 rootless 用户:
PS C:\Users\User> podman machine set --rootful=false
如果你现在尝试运行 Podman 容器,它会失败,因为机器实例实际上并没有运行。你需要启动机器实例。
F.2.3 启动 WSL 2 实例
尝试执行 podman version 命令失败,因为 WSL 2 实例尚未启动:
PS C:\Users\User> podman version
Cannot connect to Podman. Please verify your connection to the Linux system
using `podman system connection list`, or try `podman machine init` and
`podman machine start` to manage a new Linux Linux VM
Error: unable to connect to Podman. failed to create sshClient: Connection
to bastion host (ssh:/ /root@localhost:38243/run/podman/podman.sock)
failed.: dial tcp [::1]:38243: connect: connection refused
正如错误信息所指出的,虚拟化 Linux 环境(WSL 2 机器实例)尚未运行,必须启动。
你可以使用 podman machine start 命令启动单个 WSL 2 实例。默认情况下,它会启动默认的 WSL 2 实例:podman-machine-default。如果你有多个 WSL 2 实例并且想要启动不同的 WSL 2 实例,你可以为 podman machine start 命令指定可选的机器名称:
PS C:\Users\User> podman machine start
Starting machine "podman-machine-default"
This machine is currently configured in rootless mode. If your containers
require root permissions (e.g. ports < 1024), or if you run into compatibility
issues with non-podman clients, you can switch using the following command:
podman machine set --rootful
API forwarding listening on: npipe:////./pipe/docker_engine
Docker API clients default to this address. You do not need to set
DOCKER_HOST.
Machine "podman-machine-default" started successfully
现在,你已准备好开始在主机上运行 Podman 命令,该主机与在 WSL 2 实例中运行的 Podman 服务进行通信。运行 podman version 命令以确认客户端和服务器配置正确。如果不正确,Podman 命令将指导你如何配置系统:
PS C:\Users\User> podman version
Client: Podman Engine
Version: 4.0.0-dev
API Version: 4.0.0-dev
Go Version: go1.17.1
Git Commit: bac389043f268e632c45fed7b4e88bdefd2d95e6-dirty
Built: Wed Feb 16 00:33:20 2022
OS/Arch: windows/amd64
Server: Podman Engine
Version: 4.0.1
API Version: 4.0.1
Go Version: go1.16.14
Built: Fri Feb 25 13:22:13 2022
OS/Arch: linux/amd64
现在,你可以在 Windows 上直接使用之前章节中学到的 Podman 命令。请确保你理解 Windows 上的 Podman 等同于 podman --remote 远程与 WSL 2 实例内部的 Podman 服务进行通信。
F.2.4 使用 podman machine 命令
在你的机器实例运行后,你可以在 PowerShell 提示符下执行 Podman 命令,就像在 Windows 内运行一样:
PS C:\Users\User> podman run ubi8-micro date
Thu Jan 6 05:09:59 UTC 2022
关闭 WSL 2 实例
当你在系统上完成容器使用后,你可能想要关闭 WSL 2 实例以节省系统资源。使用 podman machine stop 命令来关闭 WSL 2 实例内的所有容器以及 WSL 2 实例:
PS C:\Users\User> podman machine stop
当你需要再次开始使用容器时,使用 podman machine start 命令启动 WSL 2 实例。
注意 所有 podman machine 命令在 Linux 上也有效,并允许你同时测试 Podman 的不同版本。Linux 上的 Podman 是完整的命令;因此,你需要使用 --remote 选项与由 podman machine 命令启动的 WSL 2 实例内部的 Podman 服务进行通信。在非 Linux 平台上,不需要 --remote 选项,因为客户端已预先配置为 --remote 模式。
列出机器
你可以使用 podman machine ls 命令列出可用的机器实例。此命令在 Windows 上返回的值反映了当前的活动使用情况,而不是固定的资源限制,这与 Mac 和 Linux 上的情况不同。磁盘存储反映了分配给每个机器实例的磁盘空间。CPU 值传达了 Windows 主机上的 CPU 数量(除非由 WSL 限制),每个机器实例重复一次。返回的内存值也重复(略有变化,这是由于采样变异性),反映了所有使用分布的 Linux 内核使用的总内存量(因为它共享)。换句话说,对于总使用量,你将磁盘大小相加,但不包括内存和 CPU。
PS C:\Users\User> podman machine ls
NAME VM TYPE CREATED LAST UP CPUS
➥ MEMORY DISK SIZE
podman-machine-default wsl 3 days ago Running 4
➥ 528.4MB 845.2MB
other wsl 4 minutes ago Running 4
➥ 524.5MB 778MB
在 WSL 提示符下使用 Podman
除了 podman machine ssh 命令外,您还可以使用 WSL 提示符访问 podman machine guest。如果您正在运行 Windows Terminal,则 podman machine guests(以 Podman 为前缀的名称)位于向下箭头下拉菜单中。或者,您可以通过使用 wsl 命令并指定支持的分发名称,从任何 PowerShell 提示符进入 WSL shell。例如,podman machine init 创建的默认实例是 podman-machine-default。您可以使用这两种方法来管理虚拟机并在功能齐全的 Linux shell 环境中执行 Podman 命令:
PS C:\Users\User> wsl -d podman-machine-default
[root@WIN10PRO /]# podman version
Client: Podman Engine
Version: 4.0.1
API Version: 4.0.1
Go Version: go1.16.14
Built: Fri Feb 25 13:22:13 2022
OS/Arch: linux/amd64
更新 Fedora
由于 Windows 机器实现基于 Fedora,而不是 Fedora CoreOS,因此修复和增强不是自动的。它们必须在虚拟机中使用 Fedora 的包管理命令 dnf 明确启动。此外,升级到 Fedora 的新版本需要导出您需要保留的所有数据,并使用 podman machine init 创建第二个机器实例(或在执行 podman machine rm 命令后替换现有的实例)。
注意 目前,在 WSL 内运行 Fedora CoreOS 难以实现,因此默认选择 Fedora。如果将来 Windows 对 CoreOS 的支持发生变化,podman machine 将迁移到 Fedora CoreOS。
例如,要获取在 podman guest` 上运行的 Fedora 版本的最新软件包,请执行以下命令:
PS C:\Users\User> podman machine ssh dnf upgrade -y
Warning: Permanently added '[localhost]:52581' (ED25519) to the list of
known hosts.
Last metadata expiration check: 1:18:35 ago on Wed Jan 5 21:13:15 2022.
Dependencies resolved.
...
Complete!
高级停止和重启
通常,要停止和重启 Podman,您会使用相应的 podman machine stop 和 podman machine start 命令。停止机器是首选方法,因为系统服务可以干净地停止。然而,在某些情况下,您可能希望强制重启 WSL 设施,包括共享的 Linux 内核,即使在机器停止后,内核仍然保持活跃。要杀死与 WSL 发行版相关的所有进程,请使用 wsl --terminate <machine name> 命令。要关闭 Linux 内核,杀死所有正在运行的发行版,请使用 wsl --shutdown 命令。在发出这些命令后,您可以使用标准的 podman machine start 命令重新启动您的实例:
PS C:\Users\User> wsl --shutdown
PS C:\Users\User> podman machine start
Starting machine...
Machine "podman-machine-default" started successfully
摘要
-
Linux 容器需要一个 Linux 内核,这意味着在 Mac 或 Windows 平台上运行容器需要运行 Linux 的虚拟机。
-
Windows 上的 Podman 不在本地运行容器。实际上,Podman 命令是
podman--remote与在 Linux 机器上运行的 Podman 服务通信,该机器由 WSL 2 支持。 -
podmanmachineinit命令将下载并安装一个虚拟 Linux 环境到您的平台上,该环境运行 Podman 服务。 -
podmanmachineinit命令还设置了 SSH 环境,以便 Podman 远程客户端能够与 WSL 2 实例内部的 Podman 服务器通信。 -
在 WSL 上运行的 Windows Podman 是完整的 Podman 命令。WSL 在 Linux 内核下运行 Podman 命令,尽管它感觉就像是在 Windows 机器上原生运行一样。


浙公网安备 33010602011771号