1.3 有了Docker为什么还要使用Podman
我经常被问到这样一个问题:“既然已经有了Docker,为什么还需要使用Podman呢?”其中一个原因是开源软件的用户可以自由选择。就如操作系统一样,操作系统通常支持各种编辑器、Shell、文件系统和Web浏览器。我相信Podman的设计从根本上是优于Docker的,而且它还提供了多种可提高容器安全性和可用性的功能。
1.3.1 为什么只能用一种方式运行容器
Podman的优势之一在于它是在Docker存在很久之后才推出的。Podman的开发者从完全不同的视角研究了改进Docker的设计方案。因为Docker是开源的,因此Podman使用了Docker的一些代码,同时也符合如开放容器计划(OCI)这样的新标准。Podman和开源社区合作,专注于开发新的功能。
在本节的其余部分,我将介绍改进之处。表1-2描述和比较了Podman和Docker中的一些可用功能。
表1-2 Podman和Docker的功能比较
1.3.2 非特权容器
Podman最重要的功能可能是它有能力在非特权(rootless)模式下运行。很多情况下,你不想给用户提供完全的root访问权限,但是用户和开发者仍需要运行容器和构建容器镜像的权限。需要root权限会导致很多对安全敏感的公司无法广泛使用Docker。然而,除了标准登录账户,在Linux中Podman不需要额外的安全功能就可以运行容器。
你可以将Docker客户端以普通用户运行,方法是将用户添加到Docker用户组(/etc/group),但我认为授予此访问权限是在Linux机器上可以做的最危险的事情之一。访问docker.sock允许你通过运行以下命令获得主机的完全root访问权限。在这个命令中,你会将整个主机操作系统根目录挂载到容器的/host目录上。--privileged标志会关闭所有容器安全性。然后你使用chroot命令将当前进程的主目录更改为/host。这意味着在执行chroot命令后,你将进入操作系统的根目录“/”,并具有完全的root特权。
$ docker run -ti --name hacker --privileged -v /:/host ubi8 chroot /host #
此时,你已经拥有机器的完整root特权,可以“为所欲为”。当你入侵机器后,你可以简单地执行docker rm命令来删除容器和你的所有操作记录。
$ docker rm hacker
当Docker配置为使用默认文件记录日志时,所有启动容器的记录都会被删除。我认为这比在没有root权限的情况下设置sudo更糟糕。因为至少在使用sudo的情况下,你有机会在你的日志文件中看到sudo的执行。
而使用Podman时,运行在系统上的进程始终由用户拥有,并且没有比普通用户更高的能力。即使你跳出容器,进程仍然以你的UID运行,并且系统上的所有操作都记录在审计日志中。Podman的用户不能简单地删除容器并覆盖他们的痕迹。要获取更多信息,请参见第6章。
提示 Docker现在具有类似Podman的非特权运行能力,但几乎没有人以这种方式运行它。只为了启动一个容器而在你的主目录中启动多个服务,这种方式并没有流行起来。
1.3.3 fork/exec模型
Docker是作为REST API服务器构建的。可以说Docker基本上是一个包含多个守护进程的客户端-服务器架构。当用户执行Docker客户端时,会借助一个命令行工具连接到Docker守护进程。然后,Docker守护进程会将镜像拉取到它的存储中,接着连接到containerd守护进程,最后containerd守护进程执行用来创建容器的OCI运行时。Docker守护进程是一个通信平台,负责容器中初始进程(PID1)的stdin、stdout和stderr的读写通信。该守护进程将所有输出信息都发送回Docker客户端。用户会认为容器的进程就像当前会话的子进程一样,但其实在背后存在大量的通信过程。图1-8展示了Docker客户端-服务器架构。
图1-8 Docker的客户端-服务器架构。容器是containerd的直接产物,而不是Docker客户端的。系统内核感知不到客户端程序和容器之间的关系
总之,Docker客户端与Docker守护进程通信,后者与containerd守护进程通信,containerd守护进程最终启动像runc这样的OCI运行时来真正启动容器的PID1。以这种方式运行容器会使得过程比较复杂。多年来,守护进程中的任何一个故障都会使得所有容器宕机,而且往往难以诊断发生了什么。而Podman的核心工程团队都有着深受UNIX系统哲学影响的操作系统背景。
UNIX和C是按照fork/exec计算模型设计的。基本上,当你运行一个新程序时,像Bash shell这样的父程序会fork(派生)一个新进程,然后将新程序作为旧程序的子程序执行。Podman工程团队认为,通过构建一个工具从容器镜像注册服务器中拉取容器镜像、配置容器存储,然后启动OCI运行时作为容器引擎子进程来启动容器,可以使容器更加简单易用。
在UNIX操作系统中,进程可以通过文件系统和进程间通信(IPC)机制共享内容。操作系统的这些特性使得多个容器引擎可以共享存储,而无须运行守护进程来控制访问和共享内容。除了使用操作系统的文件系统所提供的锁机制,容器引擎之间不需要进行通信。后续章节将探讨这种机制的优缺点。图1-9显示了Podman的架构和通信流程。
图1-9 Podman的fork/exec架构。用户启动Podman来执行OCI运行时,接着OCI运行时启动容器。容器是Podman的直接产物
1.3.4 Podman是无守护进程的
从根本上来说,Podman是不同于Docker的,因为它是无守护进程的。Podman可以像Docker一样运行所有容器镜像,并使用相同的容器运行时来启动容器。然而,Podman无须多个持续以root权限运行的守护进程就可以实现这些功能。
假设你有一个要在系统启动时运行的Web服务。该Web服务被打包在容器内,因此你需要一个容器引擎。如果使用Docker,你需要将其安装在你的机器上并运行,并且与之相关的每个守护进程都须保持运行状态且接受服务连接。然后,启动Docker客户端以启动Web服务。现在你的容器化应用程序以及所有Docker守护进程都在运行。使用Podman,则仅须使用Podman命令来启动容器,Podman本身并不存在守护进程。Podman创建的容器将持续运行,而且不会产生运行多个守护进程的额外开销。在物联网设备和边缘服务器等低端机器上,更少的开销往往更受欢迎。
1.3.5 用户友好的命令行
Docker的一个非常棒的特性是它具有简单易用的命令行界面。当然,还有其他容器命令行,如RKT、lxc和lxcd,也有自己独立的命令行界面。Podman团队早就意识到,独特的命令行界面并不会有助于Podman获得更多的市场份额。Docker是主导工具,几乎每个想要尝试容器的人都使用Docker命令行界面。此外,如果你在线搜索有关如何使用容器的信息,你几乎肯定会得到Docker命令行的使用示例。所以从一开始,我们就认为Podman必须与Docker命令行相匹配。我们很快就开发了一个命令:alias Docker = Podman。
使用此命令,你可以继续输入Docker命令,但Podman会负责运行你的容器。假如Podman命令行与Docker不同,用户会很容易将其视为Podman的一个bug,并且往往要求Podman做出修改以适应用户的使用习惯。当然,也有一些命令是Podman不支持的,如Docker Swarm。但在大多数情况下,Podman是可以完全替换Docker CLI的。
很多发行版提供了一个名为podman-docker的软件包,该软件包将别名从docker更改为podman并提供手册页的链接。这个别名的修改意味着,当你输入docker ps命令的时候,实际执行的是podman ps命令。如果你运行man docker ps,则会显示podman ps的手册页。图1-10是来自一位Podman用户的推特消息,他将docker命令的别名设置为podman,之后他惊讶地发现他已经使用Podman两个月了,而他一直认为自己使用的是Docker。
早在2018年,Alan Moran就曾发推文说:“我完全忘记了大约两个月前我设置了‘alias docker="podman"’,这真是一场梦。”Joe Thomson回复说:“那是什么提醒了你?”Alan Moran回答:“docker help。”然后Podman help就出现了。
图1-10 关于“alias docker = podman”的推文
1.3.6 支持REST API
Podman可以作为一个由套接字激活的REST API服务来运行,因此允许远程客户端管理和启动Podman容器。Podman支持Docker API以及涉及Podman高级功能的Podman API。通过使用Docker API,Podman支持docker-compose和其他使用docker-py Python绑定的工具。这意味着,即使你的基础设施是使用Docker套接字来启动容器的,你也可以简单地将Docker替换为Podman服务,并继续使用你现有的脚本和工具。第9章会介绍Podman服务。
基于Podman REST API,macOS、Windows和Linux系统上的远程Podman客户端可以与运行在Linux机器上的Podman容器进行交互。附录E和F将介绍如何在macOS和Windows上使用Podman。
1.3.7 与systemd集成
systemd是操作系统上最基础的init系统。Linux系统上的init进程是内核启动时启动的第一个进程。因此,init系统是操作系统上所有进程的“祖先”,可以对所有进程进行监控。Podman想把容器的运行和init系统完全结合起来。用户希望在系统启动时使用systemd启动和停止容器。容器需要支持以下操作。
■ 支持容器内的systemd。
■ 支持套接字激活。
■ 支持通过systemd通知机制来告诉systemd容器化应用程序已完全激活。
■ 允许systemd完全管理容器化应用程序的cgroups和生命周期。
基本上,容器是作为systemd单元文件中的服务来运行的。许多开发人员希望在容器内运行systemd,以便在容器内运行多个系统定义的服务。
然而,Docker社区不同意这一点,并拒绝了所有试图将systemd集成到Docker中的“pull request”。他们认为Docker应该管理容器的生命周期,同时也不想接纳那些想要在容器内运行systemd的用户的想法。
Docker社区认为Docker守护进程应该作为进程的控制器,由Docker守护进程管理容器的生命周期,并在启动时启动和停止容器,而不是systemd。然而,问题在于systemd拥有比Docker更多的功能,包括启动顺序、套接字激活、服务就绪通知等。图 1-11 是第一届 DockerCon 会议上Docker员工的实际工牌,反映了他们对systemd的不认同。
图1-11 DockerCon会议上的Docker员工工牌
开发人员在设计Podman时就希望确保它与systemd完全集成。当你在容器内运行systemd时,Podman以systemd期望的方式设置容器,并允许它以有限权限作为容器的PID1进程运行。通过systemd单元文件,Podman允许你在容器内运行服务,就像服务在操作系统或者虚拟机中运行一样。Podman支持套接字激活、服务通知和很多其他的systemd单元文件功能。Podman使得生成符合最佳实践的systemd单元文件很简单,以便在systemd服务中运行容器。要获取更多信息,请参阅第7章中关于systemd集成的内容。
Containers项目(https://github.com/containers)是Podman、容器库和其他容器管理工具的所在之处,它希望拥抱操作系统的所有功能并将其完全集成。第 7 章将介绍Podman与systemd的集成。
1.3.8 pod
Podman的优势之一在其名称中就可以看出来。前文就曾提到,Podman实际上是pod manager的缩写。正如Kubernetes官方文档所说,“pod(像海豹群一样,因此有了Podman的Logo,或者说是豌豆荚)是一组(一个或多个)容器;这些容器共享存储、网络资源及有关如何运行这些容器的规范声明”。Podman既可以像Docker一样一次创建一个容器,也可以在一个pod中同时管理多个容器。容器的设计目标之一是将服务拆分到多个独立的容器中,即微服务。然后你可以将这些容器关联起来构建一个更大的服务。pod允许你像管理单个实体一样,将多个服务组合在一起形成一个更大的服务。Podman的目标之一是允许创建并使用pod做一些功能验证。图1-12展示了运行在同一个系统上的两个pod,每个pod包含3个容器。
图1-12 运行在一个主机上的两个pod。每个pod运行两个不同的应用容器和一个infra容器
Podman的podman generate kube命令允许你基于运行的容器和pod生成Kubernetes YAML文件,可以在第7章中看到更多与此相关的介绍。
类似地,Podman的podman play kube命令允许用户执行Kubernetes YAML文件并在你的主机上生成pod和容器。我建议在单个主机上使用Podman运行pod和容器,如果是多台机器则建议使用Kubernetes将用户的pod和容器调度到多台机器上运行并通过用户的基础设施管理。其他项目如kind(https://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 短名称与容器镜像名称映射表
在使用短名称时,Docker被硬编码为总是从https://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还支持很多其他功能,如注册服务器黑名单、只拉取已签名镜像、设置镜像的mirror和指定硬编码的短名称,这样使得特定的短名称直接映射到全名称(请查阅第5章)。
1.3.10 支持多种传输方式
Podman支持多种不同的容器镜像源和目标,这些源和目标被称为传输方式(见表1-4)。Podman可以从容器镜像注册服务器和本地容器存储中拉取镜像,还支持以OCI、OCI TAR、传统的Docker TAR、目录格式存储的镜像,以及直接从Docker守护进程中获取的镜像。Podman命令可以轻松地运行每种格式的镜像。
表1-4 Podman支持的传输方式
1.3.11 完全可定制
容器引擎往往有很多内置常量,比如它们运行时使用的命名空间、SELinux是否启用以及容器运行时运行哪些功能。如果使用Docker,这些常量的值大部分都是硬编码的,并且默认不能被修改。对比来看,Podman则具有完全可定制的配置。
Podman有内置的默认值,但是定义了三处存储其配置文件的位置。
■ /usr/share/containers/containers.conf:每个发行版可以定义其想要使用的配置。
■ /etc/containers/containers.conf:可以在这里设置系统级别的参数覆盖。
■ $HOME/.config/containers/containers.conf:只能在非特权模式下指定。
通过这些配置文件,你可以配置自己想要的Podman默认运行方式。你也可以在默认配置下以更高的安全性来运行Podman。
1.3.12 支持用户命名空间
Podman与用户命名空间完全集成。非特权模式依赖用户命名空间,用户命名空间允许将多个UID分配给一个用户。用户命名空间使得在同一个系统上的不同用户之间保持隔离,因此,你可以让多个非特权用户运行具有多个UID的容器,所有用户之间相互隔离。
用户命名空间可以将容器相互隔离。Podman可轻松启动多个具有唯一用户命名空间的容器,然后内核会基于UID分离,将进程与主机用户隔离并使进程彼此之间也隔离开来。
与此不同,Docker仅支持在单独的用户命名空间运行容器,这意味着所有容器都运行在同一个用户命名空间。一个容器里的root用户与另一个容器里的root用户相同。Docker不支持在不同的用户命名空间运行每个容器,这意味着容器从用户命名空间的角度来看会相互攻击。所以即使Docker支持了这种模式,也几乎没有人使用Docker在独立的用户命名空间运行容器。