docker容器构建
构建系统由用于从源代码过渡到正在运行的应用程序的工具和过程组成。 这种过渡还涉及将代码的读者从软件开发人员更改为最终用户,无论最终用户是运营方面的同事还是部署系统的同事。
在使用容器创建了一些构建系统之后,我认为我有一个不错的,可重复的方法值得分享。 这些构建系统用于生成嵌入式硬件的可加载软件映像,并编译机器学习算法,但是这种方法足够抽象,可以用于任何基于容器的构建系统。
这种方法是关于以易于使用和维护的方式创建或组织构建系统。 这与处理容器化任何特定软件编译器或工具所需的技巧无关。 它适用于软件开发人员构建软件以将可维护的映像传递给其他技术用户(无论是系统管理员,DevOps工程师还是其他职称)的常见用例。 最终用户远离了构建系统,因此他们可以专注于软件。
为什么要对构建系统进行容器化?
创建可重复的,基于容器的构建系统可以为软件团队带来许多好处:
- 重点:我想专注于编写我的应用程序。 当我调用一个工具来“构建”时,我希望工具集提供一个现成的二进制文件。 我不想花时间对构建系统进行故障排除。 实际上,我宁愿不了解也不关心构建系统。
- 相同的构建行为:无论用例如何,我都想确保整个团队使用相同版本的工具集,并在构建时获得相同的结果。 否则,我将不断处理“它在我的PC上而不在您的PC上运行的情况”。 在团队项目中,使用相同的工具集版本并为给定的输入源文件集获得相同的输出至关重要。
- 易于设置和将来的迁移:即使向每个人都提供了一套详细的说明来为项目安装工具集,也可能有人会弄错。 或者可能由于每个人如何定制他们的Linux环境而出现问题。 通过在团队(或其他操作系统)中使用不同的Linux发行版,可以使情况更加复杂。 当需要移至该工具集的下一版本时,这些问题可能很快变得更糟。 使用容器和本文中的指南将使向新版本的迁移更加容易。
遍历容器化的构建系统
我创建了一个教程存储库,您可以稍后克隆和检查该存储库 ,或者继续阅读本文。 我将遍历存储库中的所有文件。 该构建系统故意微不足道(它运行gcc )以将精力集中在构建系统架构上。
建立系统需求
我认为在构建系统中需要考虑的两个关键方面是:
- 标准的构建调用:我希望能够通过指向某个路径为/ path / to / workdir的工作目录来构建代码。 我想将构建调用为:
. / build.sh / path / to / workdir
. / build.sh / path / to / workdir
- 为了使示例架构简单(出于解释目的),我假设输出也在/ path / to / workdir中的某个位置生成。 (否则,这将增加暴露给容器的卷的数量,这并不困难,但解释起来比较麻烦。)
- 通过shell进行自定义构建调用:有时,工具集需要以不可预见的方式使用。 除了调用工具集的标准build.sh之外,如果需要,可以将其中一些添加为build.sh的选项。 但是我始终希望能够获得一个可以直接调用工具集命令的shell。 在这个简单的示例中,我有时想尝试不同的gcc优化选项以查看效果。 为此,我想调用:
. / shell.sh / path / to / workdir
. / shell.sh / path / to / workdir
- 这应该使我进入容器内的Bash shell,可以访问工具集和我的workdir ,因此我可以根据需要尝试使用工具集。
建立系统架构
为了符合上述基本要求,以下是我构建构建系统的方式:
在底部,工作目录代表需要由软件开发人员最终用户构建的任何软件源代码。 通常,此工作目录将是源代码存储库。 最终用户可以在调用构建之前以所需的任何方式来操纵此源代码存储库。 例如,如果他们使用git进行版本控制,则可以git签出正在使用的功能分支并添加或修改文件。 这样可以使构建系统独立于workdir 。
顶部的三个块共同代表了容器化的构建系统。 顶部最左边的(黄色)块表示最终用户将用来与构建系统进行交互的脚本( build.sh和shell.sh )。
中间(红色块)是Dockerfile和关联的脚本build_docker_image.sh 。 开发人员(在本例中为我)通常将执行此脚本并生成容器映像。 (实际上,我将执行很多次,直到一切正常为止,但这是另外一回事了。)然后,我将图像分发给最终用户,例如通过容器信任的注册表。 最终用户将需要此图像。 此外,他们还将克隆构建系统存储库(即与教程存储库等效的存储库 )。
当最终用户调用build.sh或shell.sh时,将在容器内执行右侧的run_build.sh脚本。 接下来,我将详细解释这些脚本。 这里的关键是最终用户不需要使用红色或蓝色块或容器如何工作的任何知识。
建立系统细节
教程存储库的文件结构映射到此体系结构。 我已经将此原型结构用于相对复杂的构建系统,因此其简单性决不是任何限制。 下面,我列出了存储库中相关文件的树形结构。 dockerize-tutorial文件夹可以替换为与构建系统相对应的任何其他名称。 在此文件夹中,我使用一个参数(即workdir的路径)调用build.sh或shell.sh 。
dockerize-tutorial
/
├── build.sh
├── shell.sh
└── swbuilder
├── build_docker_image.sh
├── install_swbuilder.dockerfile
└── scripts
└── run_build.sh
请注意,我故意排除了上面的example_workdir ,您可以在教程存储库中找到它。 实际的源代码通常将驻留在单独的存储库中,而不是构建工具存储库的一部分; 我将其包含在此存储库中,因此在本教程中不必处理两个存储库。
如果您仅对概念感兴趣,则无需进行本教程,因为我将解释所有文件。 但是,如果要继续(并已安装Docker),请首先使用以下命令构建容器映像swbuilder:v1 :
cd dockerize-tutorial
/ swbuilder
/
.
/ build_docker_image.sh
docker image
ls
# resulting image will be swbuilder:v1
然后将build.sh调用为:
cd dockerize-tutorial
.
/ build.sh ~
/ repos
/ dockerize-tutorial
/ example_workdir
下面是build.sh的代码。 该脚本从容器映像swbuilder:v1实例化一个容器。 它执行两个卷映射:一个从example_workdir文件夹到容器内部路径/ workdir的卷 ,第二个从容器外部的dockerize-tutorial / swbuilder / scripts到容器内的/ scripts 。
docker container run \
--volume $
(
pwd
)
/ swbuilder
/ scripts:
/ scripts \
--volume
$1 :
/ workdir \
--user $
(
id
-u
${USER}
) :$
(
id
-g
${USER}
) \
--rm
-it
--name build_swbuilder swbuilder:v1 \
build
此外, build.sh还会调用容器以用户名(和组,本教程假定为相同)运行,以便在访问生成的构建输出时文件权限不会出现问题。
请注意, shell.sh是相同的,除了两件事: build.sh创建一个名为build_swbuilder的容器,而shell.sh创建一个名为shell_swbuilder的容器 。 这样,如果在另一个脚本运行时调用其中一个脚本,则不会发生冲突。
这两个脚本之间的另一个主要区别是最后一个参数: build.sh传递参数build,而shell.sh传递参数shell 。 如果查看用于创建容器映像的Dockerfile ,则最后一行包含以下ENTRYPOINT 。 这意味着上面的docker 容器运行调用将导致以build或shell作为唯一输入参数来执行run_build.sh脚本。
# run bash script and process the input command
ENTRYPOINT
[
"/bin/bash" ,
"/scripts/run_build.sh"
]
run_build.sh使用此输入参数来启动Bash Shell或调用gcc来执行琐碎的helloworld.c项目的构建。 实际的构建系统通常会调用Makefile而不直接运行gcc 。
cd
/ workdir
if
[
$1 =
"shell"
] ;
then
echo
"Starting Bash Shell"
/ bin
/
bash
elif
[
$1 =
"build"
] ;
then
echo
"Performing SW Build"
gcc helloworld.c
-o helloworld
-Wall
fi
如果用例需要,您当然可以传递多个参数。 对于我处理过的构建系统,构建通常是针对具有特定make调用的给定项目。 如果构建系统的构建调用很复杂,则可以让run_build.sh在workdir中调用最终用户必须编写的特定脚本。
关于脚本文件夹的注释
您可能想知道为什么scripts文件夹位于树形结构的深处,而不是位于存储库的顶层。 每种方法都行得通,但是我不想鼓励最终用户四处摸索并在那里进行更改。 将其放置得更深是使它变得更难戳的一种方法。 另外,我可以添加一个.dockerignore文件来忽略scripts文件夹,因为它不必是容器上下文的一部分。 但是因为它很小,所以我没有打扰。
简单而灵活
尽管该方法很简单,但我将其用于一些非常不同的构建系统,并发现它非常灵活。 相对稳定的方面(例如,每年仅更改几次的给定工具集)固定在容器映像中。 更加灵活的方面作为脚本保留在容器映像的外部。 这使我可以通过更新脚本并将更改推送到构建系统存储库来轻松地修改如何调用工具集。 用户所需要做的就是将更改拖到本地构建系统存储库中,这通常非常快(与更新Docker映像不同)。 该结构使其自身具有所需的数量和脚本,同时使最终用户摆脱了复杂性。
翻译自: https://opensource.com/article/20/4/how-containerize-build-system
docker容器构建