这是有关构建容器镜像的一系列博客文章中的第二篇. 该系列从 "未来我们如何构建容器镜像?" 开始. 该文章探讨了自 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 命令.
- !/bin/bash
- 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 提供了用于构建容器映像的两个最受欢迎的功能; 无守护程序和无根构建. 但是, 这些工具在越来越拥挤的空间中竞争, 尽管仍处于起步阶段, 但它们确实缺少某些类似工具当前能提供的功能.
来源: http://www.tuicool.com/articles/BVRvaeq