对齐部署镜像和描述符是很困难的,但是某些策略可以使整个过程更高效。
在软件架构中,当两个组件之间有某些概念性或技术上的差异时会出现 阻抗失配impedance mismatch。这个术语其实是从电子工程中借用的,表示电路中输入和输出的电子阻抗必须要匹配。
在软件开发中,存储在镜像仓库中的镜像与存储在源码控制管理系统(LCTT 译注:SCM,Source Code Management)中它的部署描述符deployment descriptor之间存在阻抗失配。你如何确定存储在 SCM 中的部署描述符表示的是正确的镜像?两个仓库追踪数据的方式并不一致,因此将一个镜像(在镜像仓库中独立存储的不可修改的二进制)和它的部署描述符(Git 中以文本文件形式存储的一系列修改记录)相匹配并不那么直观。
注意:本文假定读者已经熟悉以下概念:
- 源码控制管理Source Control Management(SCM)系统和分支
- Docker 或符合 OCI 标准的镜像和容器
- 容器编排系统Container Orchestration Platforms(COP),如 Kubernetes
- 持续集成/持续交付Continuous Integration/Continuous Delivery(CI/CD)
- 软件开发生命周期Software development lifecycle(SDLC)环境
阻抗失配:SCM 与镜像仓库
为了更好地理解阻抗失配在什么场景下会成为问题,请考虑任意项目中的软件开发生命周期环境(SDLC),如开发、测试或发布环境。
测试环境不会有阻抗失配。现在使用 CI/CD 的最佳实践中开发分支的最新提交都会对应开发环境中的最新部署。因此,一个典型的、成功的 CI/CD 开发流程如下:
- 向 SCM 的开发分支提交新的修改
- 新提交触发一次镜像构建
- 新生成的镜像被推送到镜像仓库,标记为开发中
- 镜像被部署到容器编排系统(COP)中的开发环境,该镜像的部署描述符也更新为从 SCM 拉取的最新描述符。
换句话说,开发环境中最新的镜像永远与最新的部署描述符匹配。回滚到前一个构建的版本也不是问题,因为 SCM 也会跟着回滚。
最终,随着开发流程继续推进,需要进行更多正式的测试,因此某个镜像 —— 镜像对应着 SCM 中的某次提交 —— 被推到测试环境。如果是一次成功的构建,那么不会有大问题,因为从开发环境推过来的镜像应该会与开发分支的最新提交相对应。
- 开发环境的最新部署被允许入库,触发入库过程
- 最新部署的镜像被标记为测试中
- 镜像在测试环境中被拉取和部署,(该镜像)对应从 SCM 拉取的最新部署描述符
到目前为止,一切都没有问题,对吗?如果出现下面的场景,会有什么问题?
- 场景 A:镜像被推到下游环境,如用户验收测试user acceptance testing(UAT),或者是生产环境。
- 场景 B:测试环境中发现了一个破坏性的 bug,镜像需要回滚到某个确定正常的版本。
在任一场景中,开发过程并没有停止,即开发分支上游有了一次或多次新的提交,而这意味着最新的部署描述符已经发生了变化,最新的镜像与之前部署在测试环境中的镜像不一致。对部署描述符的修改可能会也可能不会对之前版本的镜像起作用,但是它们一定是不可信任的。如果它们有了变化,那么它们就一定与目前为止你测试过的想要部署的镜像的部署描述符不一致。
问题的关键是:如果部署的镜像不是镜像库中的最新版本,你怎么确定与部署的镜像相对应的是 SCM 中的哪个部署描述符? 一言以蔽之,无法确定。两个库直接有阻抗失配。如果要详细阐述下,那么是有方法可以解决的,但是你需要做很多工作,这部分内容就是文章接下来的主题了。请注意,下面的方案并不是解决问题的唯一办法,但是已经投入到生产环境并已经对很多项目起了作用,而且已经被构建并部署到生产环境中运行了超过一年。
二进制与部署描述符
源码通常被构建成一个 Docker 镜像或符合 OCI 标准的镜像,该镜像通常被部署到一个容器编排平台(COP)上,如 Kubernetes。部署到 COP 需要部署描述符来定义镜像被如何部署以及作为容器运行,如 Kubernetes 部署 或 CronJobs。这是因为在镜像和它的部署描述符之间有本质差异,在这里可以看到阻抗失配。在这次讨论中,我们认为镜像是存储在镜像仓库中不可修改的二进制。对源码的任何修改都不会修改镜像,而是用另一个新的镜像去替换它。
相比之下,部署描述符是文本文件,因而可以被认为是源码且可修改。如果遵循最佳实践,那么部署描述符是被存储在 SCM,所有修改都会提交,而这很容易回溯。
解决阻抗失配
建议的解决方案的第一部分,就是提供一个能匹配镜像仓库中的镜像与对保存部署描述符的 SCM 做的代码提交的方法。最直接的解决方案是用源提交的哈希值标记镜像。这个方法可以区分不同版本的镜像、容易分辨,并且提供足够的信息来查找正确的部署描述符,以便镜像更好地部署到 COP。
再回顾下上面的场景:
- 场景 A 镜像被推到下游环境: 当镜像被从测试环境推到 UAT 环境时,我们可以从镜像的标签中知道应该从 SCM 的哪一次源码提交拉取部署描述符。
- 场景 B 当一个镜像需要在某一环节中回滚:无论我们选择回滚到那个镜像版本,我们都可以知道从 SCM 的哪一次源码提交拉取正确的部署描述符。
在每一种情景中,无论在某个镜像被部署到测试环境后开发分支有多少次提交和构建,对于每一次升级的镜像,我们都可以找到它当初部署时对应的部署描述符。
然而,这并不是阻抗失配的完整解决方案。再考虑两个场景:
- 场景 C 在负载测试环境中,会尝试对不同的部署描述符进行多次部署,以此来验证某一次构建的表现。
- 场景 D 一个镜像被推送到下游环境,在该环境中部署描述符有一个错误。
在上面的所有场景中,我们都需要修改部署描述符,但是目前为止我们只有一个源码提交哈希。请记住,最佳实践要求我们所有对源码的修改都要先提交到 SCM。某次提交的哈希本身是无法修改的,因此我们需要一个比仅仅追踪原来的源码提交哈希更好地解决方案。
解决方案是基于原来的源码提交哈希新建一个分支。我们把这个分支称为部署分支。每当一个镜像被推到下游测试或发布环境时,你应该基于前一个 SDLC 环境的部署分支的最新提交创建一个新的部署分支。
这样同一个镜像可以重复多次部署到不同的 SDLC 环境,并在后面每个环境中可以感知前面发现的改动或对镜像做的修改。
注意: 在某个环境中做的修改是如何影响下一个环境的,是用可以共享数据的工具(如 Helm Charts)还是手动剪切、粘贴到其他目录,都不在本文讨论的范围内。
因此,当一个镜像被从一个 SDLC 环境中推到下一环境时:
创建一个部署分支
- 如果镜像是从开发环境中推过来的,那么部署分支就基于构建这个镜像的源码提交哈希创建
- 否则,部署分支基于当前部署分支的最新提交创建
镜像被部署到下一个 SDLC 环境,使用的部署描述符是该环境中新创建的部署分支的部署描述符
图 1:部署分支树
- 部署分支
- 下游环境的第一个部署分支,只有一次提交
- 下游环境的第二个部署分支,只有一次提交
有了部署分支这个解决方案,再回顾下上面的场景 C 和场景 D:
- 场景 C 修改已经部署到下游 SDLC 环境中的镜像的部署描述符
- 场景 D 修复某个 SDLC 环境中部署描述符的错误
两个场景中,工作流如下:
- 把对部署描述符做的修改提交到 SLDC 环境和镜像对应的部署分支
- 通过部署分支最新提交对应的部署描述符把镜像重新部署到 SLDC 环境
这样,部署分支彻底解决了(存储着代表一次独一无二的构建的单一的、不可修改的镜像的)镜像仓库与(存储着对应一个或多个 SDLC 环境的可修改的部署描述符的)SCM 仓库之间的阻抗失配。
实践中的思考
这看起来像是行得通的解决方案,但同时它也为开发者和运维人员带来了新的实践中的问题,比如:
A. 为了更好地管理部署分支,部署描述符作为资源应该保存在哪里,是否要与构建镜像的源码保存在同一个 SCM 仓库?
到目前为止,我们都在避免谈论应该把部署描述符放在哪个仓库里。在还没有太多细节需要处理时,我们推荐把所有 SDLC 环境的部署描述符与镜像源码放在同一个 SCM 仓库。当部署分支创建后,镜像的源码可以作为方便找到部署的容器中运行的镜像的引用来使用。
上面提到过,可以通过镜像的标签来关联镜像与原始的源码提交。在一个单独的仓库中查找某次提交的源码的引用,会给开发者带来更大的困难(即便借助工具),这就是没有必要把所有资源都分开存储的原因。
B. 应该在部署分支上修改构建镜像的源码吗?
简答:不应该。
详细阐述:不应该,因为永远不要在部署分支上构建镜像,它们是在开发分支上构建的。修改部署分支上定义一个镜像的源码会破坏被部署的镜像的构建记录,而且这些修改并不会对镜像的功能生效。在对比两个部署分支的版本时这也会成为问题。这可能会导致两个版本的功能差异有错误的测试结果(这是使用部署分支的一个很小的额外好处)。
C. 为什么使用镜像 标签tag?标记label 不可以吗?
通过 标签tag 可以在仓库中很容易地查找镜像,可读性也很好。在一组镜像中读取和查找 标记label 的值需要拉取所有镜像的清单文件manifest,而这会增加复杂度、降低性能。而且,考虑到历史记录的追踪和不同版本的查找,对不同版本的镜像添加 标签tag 也很有必要,因此使用源码提交哈希是保证唯一性,以及保存能即时生效的有用信息的最简单的解决方案。
D. 创建部署分支的最佳实践是怎样的?
DevOps 最重要的三个原则:自动化、自动化、自动化。
依赖资源来持续地强迫遵循最佳实践,充其量只是碰运气,因此在实现镜像的升级、回滚等 CI/CD 流水线时,把自动化部署分支写到脚本里。
E. 对部署分支的命名规范有建议吗?
<部署分支标识>-<环境>-<源码提交哈希>
- 部署分支标识: 所有部署分支范围内唯一的字符串;如 “deployment” 或 “deploy”
- 环境: 部署分支适用的 SDLC 环境;如 “qa”(测试环境)、 “stg”(预生产环境)、 或 “prod”(生产环境)
- 源码提交哈希: 源码提交哈希中包含原来构建被部署的镜像的源码,开发者可以通过它很容易地查找到创建镜像的原始提交,同时也能保证分支名唯一。
例如, deployment-qa-asdf78s 表示推到 QA 环境的部署分支, deployment-stg-asdf78s 表示推到 STG 环境的部署分支。
F. 你怎么识别环境中运行的哪个镜像版本?
我们的建议是把最新的部署分支提交哈希和源码提交哈希添加到 标记 中。开发者和运维人员可以通过这两个独一无二的标识符查找到部署的所有东西及其来源。在诸如执行回滚或前滚操作时,使用那些不同版本的部署的选择器也能清理资源碎片。
G. 什么时候应该把部署分支的修改合并回开发分支?
这完全取决于开发团队。
如果你修改的目的是为了做负载测试,只是想验证什么情况会让程序崩溃,那么这些修改不应该被合并回开发分支。另一方面,如果你发现和修复了一个错误,或者对下游环境的部署做了调整,那么就应该把部署分支的修改合并回开发分支。
H. 有现成的部署分支示例让我们试水吗?
el-CICD 已经在生产上使用这个策略持续一年半应用到超过一百个项目了,覆盖所有的 SDLC 环境,包括管理生产环境的部署。如果你可以访问 OKD、Red Hat OpenShift lab cluster 或 Red Hat CodeReady Containers,你可以下载el-CICD 的最新版本,参照 教程 来学习部署分支是何时以怎样的方式创建和使用的。
结语
通过实践上面的例子可以帮助你更好的理解开发过程中阻抗失配相关的问题。对齐镜像和部署描述符是成功管理部署的关键部分。