DevSecOps实战
上QQ阅读APP看书,第一时间看更新

3.3 源代码管理和安全

开发人员根据上一节的内容安全地产出代码后,下一步是对代码进行管理并保证代码的安全,通过梳理不同场景下处理代码的流程,以及代码的进一步评审和改进,更有效地对代码和研发流程进行优化。

3.3.1 源代码安全管理

源代码管理(也称为版本管理)允许开发人员协作处理代码并跟踪更改。源代码管理系统提供代码开发的运行历史,有助于在合并来自多个源的内容时解决冲突,并在需要时恢复项目之前的版本。另外,借助源代码管理系统,团队的所有成员可以一起协作编码,并通过识别谁做出了更改以及做出了哪些更改来快速解决问题。此外,源代码管理系统可以帮助简化流程,并为所有代码提供集中式源代码,成为唯一的代码源从而方便管理。第2章介绍了市场上流行的源代码管理工具Git,以及相应的产品GitHub和GitLab。

源代码属于公司的敏感数据,因此,对于源代码的安全管控可以防止源代码泄露和被轻易更改,从而保护公司的资产。对于源代码的安全管控更多是对于人员和权限的管控。首先,对源代码的访问控制必须严格遵循最小权限原则,并为不同用户账号、服务账号分配不同的适合工作的最小访问权限。另外,要求连接源代码库时必须校验源代码中用户身份及其口令。在源代码库中要求区别对待不同用户的可访问权、可创建权、可编辑权、可删除权、可销毁权。严格控制用户的读写权限,应以最低权限为原则分配权限;开发人员不再需要对相关信息系统源代码做更新时,须及时删除账号。工作任务变化后要实时回收用户账号、服务账号的相关权限,对源代码库的管理要求建立专人管理制度——专人专管。每个普通用户切实保证自己的用户身份和口令不泄露。用户要经常更换自己在源代码库中账号的口令。同时,对源代码的版本控制必须严格执行,以避免采用未经安全扫描检测版本的源代码被发布。

从完整性来说,源代码的存储须严格控制,仅有被安全部门批准的代码库可以用于代码存储。另外,所有针对源代码的改动都必须有记录,能够清楚体现源代码改动的责任人、时间、位置等关键信息,禁止开发人员使用公用账户对源代码进行改动。同时,源代码库必须保存项目开发生命周期内所包含的所有源代码改动及变更记录。除此之外,应当对软件源代码中的变量、函数、过程、控件、注释和排版等制定统一的规范。源代码的标准化在一定程度上可以提高应用系统的可靠性与安全性。

3.3.2 分支策略

说起版本控制,肯定绕不开分支策略话题。就像学开车必须学会交通规则一样,分支策略是代码版本控制的基础组成部分。为团队定制一套合适的分支管理策略,就好比制定了一套合理的交通规则,可以让团队的代码更加有序地演进,尽可能降低多分支带来的复杂度,并避免由于分支混乱而引发的各种“车祸”。因此,分支策略本质上解决了两个主要问题:冲突和返工。通过避免冲突和减少返工,分支策略提升了软件开发质量和效率,并且节省了成本。

冲突一般指一个需求的开发被其他需求开发活动所干扰。比较典型的提交冲突一般是由于多人同时在同一个应用的同一个分支上开发,这时他们的工作会很容易发生冲突。另一种冲突就是合并的冲突,如主干上的某个变更合并到发布过的一个分支上,由于代码的基线不同,产生冲突,无法合并。分支策略是通过合理变更提交约定和约束策略,来避免或减少上述冲突情况发生。返工是在软件开发中严重影响研发效能并且增加研发成本的因素。对于采用窗口制固定时间进行发布策略的团队来说,很多功能变更被提交到待发布分支上,如果此时测试待发布分支,发现某个功能有严重问题时,需要将该问题相关的变更提交从发布分支上移除,不然会应影响其他功能的发布。这个移除返工的过程包括相关变更提交的识别、回滚、重新构建和测试,最后问题修复后再次提交。

在研发过程中,通常会按照以下基本原则进行分支管理:

1)稳定单主干:一个代码仓库应该保有且仅保有一个主干分支,并且此主干分支应该一直都是非常稳定的,也就是仅仅用来发布新版本,而不用来做开发。

2)最少长期分支:在避免冲突的前提下,尽量减少长期分支的数量。

3)配置保护分支:不允许开发者直接提交develop和master分支,需要将其配置为保护分支。

4)配置合并条件:通常为需要通过代码评审或者额外加上自动化代码检查。

5)逐步合并提交代码:如feature->develop->master,避免feature->develop和feature-> master同时存在。

6)发布不可变:发布的版本应该是不可变且可追溯的。

7)自动化事件触发:分支的持续集成过程应该是通过提交事件或制品变更事件自动触发。

常用的分支策略主要分为四大类:主干开发、Git Flow、GitHub Flow和GitLab Flow。开发人员可以根据不同的场景,采取对应的不同的分支开发策略。

1. 主干开发

主干开发模式(图3-1),就是所有开发者都在有且仅有的一个主干分支上进行协作开发的模式。这种模式保有且仅保有一个主干分支进行开发协作。因为没有其他分支,所以在一定程度上避免了合并代码带来的困扰。由于团队共享同一个主干分支进行开发,并且每次代码提交都会触发集成验证,所以要求每次代码的变更在主干上都能快速地验证,以确定是否接受下一次代码变更和提交(每次代码变更都是基于前一个稳定的版本)。为了保证主干开发模式一直处于稳定的可工作状态,这就需要每次的变更要小并且快速完成验证(比如自动化代码评审)。

081-01

图3-1 主干开发模式

场景:主干开发模式一般在项目较小或服务拆分较细且功能明确解耦的条件下,会存在1~2人开发一个应用/服务的情况。

分支类型:

  • master分支:最新代码,在master分支做新功能开发和缺陷修复。
  • release分支:发布最新代码的发布分支。

开发流程:

1)新功能开发时直接在master分支上进行,版本通过tag标注,比如1.1.0。

2)开发提测时,拉取release分支部署到测试环境CIT/SIT。CIT环境用于开发人员的自测(比如冒烟测试或单元测试)。测试中的bug-fix修复在release分支上进行,与新功能开发区分,然后从release分支上拉取代码再一次构建。如果集成测试失败,则必须回滚到上一个版本,除非问题可以及时修复(比如当天)。

3)上线后hot-fix修复也在release分支上进行,与新功能开发区分。

4)上线后必须完成release分支到master分支的合并。

由上可以看出,主干开发非常适用于持续集成,可以根据主干基线做到随时发布,从而实现持续交付。要达到这种程度,需要团队的协作和自动化能力非常成熟,可以快速地对主干的变更提交完成编译、测试和验证。同时,因为采取了发布分支的模式,需要梳理清楚产品版本、分支、部署环境等信息和对应关系,避免发布分支混乱。

2. Git Flow

Git Flow(图3-2)是为了解决多个不同功能之间并行开发所需要的一种工作方式。当开始一个需求开发时,从主干上拉取一个特性分支,所有关于该需求的开发工作都发生在这个特性分支上。当完成该需求开发后,再把特性分支上的代码合并回主分支,并准备发布。除了master和release分支外,Git Flow还有以下几种分支:

  • feature分支:开发者进行功能开发的分支。
  • develop分支:主开发分支,对开发功能进行集成的分支。开发负责人从master上拉取develop分支,正常迭代开发内容在此分支上做提交,然后合并到master分支。
  • hotfix分支:对线上缺陷进行修正工作的分支。
082-01

图3-2 Git Flow

每个特性分支都有属于自己的开发分支,当开发者需要在两个需求上进行工作时,需要通过check out命令在两个分支之间进行切换,其主要目的是防止开发过程中的相互干扰。

Git Flow引入了一种hotfix分支,专门用于线上缺陷的修复。在hotfix分支对问题进行修改及验证完成之后,再集成到develop分支,以及同步到master分支,并且删除hotfix分支。其实可以把hotfix理解为一种专门用来修复缺陷的feature分支,只是它的变更提交在集成到develop分支的同时需要同步到master分支上。发布之前如果发现缺陷,则从release分支上拉出一个hotfix分支。发布之后如果发现缺陷,则基于master分支拉出一个hotfix分支。

Git Flow开发流程可以总结如下:

1)新迭代版本开始时,从master上拉取develop分支并打标签(比如1.6.0)。开发人员再从develop分支上拉取一个feature分支在本地完成编码,调试通过后,提交代码到feature分支,进入代码评审阶段(比如code review),通过合并到develop分支进行集成验证。集成验证完毕,feature分支会被删除。

2)触发CI流水线,部署最新代码到开发环境,开发人员在开发环境自测(比如单元测试或手动测试)。当完成了问题修复和自测成功后,确定本次提测版本号,从develop分支拉出一个release分支作为发布分支。

3)确认功能测试环境部署成功,以及冒烟测试通过,开发进行新一轮测试。测试中的Bug反馈给开发,开发在对应版本的release分支上进行修复并同步给develop分支。

4)测试完成并产出测试报告后,项目负责人评审测试报告并同意发生产。项目负责人确认人工卡点通过,正式流水线自动运行生产部署,即生产发布。对于预生产缺陷线上紧急修复问题,直接在对应版本的release分支上进行修复。完成后运行正式流水线,部署到测试环境测试,测试通过后进行发布。成功发布后将release分支合并到develop和master分支。

Git Flow的分支模式提供了相对全面、完备的分支类型,用来覆盖开发过程中的大部分场景。然而,功能强大的代价是分支模式过于复杂。那有没有既包含对于主线的隔离,又稍微轻量简单一点的分支模式可以使用呢?

3. GitHub Flow

GitHub Flow(图3-3)没有develop、release和hotfix分支,只有master和feature分支。由于GitHub Flow强调进行小的、持续的和快速的发布,因此它认为在这种场景下,变更和缺陷修复没有大的区别,因此所有的改动都在feature分支上进行。

083-01

图3-3 GitHub Flow

GitHub Flow的使用基本原则和整体流程如下:

1)master分支上的代码都应该是最新的、稳定的、可部署的版本。

2)当有一个开发需求时,从master上拉起一个feature开发分支。研发人员持续提交代码变更到所在任务的开发分支上。

3)当任务完成准备合并代码到master主干分支时,通过发起pull request,提交代码评审。

4)通过代码评审,并且feature开发分支被部署到测试环境进行验证后,合并到master分支。

5)如果评审及验证通过,则代码合并到master主干上,立即部署到生产环境。

GitHub Flow相比Git Flow简单了很多,适用于持续交付,可尽快地发现master分支的问题,并能通过rollback等机制,快速恢复。GitHub Flow实现了非常频繁、快速、简单的部署,意味着可以最小化未发布的代码量,这也是精益和持续交付所倡导的最佳实践。

4. GitLab Flow

GitLab Flow(图3-4)在开发侧与GitHub Flow区别不大,只是将pull request改成了merge request。最大的区别在于发布侧,即引入了对应生产环境的production分支和对应准生产环境的pre-production分支。这样的话,master分支对应的是部署在集成环境上的代码,pre-production对应的是部署在准生产(预发布)环境的代码,production对应的是部署在生产环境的代码。

084-01

图3-4 GitLab Flow

当一个需求被开发完成后,提交merge request,将代码合并到master,并部署在集成环境测试验证。当测试验证通过之后,提交merge request,将master代码合并到pre-production分支,并部署在预发布环境,在预发布环境上进行测试验证。当预发布环境测试验证成功后,再提交merge request,将pre-production分支上的代码合并到production分支。除了这种按环境将主干发布向下游合并并按顺序部署发布的过程,GitLab Flow同样支持不同版本的发布分支,即不同的版本会从master上拉出发布分支,不同的发布分支再以pre-production分支和production分支的方式进行发布。从GitLab Flow的工作流程可以看出,GitLab Flow在发布侧做了更多的工作。同样GitLab Flow因为与GitLab工具强依赖,所以GitLab Flow与GitLab中的issue系统也有很好的集成,在其推荐的工作模式中每次新建一个新的feature分支,都是从一个issue发起的,即建立了issue与feature开发分支之间的映射。

实践中,企业需要根据场景判断哪种分支策略更适合自己,甚至有时需要基于现有分支策略,定制适合场景的特殊分支策略。表3-1对比了四种分支模式的优点和缺点。

表3-1 主干开发、Git Flow、GitHub Flow和GitLab Flow的对比

085-01

了解了常见的分支模式后,就可以根据自身业务特点和团队规模来选择适合的实践,总之,没有绝对好的模式,只有最适合的模式。一旦选择了某个分支模式,就要保证切实执行,并定期评审,确保现有分支模式符合当前研发现状需要。关于如何选择合适分支模式,可以大体考量表3-2中的几个参数。

表3-2 主干开发、Git Flow、GitHub Flow和GitLab Flow对应的场景

085-02

GitHub Flow和主干开发对持续集成和自动化测试等基础设施有着比较高的要求,所以如果相关基础设施不完善,则不建议使用。如果实际中由于场景特殊,造成了以上四种主流分支策略都无法满足要求,则也可以定义自己的分支模式。

3.3.3 代码评审

虽然代码质量分析工具可以在代码规范、代码重复、复杂度等方面帮助开发人员发现并修正问题,但涉及业务逻辑和可读性等方面的建议,机器还是无法完全取代人的。代码评审(Code Review)是通过团队里的人力资源,人工审核相关代码的可读性和变更的合理性以及效能等;代码评审是DevOps开发的一个重要环节,是保障代码质量的重要手段之一。Google引入Code Review的初衷就是为了保证代码具有良好的可读性。有效的代码评审可降低故障率。

代码评审机制是通过在源代码管理工具中设置卡点来实现的。比如发布分支不允许任何人直接拉取变更,而必须通过代码评审才能合并,包括一些合并的卡点条件。合并检查与分支权限协同管控,能为团队提供更加灵活可控的开发工作流。

  • 卡点设置。要求合并前需要通过代码评审。可设置人工评审卡点,如评审需要的最少通过人数、库内什么角色能通过等。
  • 评审人选择。不同分支可能存在不同的负责人,这些负责人可以被配置为默认的评审人之一,达到创建即指派分支评审人的效果。另外,被修改代码的文件的owner也可以作为评审人,因为他们可能是最熟悉这份代码的人。

另外也可以在人工评审前进行pre-commit自动化检查,比如进行代码规范、代码质量等自动化形式的检查并设置卡点,从而实现机器和人对代码的双重评审。pre-commit是指发起MR(合并请求)时触发代码质量和安全分析,将分析结果返回到代码托管平台,同时设置质量门禁(比如严重代码质量问题和高危安全漏洞等)。如果结果不满足质量红线的要求,则禁止代码合并。这种MR阶段的代码分析和质量门禁设置,将常见的CI阶段的代码分析和质量门禁设置左移到代码合并阶段,从而保证了开发分支的稳定性和减少了修复成本。

为了保证代码的质量和安全问题不被引入生产环境,需要提前进行检查。越早进行检查,引入的风险越小,成本也越低。因此,在每次代码提交或者合并请求时进行代码检测,从起点发现并扼杀问题,保障后续应用研发流程的稳定性。代码评审相关的最佳实践总结如下。

1. 将提交做小做好

为了让评审者能够清晰地理解代码变更的背景和信息,每次代码提交量尽量小,比如每次提交只修改一个到几个文件,每次只修改几行到几十行。小的提交目的就是将问题解耦——“Do one thing and do it well”。每次提交应该是一个完整的功能,可能是修改某个Bug或完成某个小需求的开发,commit message记录本次commit的详细说明,大体分为提交标题、主体body和sign签名等。

2. 详细充分描述

沟通的基础是基于有效信息量,这就要求评审时列出的问题描述能尽量充分和详尽。否则,很容易造成评审人员的错误理解而需要进一步沟通,从而影响沟通效率。因此,对于代码评审的描述应尽可能包含需求背景、改动点、影响范围和改动理由等。一段清晰的评审描述能让评审人员充分理解需求背景,快速开始评审,降低沟通成本。同样,对于评审人员评审过程中留下的评论、修改建议,以及问题的描述也应当描述充分,方便理解。

3. 少食多餐,小步快跑

在真正的代码评审实践中,经常会碰到需要评审的代码内容较多和改动较大的情况,一次评审规模越大,则越耗时,成本也会越高。正确的方法是将大规模的代码评审过程进行拆解,因此要求每次提交的内容也尽可能的小而独立,最终将代码评审当作一个“日常习惯”而不是一个“盖章动作”。另外,卡着时间、临上线前做代码评审非常不可取,这样留给评审人的时间非常有限,往往达不到预期的效果。推荐做法是当程序员做完一个小的提交后,就开始进行代码评审。

4. 问题追踪和闭环

虽然做代码评审的人员一般都在同一个团队,方便沟通,但事后仍要把评审讨论中出现的问题记录下来,方便对问题进行分析和沉淀,最终形成闭环,从而影响团队以及后续的工作。并且使用工具对这些问题的状态进行跟踪,确保问题最终得以解决。

5. 度量和反馈

通过度量数据分析团队代码质量,以及代码质量改进的情况,基于问题有策略地减少技术负债和提升团队研效能力。