需要多少次迭代才能使容器配置恰到好处?每次迭代又需要多长时间? 如果您的答案是“次数太多,时间太长”,那么我的经历与您相似。 从表面上看,创建配置文件似乎是一项简单的练习:在配置文件中实现与手动安装系统时相同的步骤。 不幸的是,我发现通常情况并非如此,一些“技巧”对于此类 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}
USERNAME 可以在执行 docker image build 时作为构建参数 (--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 条评论