要使容器配置恰到好处需要多少次迭代?每次迭代需要多长时间?好吧,如果你的回答是“太多次和太长时间”,那么我的经验与你相似。从表面上看,创建配置文件似乎是一项简单的练习:在配置文件中实现与手动安装系统时执行的相同步骤。不幸的是,我发现通常情况并非如此,一些“技巧”对于此类 DevOps 练习很有用。
在本文中,我将分享一些我发现的有助于最大限度地减少迭代次数和时长的技术。此外,我将概述一些超出标准实践之外的良好实践。
在我之前关于容器化构建系统的文章的教程仓库中,我添加了一个名为 /tutorial2_docker_tricks 的文件夹,其中包含一个示例,涵盖了我将在本文中介绍的一些技巧。如果您想跟随学习并安装了 Git,可以使用以下命令在本地拉取它:
$ git clone https://github.com/ravi-chandran/dockerize-tutorial
本教程已在 Docker Desktop Edition 上测试过,但它应该适用于任何兼容的 Linux 容器系统(如 Podman)。
节省容器镜像构建迭代的时间
如果 Dockerfile 涉及下载和安装 5GB 文件,即使网络速度良好,每次 docker image build 迭代也可能需要很长时间。并且忘记包含要安装的一项内容可能意味着重建该点之后的所有层。
解决此挑战的一种方法是使用本地 HTTP 服务器,以避免在 docker image build 迭代期间多次从互联网下载大型文件。为了通过示例说明这一点,假设您需要在 Ubuntu 18.04 下创建一个包含 Anaconda 3 的容器镜像。Anaconda 3 安装程序是一个约 0.5GB 的文件,因此这将是本示例中的“大型”文件。
请注意,您不希望使用 COPY 指令,因为它会创建一个新层。您还应该在使用大型安装程序后将其删除,以最大限度地减小容器镜像大小。您可以使用多阶段构建,但我发现以下方法足够且非常有效。
基本思想是在本地使用基于 Python 的 HTTP 服务器来服务大型文件,并让 Dockerfile 从此本地服务器 wget 大型文件。让我们探讨如何有效地设置它的细节。提醒一下,您可以访问完整示例。
本示例仓库中 tutorial2_docker_tricks/ 文件夹的必要内容是
tutorial2_docker_tricks/
├── build_docker_image.sh # builds the docker image
├── run_container.sh # instantiates a container from the image
├── install_anaconda.dockerfile # Dockerfile for creating our target docker image
├── .dockerignore # used to ignore contents of the installer/ folder from the docker context
├── installer # folder with all our large files required for creating the docker image
│ └── Anaconda3-2019.10-Linux-x86_64.sh # from https://repo.anaconda.com/archive/Anaconda3-2019.10-Linux-x86_64.sh
└── workdir # example folder used as a volume in the running container
该方法的关键步骤是
- 将大型文件放在 installer/ 文件夹中。在本示例中,我有一个大型 Anaconda 安装程序文件 Anaconda3-2019.10-Linux-x86_64.sh。如果您克隆我的Git 仓库,您将找不到此文件,因为只有您作为容器镜像创建者才需要此源文件。镜像的最终用户不需要。下载安装程序以跟随示例进行操作。
- 创建 .dockerignore 文件并使其忽略 installer/ 文件夹,以避免 Docker 将所有大型文件复制到构建上下文中。
- 在终端中,cd 进入 tutorial2_docker_tricks/ 文件夹并执行构建脚本,命令为 ./build_docker_image.sh。
- 在 build_docker_image.sh 中,启动 Python HTTP 服务器以服务 installer/ 文件夹中的任何文件
cd installer python3 -m http.server --bind 10.0.2.15 8888 & cd ..
- 如果您对奇怪的互联网协议 (IP) 地址感到疑惑,我正在使用 VirtualBox Linux VM,当我运行 ifconfig 时,10.0.2.15 显示为以太网适配器的地址。此 IP 似乎是 VirtualBox 使用的约定。如果您的设置不同,则需要更新此 IP 地址以匹配您的环境,然后更新 build_docker_image.sh 和 install_anaconda.dockerfile。本示例中服务器的端口号设置为 8888。请注意,IP 和端口号可以作为构建参数传入,但我为了简洁起见对其进行了硬编码。
- 由于 HTTP 服务器设置为在后台运行,请在脚本末尾附近使用 kill -9 命令停止服务器,我发现了一种优雅的方法
kill -9 `ps -ef | grep http.server | grep 8888 | awk '{print $2}'
- 请注意,相同的 kill -9 也用于脚本的早期(在启动 HTTP 服务器之前)。一般来说,当我对任何我可能故意中断的构建脚本进行迭代时,这确保了每次 HTTP 服务器的干净启动。
- 在Dockerfile中,有一个 RUN wget 指令,用于从本地 HTTP 服务器下载 Anaconda 安装程序。它还在安装后删除安装程序文件并进行清理。最重要的是,所有这些操作都在同一层中执行,以最大限度地减小镜像大小
# install Anaconda by downloading the installer via the local http server ARG ANACONDA RUN wget --no-proxy http://10.0.2.15:8888/${ANACONDA} -O ~/anaconda.sh \ && /bin/bash ~/anaconda.sh -b -p /opt/conda \ && rm ~/anaconda.sh \ && rm -fr /var/lib/apt/lists/{apt,dpkg,cache,log} /tmp/* /var/tmp/*
- 此文件运行包装脚本 anaconda.sh,并通过使用 rm 删除大型文件来清理。
- 构建完成后,您应该看到一个镜像 anaconda_ubuntu1804:v1。(您可以使用 docker image ls 列出镜像。)
- 您可以使用终端中的 ./run_container.sh 从此镜像实例化一个容器,同时在 tutorial2_docker_tricks/ 文件夹中。您可以使用以下命令验证 Anaconda 是否已安装:
$ ./run_container.sh $ python --version Python 3.7.5 $ conda --version conda 4.8.0 $ anaconda --version anaconda Command line client (version 1.7.2)
- 您会注意到 run_container.sh 设置了一个卷 workdir。在本示例仓库中,文件夹 workdir/ 是空的。这是我用来设置卷的约定,我可以在其中放置独立于容器镜像的 Python 和其他脚本。
最小化容器镜像大小
每个 RUN 命令都相当于执行一个新的 shell,并且每个 RUN 命令都会创建一个层。使用单独的 RUN 命令来模仿安装说明的简单方法最终可能会在一个或多个相互依赖的步骤中中断。如果它恰好有效,通常会导致更大的镜像。在一个 RUN 命令中链接多个安装步骤并包含 autoremove、autoclean 和 rm 命令(如下例所示)对于最小化每个层的大小很有用。根据安装的内容,可能不需要其中一些步骤。但是,由于这些步骤花费的时间微不足道,我总是在调用 apt-get 的 RUN 命令末尾为了保险起见而加入它们
RUN apt-get update \
&& DEBIAN_FRONTEND=noninteractive \
apt-get -y --quiet --no-install-recommends install \
# list of packages being installed go here \
&& apt-get -y autoremove \
&& apt-get clean autoclean \
&& rm -fr /var/lib/apt/lists/{apt,dpkg,cache,log} /tmp/* /var/tmp/*
此外,请确保您已设置 .dockerignore 文件以忽略不需要发送到 Docker 构建上下文的项目(例如前面示例中的 Anaconda 安装程序文件)。
组织构建工具 I/O
对于软件构建系统,构建输入和输出——配置和调用工具的所有脚本——应位于镜像和最终运行的容器之外。容器本身应保持无状态,以便不同的用户对其具有相同的结果。我在我之前的文章中对此进行了广泛的介绍,但我想强调这一点,因为它对我来说一直是一个有用的约定。最好通过设置容器卷来访问这些输入和输出。
我不得不使用一个容器镜像,该镜像以源代码和大型预构建二进制文件的形式提供数据。作为一名软件开发人员,我被期望在容器中编辑代码。这是有问题的,因为容器默认是无状态的:它们不保存容器内的数据,因为它们被设计为可丢弃的。但我还是做了,并且在每天结束时,我停止了容器,并且不得不小心不要删除它,因为必须维护状态以便我可以在第二天继续工作。这种方法的缺点是,如果有不止一个人参与该项目,开发状态就会出现分歧。在这种方法中,跨开发人员拥有相同的构建系统的价值在某种程度上丧失了。
以非 root 用户生成输出
I/O 的一个重要方面涉及在容器中运行工具时生成的输出文件的所有权。默认情况下,由于 Docker 以 root 身份运行,因此输出文件将归 root 所有,这很不方便。您通常希望以非 root 用户身份工作。在生成构建输出后更改所有权可以使用脚本完成,但这只是一个额外的且不必要的步骤。最好在 Dockerfile 中尽早设置 USER 参数
ARG USERNAME
# other commands...
USER ${USERNAME}
执行 docker image build 时,可以将 USERNAME 作为构建参数 (--build-arg) 传入。您可以在示例Dockerfile和相应的构建脚本中看到此示例。
工具的某些部分也可能需要以非 root 用户身份安装。因此,Dockerfile 中的安装顺序可能需要与您手动和直接在 Linux 下安装时的方式不同。
非交互式安装
交互性与容器自动化相反。我发现
DEBIAN_FRONTEND=noninteractive apt-get -y --quiet --no-install-recommends
apt-get install 指令的选项(如上面的示例所示)对于防止安装程序打开对话框是必要的。请注意,这些选项应作为 RUN 指令的一部分使用。DEBIAN_FRONTEND=noninteractive 不应在 Dockerfile 中设置为环境变量 (ENV),正如此 FAQ 解释的那样,因为它将被容器继承。
记录您的构建和运行输出
调试构建失败的原因是一项常见任务,日志是执行此操作的好方法。使用 Bash 脚本中的 tee 实用程序保存容器镜像构建或容器运行会话期间发生的所有事情的 TypeScript。换句话说,在脚本中的 docker image build 和 docker image run 命令的末尾添加 |& tee $BASH_SOURCE.log。请参阅镜像构建和容器运行脚本中的示例。
这种 tee 技术的作用是生成一个与 Bash 脚本同名但附加了 .log 扩展名的文件,以便您知道它来自哪个脚本。运行脚本时您在终端上看到的所有内容都将记录到这个同名文件中。
这对于您的容器镜像用户在某些功能无法正常工作时向您报告问题尤其有价值。您可以要求他们向您发送日志文件以帮助诊断问题。许多工具会生成大量输出,以至于很容易超出终端缓冲区默认大小。仅依靠终端的缓冲区容量来复制粘贴错误消息可能不足以诊断问题,因为较早的错误可能已丢失。
我发现这很有用,即使在容器镜像构建脚本中也是如此,尤其是在使用上面讨论的基于 Python 的 HTTP 服务器时。服务器在下载过程中生成了太多行,以至于通常会超出终端的缓冲区。
优雅地处理代理
在我的工作环境中,需要代理才能访问互联网,以便在 RUN apt-get 和 RUN wget 命令中下载资源。代理通常从环境变量 http_proxy 或 https_proxy 推断出来。虽然可以使用 ENV 命令在 Dockerfile 中硬编码此类代理设置,但直接使用 ENV 进行代理存在多个问题。
如果您是唯一一个将构建容器的人,那么这也许可以工作。但是 Dockerfile 可能无法被具有不同代理设置的不同位置的其他人使用。另一个问题是 IT 部门可能在某个时候更改代理,导致 Dockerfile 将不再工作。此外,Dockerfile 是一个精确的文档,指定了一个配置受控的系统,并且每次更改都将受到质量保证的审查。
避免硬编码代理的一种简单方法是在 docker image build 命令中将您的本地代理设置作为构建参数传递
docker image build \
--build-arg MY_PROXY=http://my_local_proxy.proxy.com:xx
然后,在 Dockerfile 中,根据构建参数设置环境变量。在本文所示的示例中,您仍然可以设置一个默认代理值,该值可以被上面的构建参数覆盖
# set a default proxy
ARG MY_PROXY=MY_PROXY=http://my_default_proxy.proxy.com:nn/
ENV http_proxy=$MY_PROXY
ENV https_proxy=$MY_PROXY
总结
这些技术帮助我显著减少了创建容器镜像和在出现问题时对其进行调试所需的时间。我将继续寻找其他最佳实践以添加到我的列表中。我希望您发现以上技术有用。
12 条评论