顺便说一句,我们正在Doximity雇用Infra,SRE,Web,移动和数据工程师(请参阅角色),以了解有关我们技术堆栈的更多信息。
在Doximity,我们正在Kubernetes上运行越来越多的应用程序和服务。为了帮助我们的团队更快地移动,我们围绕Kubernetes构建了一个平台,可将代码从开发人员的笔记本电脑快速安全地获取到云中。
部署到我们的平台时,第一步是将应用程序容器化。打包应用程序使我们可以通过打包应用程序及其依赖项来以可重复且可靠的方式部署和运行它。容器化还包括做出配置选择,使应用程序能够在容器环境中更好地运行,例如登录到stdout。容器化应用程序的结果是一个图像。一旦团队有了映像,他们就可以在平台上运行其应用程序。但是,他们如何获得该图像?
让我们从头开始。随着越来越多的开发团队将其应用程序移至我们的平台上,他们需要一种持续构建和部署其容器映像的方法。我们通过将其构建到平台中消除了这种负担。我们研究了将应用程序源代码转换为容器映像的不同方法。最受欢迎的两个是Dockerfiles和buildpacks。在这篇文章中,我将解释我们在平台中所做的选择,以将我们的开发团队从Dockerfiles迁移到buildpacks。
Dockerfile是一个文本文件,其中包含将由Docker执行以构建容器映像的命令。 Dockerfile始终以FROM指令开头,该指令指定要从其开始的基本映像。后续命令建立在该基础映像之上并对其进行修改。
下面是一个例子。该Dockerfile使用ruby基本映像。大部分Dockerfile包含用于添加构建和运行应用程序所需的库和包的命令。如果您了解BuildKit,则可以配置这些命令以优化缓存利用率和其他功能。 Dockerfile的最后一部分是启动映像时默认运行的内容。
FROM ruby:2.6.6-alpine3.12#安装工具RUN apk添加--no-cache --update tool = v1.2.3 \ more-tools = v3.4.5 \ extra-tools = v6.7.8#设置非root用户用户RUN addgroup -g 1000 -S app&& adduser -u 1000 -S -D -G app app#创建一些文件夹,安装bundler,配置bundler,运行bundle install,移动一些文件,设置一些环境变量,配置yarn,配置注册表访问,…#缓存示例-优化的命令RUN --mount =类型= cache,uid = 1000,gid = 1000,target = / app / .cache / yarn \ --mount =类型= cache,uid = 1000,gid = 1000,target = / app /.cache/node_modules \ --mount =类型= cache,uid = 1000,gid = 1000,target = / app / tmp / cache \ yarn install --check-files --non-interactive --production --modules-文件夹/app/.cache/node_modules --verbose&& \ cp -ar /app/.cache/node_modules node_modules&& \ SECRET_KEY_BASE =基于某些秘密密钥的包exec rails资产:预编译ENV PORT 8080 EXPOSE 8080 CMD [" bundle&#34 ;," exec&#34 ;," rails&#34 ;, "服务器"]
如果您曾经使用过Heroku或Cloud Foundry,那么您已经使用过buildpack。
buildpack是将源代码转换为可运行容器映像的程序。通常,构建包会封装单个语言生态系统工具链。有用于Ruby,Go,NodeJ,Java,Python等的buildpack。
这些构建包可以组合在一起,称为“构建器”。在构建器中,每个buildpack将检查应用程序源代码并检测是否应参与构建过程。例如,Go构建包将查找以* .go结尾的文件,而Ruby构建包将查找* .rb文件。一旦一个构建包(或一组构建包)匹配,它将继续进行构建步骤。那就是buildpack完成工作的时候。它可能会在容器映像中添加一个包含依赖项的层(例如Go发行版),或者运行一个命令(例如go build)。
这些天有些地方可以找到buildpack。构建包思想的发起者Heroku有自己的存储库。最近,Google已开始将buildpack集成到他们的许多云产品中。我们出于某些原因选择使用Paketo构建包作为平台。 Paketo项目是开源的,由Cloud Foundry基金会支持,并拥有一个由VMware赞助的全职核心开发团队。他们选择创建许多小型的模块化构建包,可以对其进行混合,匹配和扩展,以服务于许多不同的用例。在幕后,Paketo构建包实现了Cloud Native Buildpacks规范,这意味着我们可以编写自己的构建包或从可能适合其他需求的不同项目中找到构建包。他们还真正致力于围绕buildpack建立一个广泛的社区。实际上,我们已经为Ruby生态系统贡献了我们的第一个buildpack!
Paketo有自己的一组参考构建器(微小,基本或完整),包括对Java,.NET Core,NodeJS,Go,PHP,Ruby和使用NGINX或Apache HTTPD的静态文件部署的支持。对于我们的平台,我们想针对我们用来构建应用程序的构建包量身定制一组可用的构建包。为此,我们创建了一个定制构建器。我们的构建器包括Go和Ruby Paketo构建包。
除了buildpacks之外,构建器还引用了另一个称为栈的容器映像。您可以像使用Dockerfile中FROM指令定义的基本映像那样来考虑堆栈。它是构建包可以构建在其上的入门映像。堆栈由构建时映像和运行时映像组成。当构建包正在分析应用程序源代码并构建应用程序的容器映像时,将使用前者。后者用作最终应用程序映像的基础。就像构建器一样,可以修改堆栈,并且我们基于Paketo完整堆栈创建了自己的堆栈,并进行了一些修改。现在,我们将开发团队使用的构建包放到了一个构建器中,该构建器运行在根据其需求定制的堆栈之上,我们准备开始构建应用程序。
不过,让我们退后一步,花点时间解释为什么我们选择将buildpack集成到我们的平台中。
在做出此决定时,我们的主要关注点是开发人员的生产力。我们希望以最小的负担将开发团队加入新平台。
我们的第一种方法很简单。我们要求开发团队在其源代码中包含一个Dockerfile。该平台将在部署之前使用该Dockerfile构建其应用程序容器映像。在一些团队完成入职之后,我们发现了这种方法的一些问题。并不熟悉Docker和Dockerfile语法。即使是有经验的人,也可能不具备编写可以快速构建(和重建)小型安全映像的Dockerfile的能力。这是一种易于在开发组织中“培养”的知识类型。
显然,我们的开发团队需要“工作正常”的东西,并且从头开始开发和优化自己的Dockerfile的成本很高。这正是buildpack最有意义的地方。使用buildpack,我们的开发团队无需编写任何东西。他们无需担心应用程序映像的大小和安全性。而且,他们无需担心优化构建和重建时间。构建包可以为他们做出那些决定。而且我们可以依靠一个开源项目及其社区来为使这些buildpack正常工作做出贡献。
尽管有些人可能会发现使用buildpacks与Dockerfiles的完全自由相比是限制性的,但我们的大多数开发人员都将这种自由视为负担。他们不想继续关注语言运行时漏洞,也不想找出在不损失构建时间速度的情况下保持图像较小的最佳方法。作为平台运营商,选择buildpack对我们也意味着巨大的效率。如果我们确定需要更新的依赖项,则无需向十二个存储库发出拉取请求来更新以十二种不同方式编写的十二个Dockerfile。取而代之的是,我们将buildpack或stack升级到了较新的版本,并与构建器一起推出了更改。
通过迁移到构建包,我们为希望将其应用程序迁移到我们平台的团队消除了很多入门障碍。
现在我们已经有一些团队在新平台上运行他们的应用程序,我们希望确保他们能够安全地执行此操作。许多现代应用程序包含数十个(即使不是数千个)依赖项,这些依赖项执行许多标准的管道和基础设施问题。我们的应用程序没有什么不同。掌握所有这些依赖项的新功能,错误修复和漏洞警报是一项全职工作。我们想要一个可以减轻开发团队负担的解决方案。
同样,这是buildpack的亮点。 Paketo构建包不断更新上游语言,运行时和框架,以响应漏洞,错误修复和新功能更新。他们的核心开发团队构建了自动化工具,该工具可以监视那些依赖项以寻找新版本并在需要时进行更新。对于像Ruby这样的buildpack,该团队具有自动化的功能,可以轮询Ruby运行时的发布站点,以确保其在堆栈中执行得很好,并使用自动拉取请求更新buildpack。在许多情况下,这些新依赖项几乎可以在应用程序发布后立即提供给我们的应用程序开发人员使用。
Cloud Native Buildpack项目还提供了其他选择,这些选择可以使默认情况下的构建包更安全。在堆栈顶部构建和运行应用程序时,构建包将以专用的非root用户身份执行。每个堆栈映像都有详细的元数据,这些元数据描述映像的组件,例如基本操作系统和软件包。每个堆栈都有用于构建和运行应用程序的单独映像。选择了运行时映像上的软件包,以排除可能带来安全风险的编译器和其他工具。您可以通过检出Paketo堆栈存储库来了解更多信息。
一旦我们的开发团队开始将他们的应用程序部署到我们的平台上,我们就想确保他们可以继续快速地完成工作。编写一个Dockerfile来确保重建后的图像能够快速有效地执行,这可能也是“黑暗的艺术”。您不仅需要知道如何在映像中获得依赖关系,还需要知道如何在需要后续重建时将其保留在高速缓存中,并且还需要知道何时将该高速缓存扔掉。将这些知识乘以我们通常在应用程序中看到的数十种依赖项。不难看出为什么我们的开发团队会努力保持其部署速度。
Buildpacks将这些知识编码到其实现中。当应用程序的Gemfile.lock更改时,Ruby buildpack知道需要重建包含所有应用程序Ruby gem的层。它还知道如何做到这一点,以保持不变的宝石。这意味着构建可以快速有效地进行。我们并没有浪费开发人员等待一遍又一遍地下载和安装相同依赖项的时间。
作为平台运营商,我们还希望获得性能上的好处。 Buildpacks可以创建可以在应用程序之间共享的图层。例如,如果我们所有的应用程序都使用相同版本的Go,则在我们的图像注册表中仅创建一层具有该版本的Go。在Kubernetes节点上,该层上只有一个副本,该节点上运行的所有容器实例都可以读取该副本。如果已经在该节点上运行的另一个应用程序正在该节点上运行,则无需获取该层。所有这些意味着我们可以有效利用注册表所占用的空间,减少Kubernetes节点上应用程序的磁盘使用量,并减少部署时间。
buildpack和Dockerfile之间的许多比较都可以归结为“常规与配置”。这是用乐高积木而不是Play-Doh建造的问题。这两个想法都有自己的位置。在选择喜欢约定的项目时,我们希望确保不会妨碍我们进行所有配置。 Paketo核心开发团队进行了适当的权衡,创建了易于组合,可配置和可替换的构建包。
从技术上讲,这意味着我们的开发团队用来构建Ruby应用程序的构建包实际上由许多较小的构建包组成。这些构建包处理构建过程中的离散部分,例如安装Bundler,使用Rails Asset Pipeline编译静态资产,或声明应用程序应使用的运行命令。这些部分中的每一个都可以放在一起,从而提供并需要构建环境的各个部分。为我们的开发团队定制更大的Ruby经验可能意味着添加,删除或重组这些片段,以更好地满足他们的需求。
今天,我们使用一个自定义构建器,其中包含我们开发团队关心的构建包。我们已经选择了适合他们的作品,并且在我们进一步了解它们的特定需求时,我们将继续自定义和精简该组构建包,以使我们的图像构建体验更加有用,高效和安全。
除了使用Paketo项目提供的构建包外,我们还开始构建自己的构建包。让我告诉您一些有关我们的第一个定制buildpack以及我们与Paketo核心团队合作在上游做出贡献的经验。
Paketo项目虽然仍是一个相对较新的社区,但仍在不断发展,我们与核心团队合作的经验非常有成效。当我们第一次开始探索使用buildpacks构建Ruby应用程序时,我们发现buildpack缺少我们需要的功能。我们不是唯一的。已经存在描述该问题的GitHub公开问题。在构建过程中,我们需要Ruby buildpack进行资产预编译。经过Ruby buildpack维护者的一些指导(感谢Sophie Wigmore和Ryan Moran),我们决定通过创建rails-assets buildpack为Paketo项目做出贡献!
我们感谢能够轻松地为Paketo项目做出贡献并与之合作。它们使我们能够贡献新功能,而无需继续构建,维护和定期发布新版本。
除了构建包本身之外,Cloud Native Buildpacks项目还使社区能够开发名为kpack的Kubernetes本地容器构建服务。该服务提供了声明性的Kubernetes资源定义,用于将源代码映射到buildpack,然后再映射到可以在我们的平台上运行的映像。它会不断监视应用程序源代码,buildpack和堆栈,并在发生更改时重建我们的映像。作为一个平台团队,拥有一个与在其上运行的应用程序一样易于维护和升级的平台,对于我们成功保持其运行至关重要。
在Doximity,我们致力于为临床医生打造优质产品。在内部,我们正在构建一个平台,以帮助我们的团队更有效地交付这些产品。在此过程中,我们对如何导航容器图像构建生态系统进行了很多思考。我们决定优先于基于Dockerfile的基于buildpacks的映像构建工作流。我希望这对您决定自己的平台有帮助。
如果您希望收到有关新博客文章的通知,请务必关注@doximity_tech。