持续集成 (Continuous Integration, CI) 存在的意义,是发现代码改动 (Gerrit ticket/Gitlab MR/Github PR, etc.) 所包含的软件问题 (Bug),并阻止这些有问题的代码改动合入代码主干 (Master)。
CI 由一系列任务(例如,Jenkins Job) 组成。一般来说,只有所有任务都成功了,代码改动才能通过验证。任何一个任务的失败,都会导致代码改动无法提交。
然而,这样的 CI 规则基于的是一个潜在的假设,那就是:如果 CI 任务失败,那么失败一定是由触发这次任务的代码改动造成的。
显然,这种假设是过于理想化的,在实践中经常并不成立。因为 CI 是一个系统,被测软件 (测试对象) 只是 CI 的组成部分,而不是 CI 的全部。CI 的失败,完全可能是由被测对象之外的因素导致的。
我们通常用 “稳定性”(Stability) 来衡量 CI 在判断代码改动是否有问题时的表现。对于一个稳定性足够高的 CI 来说,当代码改动没有问题时,它能够稳定地成功;而当代码改动有问题时,它又能够稳定地失败。这是目标。
虽然 CI 的稳定性受多方面因素制约。但是对于 CI 工程师来说,提高 CI*自身*(即被测软件之外的部分) 的质量 (Quality),是一件自主可控的,能够促进 CI 整体更稳定的事情。
然而,就像如何持续提高软件质量是软件工程师面临的重大挑战一样,如何提高 CI 自身的质量,也是 CI 工程师需要面对的重大挑战。
那么,如何打造高质量 CI 呢?这些年,有一个新的理念被提出来,即 “Everything As Code” —— 一切即代码。它被认为是从技术和流程角度来提高 CI 质量的一个重要武器。
这个理念的出发点,是将软件开发的最佳实践(Best Practices) 应用到 CI 中。将 CI 系统的开发/维护,视为特定形态的软件开发/维护;像打造高质量代码一样,打造高质量 CI 系统。
所谓最佳实践,就是在实践中被证明具有良好的效果,并且被业界广泛采纳的经验和方法。那么软件开发有哪些最佳实践呢?
代码集中统一管理:代码集中在一个仓库 (一般是 Git Repository),实现单一代码源(Single Source of Truth),避免出现歧义和混乱;并且代码主干被严格保护,任何针对主干的代码改动都需要经过重重检验,才能被接纳。
分布式开发和协作:多个开发者 (Developer) 分布式地工作在同一个代码仓库上,并且通过 pull, push, rebase, cherry-pick 等操作实现密切协作;开发者提交的任何代码改动,都需要经过其他 (资深) 开发者严格的Code Review (类似于学术论文里的同行评审)。Code Review 极端重要。它既是一种保障代码质量的手段,也是团队成员交流工作,碰撞思想和提高技术的重要途径。
自动化检查和测试:任何的代码改动,都需要经受各种自动化检查和测试的考验,包括但不限于代码风格检查,代码复杂度检查,代码重复度检查,静态分析,单元测试,集成测试,测试覆盖度检查等。它们从不同角度保证代码的可读性,简洁性和 (最重要的) 代码质量。
迭代开发和版本化:软件的开发是迭代和增量式的。每次更新后的软件被打上特定的版本号。版本号随着软件功能的演进而演进。版本化的好处是易于追溯,比较和回退(一种让软件快速恢复工作的手段)。
我们可以看到,上述这些最佳实践基本都是围绕软件质量展开的。遵从这些最佳实践,有助于降低软件开发的风险,提升软件产品的质量。
“Everything As Code” (EAC),一切即代码,就是将 CI 系统代码化,并运用软件开发最佳实践来提升 CI 系统的开发和维护质量。具体来说,EAC 包括以下方面。
CI 管道即代码 (CI Pipeline As Code)。所谓管道 (Pipeline),就是单向的,由一系列顺序的步骤 (Step) 组成的工作流 (Workflow)。工作流中每一步的输出是下一步的输入。CI 管道,就是面向 CI 的工作流。一个典型的 CI 管道如下图所示:
以 Jenkins 为例,实现这么一个管道,传统的方式是将脚本写在 Jenkins Job 配置里。而 EAC 倡导的做法是:采用 Jenkinsfile 来描述 CI 管道,使用 Git 仓库来管理 Jenkinsfile,并且让 Jenkins Job 通过 Git 命令获取 Jenkinsfile。一个典型的 Jenkinsfile 如下:
pipeline {
agent any
stages {
stage('Build') {
steps {
echo 'Building..'
}
}
stage('Test') {
steps {
echo 'Testing..'
}
}
stage('Deliver') {
steps {
echo 'Deploying....'
}
}
}
}
将 CI 管道代码化,能够方便多人维护同一份 CI 脚本,并且可以使用静态检查,测试,Code Review 等手段来检验任何针对 CI 脚本的改动,保障 CI 脚本的质量。同时,对 CI 脚本修改的追溯也更加方便。
配置即代码 (Configuration as Code)。CI 作为一个系统,拥有各种各样配置信息。例如,如果 CI 使用的 Gitlab-Jenkins 技术栈,由于 Gitlab 与 Jenkins Job 之间需要通过 webhook 来通信,因此 webhook 配置管理就成为 CI 工作的一部分。
传统的做法是通过 Gitlab UI 来进行 webhook 的添加,修改和删除。当 webhook 数量很多时,这种做法效率比较低,并且易于出错。针对这种情况,EAC 提倡将 webhook 作为配置文件来管理,并通过 Gitlab API 来获取 webhook 配置文件和使配置文件生效。
将配置代码化的好处是可以将所有的配置集中在一起管理,这样配置改动 (包含添加/修改/删除) 更加方便,改动的内容也更加可见 (Visible)。同时我们可以创建一些测试用例针对配置改动进行测试,避免未经验证的改动直接上线造成事故。
测试用例即代码 (Automated Test Case as Code)。对于与代码强依赖的白盒测试,例如 UT/MT,一般测试用例是 (和软件代码一起) 通过 Git 仓库管理。同样,对于更高级别的测试,例如集成测试/系统测试等,我们也可以将自动化的测试脚本和用例通过 Git 仓库来管理 (与软件代码在同一个仓库,或者在独立的仓库)。
EAC 提倡通过 Git 管理测试脚本,并运用代码风格检查,重复度检查,针对测试用例的测试,Code Review 等手段,提升测试用例的可读性,简洁性和质量。当自动化测试规模较大时,尤其需要采用这种办法。例如,我之前经历的一个自动化测试项目,有 10 多位测试人员,200+ 文件,300+ 用例,30000+ 行脚本,相当于一定规模的软件项目。难以想象如果不采用软件开发的最佳实践,我们如何保障这个自动化测试项目自身的质量。
基础设施即代码 (Infrastructure as Code)。CI 中每一个任务的执行,都离不开特定的执行环境 (基础设施)。那么,如何管理好 CI 的执行环境呢?在我多年的实践中,发现容器化的 CI 执行环境变得越来越重要,越来越不可或缺。容器化可以让环境变得更加可维护,可扩展和可复制。以 Docker 为例,EAC 提倡使用 Dockerfile 去定义执行环境。这样我们可以像管理代码一样,管理 CI 执行环境。创建新环境,给环境安装新的工具,添加新的环境变量等,都是通过修改 Dockerfile(代码) 地形式去完成。并且,可以创建自动化测试 Job,针对修改的 Dockerfile 进行测试,避免未经验证的环境改动直接生效。
文档即代码 (Document as Code)。CI 是服务于整个研发团队的,描述 CI 使用指南的文档是 CI 工作内容的重要一环。EAC 提倡使用 Markdown 等形式编写文档,并与 CI 脚本一样通过 Git 管理文档。这样,当 CI 脚本更新时,CI 文档可以同步更新;当 CI 文档更新时,其他 CI 工程师能够在 Review 脚本改动时,同时 Review 文档的改动。
总结一下, 所谓"Everything As Code",就是将 CI 系统被测软件以外的部分,包括 CI 脚本/配置信息/自动化测试用例/CI 执行环境/CI 文档等代码化,并使用 Git 进行集中管理,同时运用自动化检查/自动化测试/Code Review 等手段,发现 CI 改动潜在的问题,阻止有问题的 CI 改动生效,从而严格保障 CI 自身的质量。
毕竟,只有 CI 自身的质量好了,CI 才能更好地发挥它发现软件代码问题的重要作用。
我是肖哥 shelwin,一个高质量软件工程实践者和推动者。欢迎添加我的个人公众号测试不将就或关注同名博客,谢谢,获得更多自动化测试, 持续集成, 软件工程实践, Python 编程等领域原创文章。