这是有关构建容器镜像的一系列博客文章中的第二篇。该系列从“未来我们如何构建容器镜像?”开始。该文章探讨了自Docker首次发布以来构建镜像的变化以及如何克服使用Dockerfile的诸多限制。这篇文章重点介绍Podman和Buildah,在以后的文章中,我们将探究该领域的其他新方法。
Podman和Buildah是两个新近出现的工具,目的是帮助构建容器镜像。它们是互补的工具,都是容器工具开放存储库(Open Repository for Container Tools)的组成部分,他们的出现源于Red Hat的一项任务,即从容器工作流中移除Docker daemon。为什么是这两种工具,每种工具都将给容器镜像构建带来什么体验?让我们先从Podman开始。
Podman
Podman的功能不止于构建容器镜像,通常拿它与Buildah一起讨论。我们在这里提及它是因为它对于容器镜像构建的一个贡献。
无守护进程构建(Daemonless Builds)
Podman尝试着无需运行守护程序即可处理和响应API请求,从而重现了熟悉的Docker CLI的全部功能。不同于客户端/服务器模式,Podman采用本地fork/exec模式,在Red Hat的眼中,这大大简化了容器生命周期的控制和安全性。
Podman模拟了Docker提供的各种客户端命令,有些拥趸甚至鼓励新用户将Podman当作docker命令的别名来使用,以便于日后过渡。Podman除了提供Docker命令套件,还能提供Podman 命定。它用来构建OCI(Open Container Initiative)兼容的容器镜像,使用Dockerfile作为其各个构建步骤的源。从这个意义上讲,它实际等同于docker build命令,但是没有Docker守护进程带来的开销。
就像你期待的那,Podman build兼容所有docker build的参数(一些偶尔才用到的参数还未被纳入,比如--cache-from),另外Podman还包含一些额外的参数用以实现以前由docker守护进程才能提供的特性(比如,注册表通信registry communication)。因此,从docker构建过渡到podman构建是一种无缝的体验,除非你有怪癖,例如需要指定处查找没有按命名规则命名的镜像。
无root权限构建(Rootless Builds)
除了实现无守护进程构建这一创举,Podman还能提供另一个受欢迎的功能-无root权限构建。过去,由于使用了Docker守护程序,使用docker build构建容器镜像必需有root权限,在安全意识强的组织中这通常被认为过于开放。在提供执行Rootless构建的能力时,Podman解决了这个严重的问题,但这并不意味着没有局限性。
从Dockerfiles构建镜像的过程涉及临时创建用于运行命令的容器,以便安装软件包,检索远程内容,构建工件(build artifacts)等。创建和运行容器通常需要root权限。那么,Podman如何解决这个问题?为了避免以root身份进行构建,Podman利用了用户命名空间(User namespaces)。命名空间(namespaces)为Linux进程提供了一种隔离机制,并且是容器抽象的主要组成部分。如果创建容器所使用的命名空间集包括用户命名空间,则调用该容器的代理可以是非特权用户-换句话说,使用用户命名空间,Podman可以使用容器来实现无root权限构建镜像。
户名称空间提供了一种方法,可以将主机默认用户命名空间中的一系列非特权用户和组的ID(UID / GID)映射到与容器关联的新用户名称空间中的一组不同的UID / GID。这样,可以将主机上的非特权UID / GID安全地映射到容器内的根用户(UID / GID = 0),从而为容器的进程提供镜像构建过程中可能需要的特权(例如,安装OS软件包)。但是,根据映射的关系,容器在主机上仅具有与颁发podman build命令的非特权用户相同的文件访问权限。这意味着可以保护主机的文件系统免遭意外或恶意破坏。
当前无root权限构建的不足
这里也存在问题。镜像通常在基本镜像(Dockerfile中的FROM指令)基础上构建,通常UID / GID = 0的用户是其内容的拥有者。当非特权用户在容器中启动容器构建时,该容器的文件由主机上的UID / GID = 0拥有,而该容器的进程将仅具有与该非特权用户相关联的文件访问权限。这可能意味着容器的进程无法写入其文件系统,这将严重阻碍容器镜像的构建。为了使镜像中的文件在容器内具有正确的所有权,需要将UID / GID集“内移”到用户命名空间映射的内联位置。当前,没有实现这一目标的最佳手段。
当调用无root权限构建并且容器要求所有权“转移”时,文件系统内容将被复制并且所有权更改(chowned)以反映映射。这显然在空间利用方面效率低下,并且花费时间,这会严重影响容器构建所花费的时间。容器背后的重要思想之一是容器镜像无需复制就可以被多个容器共享。理想情况下,当容器的文件系统是由其constituent layers构成时,这种转移应该作为安装操作的一部分而无需重复进行。
大多数容器运行时都使用overlayfs来构成容器的文件系统,该文件系统不支持在其挂载上转移UID / GID,但是最近Ubuntu成为第一个内核支持(shiftfs)overlays的Linux发行版。这个版本已经在Linux Containers LXD项目中使用。
对于Podman而言,临时的补救措施是在Linux内核版本4.19中为overlayfs引入了mount选项。该选项仅将文件和目录的元数据复制到读/写层,而不是内容本身。最终,为了实现这个目标,社区还是要等待主流linux内核支持UID / GID 转移。
让我们继续学习Buildah,并说明它与Podman构建之间的关系以及不同之处。
Buildah
到目前为止,我们还没有提到podman build如何在后台使用Buildah来执行容器映像的构建。这意味着无守护程序和无root权限构建也是Buildah的功能。与Podman不同,Buildah针对容器镜像的构建有专有的功能,并且还具有许多其他功能,这些功能不仅限于基于Dockerfiles构建镜像。
大部分容器镜像都是使用Dockerfile构建的。我们已经讨论了podman build如何使用Dockerfile来构建镜像,同时 Buildah也可以使用buildah bud命令从Dockerfile中构建镜像。但是,Buildah的创新来自于对 Dockerfile的替代方法的探寻。使用替代方法的理由是,所需的只是 “ OCI兼容”镜像的“捆绑”,而达到最终不需要Dockerfile的目标。 Buildah的维护者坚持认为Dockerfile是一个障碍。
Buildah如何工作
尽管另辟蹊径于Dockerfile的意图很明确,但Buildah使用非常相似的过程来构建容器镜像。 Docker build启动一个新容器来处理每个Dockerfile指令,从而导致在将该容器提交为新映像之前需要创建新的/更改的内容或镜像元数据。下一个指令用之前创建的镜像来创建容器,并将其提交为新的镜像,依此类推。 Buildah做同样的事情,但是它不使用Dockerfile指令,而是执行Buildah子命令,并且在每个子命令执行后都不需要“提交(submit)”。
构建的过程从buildah from命令开始,结果会是根据给定参数的镜像生成一个正在运行的容器,这很像Dockerfile的FORM指令。作为构建镜像的一部分,在容器中执行指令(比如,创建新用户,或者从源创建artifact),镜像制作者可以用buildah run,它可以在需要时进行交互。除了运行为容器镜像创建内容的命令外,Buildah还提供了一种使用buildah config定义镜像元数据的方法。这样就可以指定诸如公开端口,默认用户,容器入口等等。buildah copy和buildah add命令直接类似于COPY和ADD Dockerfile指令,用于将外部内容获取到镜像中。使用buildah mount,甚至可以将容器的根文件系统挂载在主机上的适当位置,以便随后使用主机本身的工具进行操作。
一旦映像制作者确信他们镜像已经制作完成,buildah commit命令会将容器制作成新镜像。
工作流
Dockerfile和docker build以及Buildah之间有一些明显的相似之处。Dockerfile强制顺序执行相关指令,那么Buildah如何在容器构建中提供相似的顺序和可重复性?建议使用守护进程以编程方式定义使用Buildah进行的容器构建,而不是使用守护程序的构建引擎来强制执行此docker build命令。
id=$(buildah from --pull node:10)
buildah run $id mkdir -p /usr/src/app
buildah config --workingdir /usr/src/app $id
buildah copy $id $PWD .
buildah run --net host $id npm install
buildah config --port 1337 --entrypoint '["npm", "start"]' $id
buildah commit $id example-app
上面的简单示例显示了如何在Bash脚本中使用Buildah实现可重复的构建。
用这种方式进行镜像构建,Buidah移除了对守护进程的依赖,并且进一步使映像构建器摆脱了Dockerfile语法的约束。由buildah构建的镜像可以被上传到镜像仓库,然后再由Podman或Docker守护进程拉取,最后平滑的运行在支持OCI规范的容器运行时上。
构建缓存和并行执行
如果镜像是使用Dockerfile和buildah bud构建的,则image layers将被缓存并以后的构建中重复使用。对于那些从Docker环境过渡到Buildah的用户来说,这是预期之内的,它可以显着提高构建执行速度。但是,如果你期望在脚本中使用Buildah命令所构建的镜像可以使用缓存,那么你会感到惊讶。缓存是不可用的。这意味着需要在每次新的构建迭代上执行整套构建步骤,而不管内容或命令是否发生任何更改。
此外,即使一个构建步骤完全独立于另一个构建步骤,Buildah也会顺序执行其构建步骤。尽管实现并行构建步骤功能已经纳入考虑之中,但是Buildah目前尚不提供此功能,这会进一步延长执行复杂容器镜像构建所需的时间。
结论
尽管Podman和Buildah之间有明显的区别,但有两种方法可以实现同一目标令人困惑。如有疑问,那么在使用Dockerfile创作镜像时应使用podman构建,而如果认为Dockerfile语法过于严格,或者采用类似脚本的方法来实现可重复性,则应使用Buildah,。值得一提的是,容器镜像和Dockerfile几乎是同义词,因此Buildah是否会在Red Hat社区之外获得足够的吸引力来最终取代Dockerfile,还有待观察。
Podman和Buildah提供了用于构建容器映像的两个最受欢迎的功能;无守护程序和无根构建。但是,这些工具在越来越拥挤的空间中竞争,尽管仍处于起步阶段,但它们确实缺少某些类似工具当前能提供的功能。