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