我最近正在开展一个小项目,学习使用 Docker 工具链,并了解其工作原理。 我决定构建(又一个) etcd 的开源 Docker 镜像并将其发布在 Docker Hub 上。 像所有开源项目一样,构建一个有效的 Docker 镜像非常简单,并没有花太多时间。反而是如何镜像让更多人觉得有用会花费更多的时间。 在这个过程中,我学到了一些关于使用 Docker , Docker Hub 和 Travis 的内容,但仍然有一些问题没有得到解答。 因此,我认为值得记录我的发现和未解答的问题,以获得反馈并提高我的理解。
首先阐述我的要求:
- 支持最新版本和一些旧版本的应用程序似乎是 Docker 社区中的常见做法。 支持常见发行版及其精简版本似乎也是一种常见做法。 因此,该镜像应该遵循社区的做法。
- Github 上的 README 自动同步到 Docker Hub 上的描述是必须的。 我的假设是开发人员通常会在 Docker Hub 上发现 Docker 镜像。 所以只在 Github 上有一个很好的自述文件是不够的。 该文档也必须存在于 Docker Hub 上。 我不想在我的 git 仓库中编写文档,然后每次有更改时手动将其复制到 Docker Hub。
- 镜像本身应该非常易于使用,既可以作为服务器运行 etcd,也可以作为 CLI 客户端运行。 我不打算讨论这个,因为这与本章主题有点无关。 但是如果你感兴趣的话,请查看 README 并在 Github 项目中打开一个 issue 来反馈问题。
第一个挑战是建立一个自动化流程来构建新镜像并将其上传到 Docker Hub。 有一篇很好的博客记录了如何使用Travis 实现这一目标,以便每次推送到代码库都会构建一个新镜像像,并且当合并分支时,构建的新镜像将被推送到Docker Hub。 设置很简单。
尽管简单,但它不符合我的任何要求。 下面尝试逐一解决。
问题#1 - 自动同步 README
虽然上面提及到的 Travis 构建过程可以构建并将镜像推送到 Docker Hub,但它无法同步 README。 Docker CLI 也不支持更新 Docker Hub 上的描述。
Docker Hub 有一个名为 Automated Builds 的东西,通过配置,可以达到 Travis 作业产生的结果。 并且,它还可以从 Github 仓库中提取 README 的内容,以便在 Docker Hub 上作为描述使用。 因此,我们可以使用 Automated Builds 来构建镜像并从 Github 同步 README。 这解决了我们的一项要求。
Automated Builds 的工作原理是我们必须在 Docker Hub 上配置它以指定 Docker 上下文的路径(即 Dockerfile 的路径),从该 Dockerfile 构建镜像并打上自己的标记。 这意味着从仓库构建的每个镜像,必须在唯一路径上存在 Dockerfile。 这很快就会成为一个问题。
问题#2 - 运行测试
虽然 Docker Hub 上的 Automated Builds 解决了 README 同步问题,但自动构建还存在一些其他问题。 你无法将其用作运行测试和获取 PR 测试状态的工具。 根本没有办法从 Docker Hub 的构建系统获得反馈给 Github。 这对于开发流程至关重要
所以有一件事是肯定的 - 如果构建失败,我们需要回到 Travis 运行测试并阻止 PR。
问题#3 - 为多个版本构建镜像
我不仅想构建 etcd 的最新版本,也包括之前的一些版本,并构建这些版本的发行版本。
支持的版本:
3.3(最新)
3.2
支持的发行版本:
Debian:stretch-slim
alpine
因此,我们必须构建总共 4 个镜像 - 每个发行版本对应一个版本。
首先看看多版本问题。
我开始查看其他开源 Docker 镜像是如何实现这一点的。 结果并不令人满意。
看了 MySQL 镜像的 Dockerfiles。 每个版本的 MySQL 的 docker 镜像都在仓库的“版本目录”中有一个专用的 Dockerfile。 他们几乎没有区别。 请参阅这两个 MySQL 的 Dockerfiles - MySQL 5.6 和 MySQL 5.7 的 Dockerfile。 在 76 行代码中,它们只有一行不同,并且只是版本不同。
我在 Nginx 的 Dockerfiles 中观察到了一个非常相似的模式。 平均而言,每个 Dockerfile 中每个基本镜像的每个版本只有 5-10%的行不同。
这看起来像是大量的重复代码。 我不想这样,因为每个镜像中唯一不同的是版本和包管理器(Alpine 和 Debian 使用不同的包管理器)。
Dockerfile 的 ARG 可用于简化此操作。 通过将值传递给 docker build 命令,ARG 可用于定义在构建镜像时可以设置的变量。 这非常方便。 由于每个版本的 Dockerfiles 唯一不同的是 URL 中的版本本身,我可以轻松地使用它并使用相同的 Dockerfile 和以下命令为不同版本构建 docker 镜像:
docker build . -t "3.3" --build-arg version=3.3 docker build . -t "3.2" --build-arg version=3.2
在带有环境矩阵的 Travis 中使用它,我们可以自动为多个版本构建 Docker 镜像。 请参阅.travis.yml 中为每个版本构建多个镜像的代码段:
language: bash services: docker env: matrix: - VERSION=3.3.1 - VERSION=3.2.19 script: - docker build . -t "$VERSION" --build-arg version=$VERSION
这解决了多版本的多镜像问题。 但我们仍然需要解决为每个版本的每个基本镜像构建镜像的问题。
问题#4 - 为多个基本镜像进行构建
除了为多个版本构建镜像,我们还存在为多个基本镜像构建镜像的问题。 现在仅仅使用 ARG 是不够的。
在这种情况下,挑战变成了每个发行版可能都有自己的具体操作方式,例如包管理。 Alpine 和 Debian 使用不同的包管理器。 因此仅使用 ARG 不足以处理所有更改。 还需要管理如何进行包的安装。 在这种情况下,我们需要一个安装包,但不需要运行 etcd。 如果我们能够以某种方式获得没有任何额外的 etcd 二进制文件,我们就可以制作出完美的 Docker 镜像。
多阶段 Docker 构建和构建器模式
Docker 的 Multi-Stage Builds 是一种干净利落地构建 Docker 镜像的方式。 通常在构建过程中,我们会安装大量随机包。 成功构建应用程序后,清理可能会很麻烦。
构建 Docker 镜像时,镜像总是越小越好。 有些人使用 bash 脚本编写清理指令。 有些人将开发和生产 Dockerfiles 分开,开发 Dockerfile 会产生一个较大包体的镜像,其包含开发所需的一切,生产 Dockerfile 会生成一个仅具有开发 Docker 镜像所必需工件的 docker 镜像。 但这两种方法都有各自的不足。
多阶段构建简化了其中的一部分,并提供了更好地组织 Dockerfiles 的方法。 你可以在 Dockerfiles 中添加多个阶段,为每个阶段构建一个完全独立的 Docker 镜像,并使用其他构造可以轻松地将工件从一个阶段复制到另一个阶段。 从 Docker Hub 上的文档中查看此示例:
FROM golang:1.7.3 WORKDIR /go/src/github.com/alexellis/href-counter/ RUN go get -d -v golang.org/x/net/html COPY app.go . RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app . FROM alpine:latest RUN apk --no-cache add ca-certificates WORKDIR /root/ COPY --from=0 /go/src/github.com/alexellis/href-counter/app . CMD ["./app"]
首先,注意这个 Dockerifle 有多个 FROM 指令。 每条指令表示不同的构建阶段,从而产生完全不同的镜像。 COPY 指令采用名为 --from 的参数,可用于从其他构建阶段复制文件。 使用此选项来复制你想要用来构建镜像的文件,并留下不需要的所有内容。
多阶段构建与 ARG 结合可以解决我们的问题。 现在可以同时使用 version 和 base_image,并使用 base_image 和 FROM 指令在构建时选择基本镜像。
FROM debian:stretch-slim ... FROM "$base_image" ... COPY --from=0 /path/to/etcd-binary /usr/local/bin CMD ["etcd"]
然后运行以下命令来构建所有的镜像:
# Version 3.3 on debian:stretch-slim docker build . -t "3.3" --build-arg version=3.3 \ --build-arg base_image="debian:stretch-slim" # Version 3.3 on alpine:latest docker build . -t "3.3:alpine" --build-arg version=3.3 \ --build-arg base_image="alpine:latest" # Version 3.2 on debian:stretch-slim docker build . -t "3.2" --build-arg version=3.2 \ --build-arg base_image="debian:stretch-slim" # Version 3.2 on alpine:latest docker build . -t "3.2:alpine" --build-arg version=3.2 \ --build-arg base_image="alpine:latest"
在这里查看完整的Dockerfile 。
这为我们解决为多版本构建镜像和多个基本镜像代码重复的问题。
Multi-Stage Builds 有一些很酷的功能。 我建议检查一下,以获得更好的 Docker 体验。
有了这个,可以通过环境构建矩阵提供的不同环境来构建在Travis 上的所有镜像,在每个环境合并代码到主分支,构建镜像、运行测试、推送构建好的镜像到Docker Hub。 这是一个示例.travis.yml 文件(上一个示例的扩展):
language: bash services: docker env: matrix: - VERSION=3.3.1 BASE_IMAGE=debian:stretch-slim - VERSION=3.2.19 BASE_IMAGE=debian:stretch-slim - VERSION=3.3.1 BASE_IMAGE=alpine - VERSION=3.2.19 BASE_IMAGE=alpine script: - | docker build . -t "$VERSION" \ --build-arg version=$VERSION \ --build-arg base_image="$BASE_IMAGE"
但同步 README 的问题仍然存在,并且 Travis 似乎也无法做到。 下面看看如何解决。
问题#5 - 捆绑在一起
我们在这里取得了以下进展:
- 我们知道如何为每个版本和每个基本镜像构建 Docker 镜像。 可用一种可管理的方式执行此操作,而无需重复代码。 并且可以在 Travis 上进行构建。
- 可以在 Travis 上构建镜像并推送到 Docker。 但是无法在 Docker Hub 上获得 README。
- 可以使用 Automated Builds 在 Docker Hub 上构建镜像,并将 README 同步到 Docker Hub。 但是我们无法为 Github 上的每个 pull 请求构建镜像,并且如果构建失败则阻止合并 pull 请求。 并且不会给 Github 任何反馈。
因此,虽然我们可以使用 Travis 构建所有镜像并为每个 PR 运行测试,但无法使用它来推送镜像。 而对于 Docker Hub 则相反。 实际上,最简单形式的 Docker Hub 的 Automated Build 系统不能用于基于 ARG 的设置,因为 Automated Builds 可以使用 Docker 上下文,即在构建设置中为“每个指定的路径”构建镜像,并期望这些路径中存在 Dockerfile。 你无法在此类设置中传递 CLI 参数。 因此,我们的自定义 docker 构建命令不适用于 Automated Builds。
自定义构建阶段钩子,用于 Docker Hub 上的自动构建
自定义构建阶段钩子允许在不同的构建阶段执行自定义脚本,从而对自动构建进行更高级的自定义。 例如,可以覆盖构建阶段以将额外参数传递给 docker 构建,或者可以覆盖推送阶段以推送到多个仓库。 当然,你还可以做很多事情。
这最终解决了我们所有的问题:
- 我们可以使用 Travis 设置为每个 PR 构建和运行测试,但不能将镜像推送到 Docker Hub。
- 只能在主分支上使用自动构建和自定义阶段钩子。 这将把我们的镜像推送到 Docker Hub 并同步来自 Github 的 README。
构建钩子脚本是很通用的。 我参考在 Travis 中构建镜像的脚本来编写钩子脚本!
在此处查看整个设置:
结论
没有重复代码,易于构建的过程,良好的开发体验和 README 的自动同步 - 这对我来说非常有用,我对最终的设置非常满意。
有人可能会说使用 Docker Automated Build 和 Travis 很难维护。 可以通过自定义构建钩子将 PR 状态推送到 Github,但这不是我到目前为止所探讨的,与在 Docker Hub 上维护 Travis 和 Automated Builds 相比,它看起来也需要更多精力。
但有一件事不确定是使用 ARG 构建 Docker 镜像的方法和多阶段构建。 虽然它适合我,但我在这个领域的经验是有限的,我想得到反馈。 你怎么看? 还有构建和分发开源 Docker 镜像更好的方法吗? 请通过评论部分告诉我你的想法。
评论 1 条评论