当前位置: 首页 > 科技观察

将构建系统容器化的指南

时间:2023-03-15 01:02:52 科技观察

构建一个可重用的系统以通过容器分发应用程序可能很复杂,但这里有一个好主意。用于将源代码转换为可运行应用程序的构建系统由工具和流程组成。转换还涉及将代码的受众从软件开发人员更改为最终用户,无论最终用户是运营还是部署的同事。在使用容器构建了一些构建系统之后,我认为有一个很好的可重用方法值得分享。虽然这些构建系统用于编译机器学习算法并为嵌入式硬件生成可加载的软件映像,但这种方法足够抽象,可以与任何基于容器的构建系统一起使用。该方法是以易于使用和维护的方式构建或组织构建系统,但不涉及处理特定编译器或工具容器化的技巧。它适用于软件开发人员构建软件并将可维护的映像移交给其他技术人员(无论是系统管理员、运维工程师还是其他职位)的常见情况。构建系统从最终用户那里抽象出来,因此他们可以专注于软件。为什么容器化构建系统?基于容器构建可重用的构建系统可以为软件团队带来很多好处:专注:我想专注于应用程序开发。当我调用一个工具来“构建”时,我希望该工具集能够生成一个随时可用的二进制文件。我不想浪费时间对构建系统进行故障排除。事实上,我宁愿不知道或不关心构建系统。一致的构建行为:无论用例如何,我都想确保整个团队使用相同版本的工具集并在构建时获得相同的结果。否则,我将不得不不断处理“我在这方面做得如何?”的问题。在团队项目中,使用相同版本的工具集并为一组给定的输入源文件生成一致的输出非常重要。易于部署和升级:即使您为每个人提供了一套安装项目工具集的详细说明,也可能有人会失败。对每个人的Linux环境进行单独修改也可能导致问题。当团队中使用不同的Linux发行版(或其他操作系统)时,事情会变得更加复杂。当工具集需要升级到下一个版本时,问题很快就会变得更糟。使用容器和本指南将使升级到新版本变得非常容易。这种将我在项目中使用的构建系统容器化的经验显然很有价值,因为它缓解了上述问题。我倾向于使用Docker作为容器工具,尽管安装和网络配置在相对临时的环境中仍然存在问题,尤其是当您在具有复杂代理的企业环境中工作时。但至少现在我要修复的构建系统问题变少了。通过一个容器化的构建系统,我创建了一个教程存储库,然后您可以克隆并检出它,或者只是跟着做。我将一个一个地浏览存储库中的文件。构建系统非常简单(它运行gcc),让您可以专注于构建系统结构。构建系统要求我认为构建系统中有两个关键点:标准化构建调用:我希望能够指定一些工作目录,如/path/to/workdir来构建代码。我想将构建调用为:./build.sh/path/to/workdir为了使示例的结构足够简单以便说明,我假设输出也在/path/to/workdir下的某处生成小路。(否则,容器中显示的卷数会增加,这并不难,但解释起来比较麻烦。)通过shell自定义构建调用:有时会以意想不到的方式调用工具集。除了标准工具集调用build.sh之外,如果需要,还可以将一些选项添加到build.sh。但是我一直希望有一个可以直接调用工具集命令的shell。在这个简单的示例中,有时我想尝试不同的gcc优化选项并查看效果。为此,我想调用:./shell.sh/path/to/workdir这将使我在容器内获得一个Bashshell,并可以访问工具集和我的工作目录(workdir),这样我就可以尝试使用此工具集。构建系统的架构为了满足以上的基本需求,我的构建系统架构是这样的:Container构建系统架构最底层的workdir代表了软件开发者用来构建的任何软件源代码。通常,此工作目录是源代码存储库。最终用户可以在构建之前以任何方式操作此存储库。例如,如果他们使用git作为他们的版本控制工具,他们可以使用gitcheckout切换到他们正在处理的功能分支并添加或修改文件。这使构建系统独立于工作目录。前三个模块一起代表容器化构建系统。最左边的黄色模块代表最终用户与构建系统交互的脚本(build.sh和shell.sh)。中间的红色模块是Dockerfile和关联的脚本build_docker_image.sh。DevOps(在这个例子中是我)通常会执行这个脚本并生成容器镜像(事实上我多次执行它直到一切正常,但那是另一回事了)。然后我将图像分发给最终用户,例如通过容器可信注册表。最终用户将需要此图像。此外,他们将克隆构建系统的存储库(即相当于教程存储库的存储库)。当最终用户调用build.sh或shell.sh时,右侧的run_build.sh脚本将在容器中执行。接下来我将详细解释这些脚本。这里的关键是最终用户不需要知道任何关于红色或蓝色模块或容器如何工作的信息就可以使用它们。构建系统详细信息将教程存储库的文件结构映射到该系统结构上。我已将此原型结构用于相对复杂的构建系统,因此它的简单性不会造成任何限制。下面我列出了存储库中相关文件的树结构。文件夹dockerize-tutorial可以替换为构建系统的任何其他名称。从这个文件夹中,我调用build.sh或shell.sh并将workdir的路径作为参数。dockerize-tutorial/├──build.sh├──shell.sh└──swbuilder├──build_docker_image.sh├──install_swbuilder.dockerfile└──scripts└──run_build.sh请注意我故意没有列出它位于example_workdir之上,但您可以在教程存储库中找到它。实际的源代码通常在一个单独的存储库中,而不是构建工具存储库的一部分;本教程不必处理两个存储库,因此我将其包含在本教程中。如果您只对概念感兴趣,则不需要本教程,因为我将解释所有文件。但是如果你继续本教程(并且已经安装了Docker),首先使用以下命令构建容器镜像swbuilder:v1:cddockerize-tutorial/swbuilder/./build_docker_image.shdockerimagels#resultingimagewillbeswbuilder:v1然后调用build.sh:cddockerize-tutorial./build.sh~/repos/dockerize-tutorial/example_workdir下面是build.sh的代码。此脚本从容器映像swbuilder:v1实例化一个容器。并且这个容器实例映射了两个卷:一个将文件夹example_workdir挂载到容器的内部路径/workdir,第二个将容器外的文件夹dockerize-tutorial/swbuilder/scripts挂载到容器上级的内部路径/scripts。docker容器运行\--volume$(pwd)/swbuilder/scripts:/scripts\--volume$1:/workdir\--user$(id-u${USER}):$(id-g${USER})\--rm-it--namebuild_swbuilderswbuilder:v1\build另外,build.sh也会使用你的用户名(和组,本教程假定相同)来运行容器,这样你就可以访问构建输出没有发生文件权限问题。请注意,shell.sh和build.sh在很大程度上是相同的,除了两个区别:build.sh创建一个名为build_swbuilder的容器,而shell.sh创建一个名为shell_swbuilder的容器。这样,当一个脚本正在运行而另一个正在被调用时,就不会发生冲突。两个脚本的另一个关键区别是最后一个参数:build.sh传入参数build而shell.sh传入shell。如果您查看用于构建容器映像的Dockerfile,最后一行包含以下ENTRYPOINT语句。这意味着上面的dockercontainerrun调用将以build或shell作为唯一的输入参数来执行run_build.sh脚本。#运行bash脚本并处理输入命令项目。一个真正的构建系统通常会使用Makefiles而不是直接运行gcc。cd/workdirif[$1="shell"];然后echo"StartingBashShell"/bin/bashelif[$1="build"];然后echo"PerformingSWBuild"gcchelloworld.c-ohelloworld-Wallfi使用时,如果需要传入多个参数,当然也是可以的。在我使用过的构建系统中,构建通常是对给定项目的调用。如果构建系统具有非常复杂的构建调用,您可以让run_build.sh调用最终用户在workdir中编写的特定脚本。关于scripts文件夹的注意事项您可能想知道为什么scripts文件夹位于目录树的深处而不是存储库的顶层。无论哪种方式都可以,但我不想鼓励最终用户四处查看和修改其中的脚本。把它放得更深是一种让他们更难翻找的方法。或者,我也可以添加一个.dockerignore文件来忽略脚本文件夹,因为它不是容器的必需部分。但因为它太小了,我没有这样做。简单灵活尽管这种方法很简单,但我已经将它用于几个完全不同的构建系统,并且发现它非常灵活。相对稳定的部分(例如,一年只修改几次的给定工具集)被固定在容器镜像中。比较灵活的部分以脚本的形式放在镜子外面。这使我能够通过修改脚本并将更改推送到构建系统存储库来轻松修改工具集的调用方式。用户需要做的就是将更改拉入本地构建系统存储库,这通常非常快(与更新Docker映像不同)。这种结构使得拥有尽可能多的卷和脚本成为可能,同时将最终用户从复杂性中解放出来。