一切即代码:高质量持续集成之道

Everything as Code

Posted by 肖哥shelwin on May 21, 2019

背景

持续集成(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

“Everything As Code” (EAC),一切即代码,就是将CI系统代码化,并运用软件开发最佳实践来提升CI系统的开发和维护质量。具体来说,EAC包括以下方面。

CI管道即代码(CI Pipeline As Code)。所谓管道(Pipeline),就是单向的,由一系列顺序的步骤(Step)组成的工作流(Workflow)。工作流中每一步的输出是下一步的输入。CI管道,就是面向CI的工作流。一个典型的CI管道如下图所示:

pipeline

以Jenkins为例,实现这么一个管道,传统的方式是将脚本写在Jenkins Job配置里。而EAC倡导的做法是:采用Jenkinsfile来描述CI管道,使用Git 仓库来管理Jenkinsfile,并且让Jenkins Job通过Git命令获取Jenkinsfile。一个典型的Jenkinsfile如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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编程等领域原创文章。

公众号