对DevOps流水线设计的优化和改进实践(201014)
对于DevOps过程支撑平台,我在前面已经写过相应的文章。在整个DevOps平台的建设过程中可以看到持续集成和持续交付始终都是平台的一个重要内容。而在整个持续集成和交付过程中,流水线设计又是相对关键的一个内容。
通过流水线设计可以很灵活的通过可视化配置的方式,将我们软件持续集成中涉及到的编译构建,打包,部署,代码检查,测试,环境迁移等各种活动编排在一起,形成一个自动化执行的完成流程。通过流水线的运行,即可以实现整个软件开发,集成和交付过程的完整自动化。
DevOps流水线概述
流水线,简单来说就是一系列组装在一起的可以执行的活动或作业。
Pipeline 流水线是指软件从版本控制库到用户手中这一过程的自动化实现是持续交付与 DevOps 的核心工程实践;Pipeline 流水线的自动化和持续流动,才能保证在不同阶段、不同节点上产品发布的一致性和稳定性,同时,也才能消除由于人工操作所引入的人为风险,同时提高效率,消除“等待”与“浪费”。
对于DevOps流水线,主要是由各类任务串联起来,而对于任务本身又分为两种类型,一种是自动化任务,一种是人工执行任务。具体如下:
- 自动化任务:包括了代码静态检查,构建,打包,部署,单元测试,环境迁移等。
- 人工任务:人工任务主要包括了检查审核,打标签基线,组件包制作等类似工作。
而通常我们看到的流水线基本都由上述两类任务组合编排而成,一个流水线可以是完全自动化执行,也可以中间加入了人工干预节点,在人工干预处理后再继续朝下执行。
比如流水线中到了测试部署完成后,可以到测试环境人工验证环节,只有人工验证通过再流转到迁移发布到生产环境动作任务。
DevOps流水线实际上和我们原来经常谈到的持续集成最佳实践是相当类似的,较大的一个差异点就在于引入了容器化技术来实现自动化部署和应用托管。至于在DevOps实践中,是否必须马上将项目切换到微服务架构框架模式,反而不是必须。
在整个DevOps流水线中,我们实际上强调个一个关键点在于一套Docker镜像文件+多套环境配置+多套构建版本标签做法。以确保我们最终构建和测试通过的版本就是我们部署到生产环境的版本。构建操作只有一次,而后面到测试环境,到UAT环境,到生产环境,都属于是镜像的环境迁移和部署。而不涉及到需要再次重新打包的问题。这个是持续集成,也是DevOps的基本要求。
在DevOps和容器云平台集成的时候可以看到,整个流水线里面有两个分离动作。
构建和打包
在这里做下解释即编译构建是我们通过类似Maven等工具进行自动化构建形成类似JAR包等部署包的过程。而打包则是将部署包基于预先定义的镜像模板制作为镜像的过程。而在传统的持续集成操作里面,往往则没有单独的打包这个动作。
DevOps流水线节点的松耦合设计
在进行流水线设计和开发的时候,我们的整个过程如下:
- 建立编译构建,打包,部署,测试等各个独立的任务节点并进行配置。
- 新建立一个流水线,然后将上面的各个任务节点串联在一起
但是这种模式构建的流水线你会发现,前面创建的任务节点由于定制了特殊的配置信息,导致只能够应用到你新建的特定流水线上面。或者简单的说,你要灵活进行任务节点之间的连线往往并不可能达到。
因此我们还是重新回顾下我们建立的各个任务操作节点。
构建操作:构建我们通常采用Maven进行自动化构建,构建完成输出一个或多个Jar包或War包。
注:对于编译构建操作,我们看到重点就是指定具体的git库和分支路径,然后采用默认标准的maven构建方式进行编译构建。那么在这种方式下,我们完全可以将类似Git库配置信息配置到具体的项目基本属性里面,这样编译构建操作同样可以通用。
注意常规方式下构建完执行进行部署操作,部署操作一般就是将构建的结果拷贝到我们的测试环境服务器,同时对初始化脚本进行启动等。而在DevOps下,该操作会变成两个操作,即一个打包,一个部署。打包是将构建完成的内容制作为镜像,部署是将镜像部署到具体的资源池和指定集群。
打包操作:实际上即基于构建完成的部署包来生成镜像。该操作一般首先基于一个基础镜像文件基础上进行,在基础镜像文件上拷贝和写入具体的部署包文件,同时在启动相应的初始化脚本。
那么首先要考虑构建操作和打包操作如何松耦合开。
打包操作简单来就是就是一个镜像制作,需要的是构建操作产生的输出。我们可以对其输出和需要拷贝的内容在构建的时候进行约定。而打包任务则是一个标准化的镜像制作任务,我们需要考虑的仅仅是基于1)基于哪个基础镜像 2)中间件容器默认目录设置 3)初始化启动命令。
即在实际的打包任务设计的时候,我们不会指定具体的部署包和部署文件,这个完全由编排的时候由上游输入。
注:在前期流水线设计的时候,为了考虑灵活性,我们往往会对打包过程的脚本自定义完全放开,包括基于哪个dockerfile制作镜像,镜像制作过程中需要运行哪些指定的脚本文件,进行哪些文件拷贝操作等。但是为了考虑灵活性,这些都必须提前进行约定,即打包操作中都统一调用一个标准命名的初始化脚本文件。
部署操作:部署操作相当更加简单,重点就是将镜像部署到哪个资源池,哪个集群节点,初始化的节点配置等。具体部署哪个镜像不要指定,而是由上游任务节点输入。
经过上面的思考,我们希望达到一个目的就是我们会形成标准的任务操作节点,完全适用于多个不同的微服务和应用项目。而不再需要针对每个项目都去建立不同的任务操作节点。
比如对于部署操作,我们如果有两种资源调度策略,那么我们创建两个部署操作任务模板即可。对于打包操作,我们根据不同的开发语言,构建不同的打包操作模板即可。
任务节点间松耦合设计的意义
这种松耦合设计才能够使流水线编排更加灵活。
比如我们在进行了构建打包后,我们希望同时将打包内容部署到开发环境和测试环境。那么则是打包动作完成后需要对接两个应用部署任务。这两个部署任务都依托上面的打包结果进行自动化部署,可以并行进行。
对于测试环境部署完成后,我们需要进行测试人员手工验证测试,如果测试通过,我们打标签后希望能够直接发布到UAT环境。而这种操作我们也希望在一个流水线来设计和完成。这样我们更加容易在持续集成看板上看到整个版本构建和迁移的完整过程。
如果这是在一个大流水线里面,那么对于UAT环境部署更多只是一个镜像的迁移操作,因此就需要一直去追溯流水线上的最近的一个打包任务节点,同时取该任务节点产生的输出来进行相应的环境部署操作。
在谈DevOps的时候,一个重点就是和QA和QC的协同,因此在流水线编排的时候一定要考虑各类测试节点,包括静态代码检查,自动化的单元测试,人工的测试验证。同时最好基于持续集成实践,能够将测试过程和整个自动化构建过程紧密结合起来。
简单来说,测试人员发现build1.0.0001版本4个bug并提交,那么在下次自动化构建完成并单元测试通过后,测试人员能够很清楚的看到哪些Bug已经修改并可以在新构建的版本进行验证。只有这样才能够形成闭环,整个流水线作业才能够更好的发挥协同作用。
两级流水线设计
在进行微服务架构设计和开发的时候可以看到,原来的单体应用已经拆分为多个微服务。比如一个财务报账应用,我们拆分为报销管理,系统管理,流程引擎,数据管理,接口管理,门户管理六个微服务模块。
那么整个报账应用实际上就是产品-》微服务的两级架构模式。
在整个DevOps支撑平台基础数据配置的时候,我们首先需要支持将报账应用配置为独立的产品,并在该产品下配置6个独立的微服务项目。
我们最终向客户交付的是整个报账应用产品,但是对于开发和持续集成的过程则是按照微服务项目的粒度进行管理。特别是在后续运维变更的时候,如果仅仅是报销管理模块发生了变更,我们希望的仅仅是对报销模块流水线进行运行和持续集成,而不是所有的微服务模块都进行重新构建。
基于两级流水线设计,我们希望达到的效果就是:
我们不用去关心产品变更的时候究竟变更了哪些微服务模块,即一次变更我们直接启动产品流水线。产品流水线启动后自动检查哪些微服务发生变更,如果发生变更则出现持续集成操作,如果没有变更直接调整到End完成节点。
在所有微服务单个流水线执行完成后,我们聚合到产品流水线进行人工测试和验证,没有问题后我们可以进一步进行打标签操作,或触发环境迁移操作。
注意环境迁移我们可以根据本次变更版本号或根据我们手工打的标签号进行,同时环境迁移操作本身不再涉及到编译构建和打包操作,因此环境迁移应该配置在产品级流水线上进行。
如果从SIT环境迁移到UAT环境后测试不通过,测试出了相应的Bug,这个时候产品流水线退回到开始节点,同时开发人员在修改完成Bug后,同样系统自动判断哪些微服务模块代码出现变更并自动触发构建打包操作,直至所有的Bug修改完成并验证通过。
注意,在传统的方式下,我们将环境迁移操作配置到微服务项目流水线上面,这种方式本身存在问题。即我们最终测试和发布的是产品,一个产品的变更往往涉及到多个微服务模块的修改,如果将环境迁移配置在微服务上,那么将大量增加人工操作去进行环境迁移和多微服务间版本同步操作,这个显然是增加了开发人员工作量。
通过流水线实现研发过程管理和持续集成协同
在DevOps实践中,一定要解决好的就是开发和测试的协同,开发测试和工程运维的协同。因此对于研发项目过程管理工具,DevOps支撑工具本身要紧密集成才能够更好的支持整个协同过程。
虽然敏捷方法论强调面对面的沟通,但是在整个持续交付过程中要减少各种无谓的沟通,通过工具进行自动化协同。类似测试不清楚开发的功能是否完成了,是否可以测试了,是否部署到SIT环境了,Bug是否修改好可以重新验证了等等问题,都应该在工具层面进行解决。
- 开发和测试协同:重点解决版本多次编译构建持续集成和测试验证过程协同
- 开发和运维协同:重点解决测试完成版本发布后,工程运维进行版本提取和部署的协同
我们重新思考下整个持续交付的过程
- 基于收集到的用户需求整理后形成软件需求,进行版本规划,纳入需求和Bug
- 规划一个新的项目版本号,后续的项目需求,任务,缺陷管理等也都基于该版本号进行
- 开发构建一个流水线,完成编译,构建,代码检查,发布,自动化单元测试,对应Dev环境
- 开发应该在对发布到Dev环境的功能再做一下自测(手工节点)
- 开发在自测没有问题的时候,应该自动的将该部署包迁移部署到SIT环境
- 开发提交一个测试申请单,关联可以进行测试的需求或Bug
- 测试人员只需关系待测试验证的需求,在SIT环境的版本一定是部署好可验证的版本
- 测试对需求进行验证,并提交Bug,测试人员修改Bug并触发流水线重新构建
- 测试在所有缺陷全部验证通过后,确认集成测试通过环境,版本自动迁移到UAT环境
- 在UAT环境的验收测试同样的道理
- 在UAT环境所有问题也验证关闭后,进入到正式发布生产环境。
流水线设计属于持续交付过程域中的一个关键内容,其核心还是为了持续集成服务。简单来说就是流水线作业解决自动化的持续集成问题,那么持续集成本身又包括哪些关键内容?
即我们常说的自动化编译构建,自动化部署,自动化测试。而在转到和容器技术结合后可以看到在编译构建完成后自动化部署进行了拆分,即部署动作变化为了首先是自动化的打包即制作容器镜像,然后才是自动化部署,部署部署基于部署包的,而是基于制品库中的容器镜像的。
可以看到,在DevOps中的流水线设计更多的是解决开发人员的自动化问题,即开发只需要check in自测通过的代码到配置管理库,那么后续的所有事情都自动化完成,最终部署到测试环境供测试人员测试。
开发人员不用关心编译构建过程,不用关心测试服务器包括测试环境数据库,中间件的安装部署等一系列的问题。同时由于一系列的自动化操作,也让代码从检入到交付测试的周期缩短了,在周期缩短后交付频度也可以进一步提升以加快迭代速度。
对于DevOps支撑平台我们一直在谈流水线自动化设计,但是更应该反过来谈持续集成方法论和最佳实践,因为流水线设计仅仅是为持续集成和持续交付服务的工具链而已。其次,对于当前流水线设计,更多是从技术层面开发人员自动化在谈,但是却少考虑研发管理开发和测试过程协同问题。
在DevOps最佳实践里面分为了研发管理,持续交付和技术运营几个关键的过程域。但是在实践的过程中,最容易出现问题的不是单个技术点,而是跨域的协同问题,或者说研发过程管理和持续集成交付本身就是密不可分的两个部分,我们只是为了容易理解和学习将其划分为了不同的过程域而已。
研发过程管理和持续集成间的协同关系
要明白任何一次新的编译构建部署完成后都涉及到测试人员测试,测试人员测试出问题后又会提交Bug,开发人员修改Bug后check in代码,等待下一次打包部署以形成多次迭代。
整个过程最好方式就是要尽量减少大量的人工沟通协同,而是应该通过工具链协同来完成。
对于传统的持续集成,一般最佳实践为每天晚上进行自动化构建和冒烟测试,而对于当期的DevOps过程,在设计完流水线后,可以手工去启动流水线作业,也可以自动去执行流水线,或者代码库变更后实时触发流水线执行。流水线执行时间频度也可以进一步缩短,假设我们每2个小时自动化的执行一次流水线作业,我们以此场景来做进一步梳理。
流水线增加启动检查节点
虽然2小时执行一次流水线,但是在执行前先进行启动前检查,如果在最近2个小时内没有新代码check in,那么不执行流水线。其次,如果上一次流水线执行实例还处于待处理或未关闭状态的时候,同样也不执行流水线作业而直接跳过。
是否需要人工验证节点
流水线打包部署包括两个方面,一个是新功能提交或新bug解决,只有这种情况才需要人工验证。因此一次流水线执行如果没有新需求或Bug状态变化,那么应该直接跳过人工验证节点并关闭。反之,则应该跳转到待人工验证环节。
需求和缺陷的管理
注意新需求和新缺陷都应该提交,都有状态,需求细分到具体的需求功能点,同时测试人员提交的缺陷应该对应到具体的需求功能点上面。一个需求开发完成后,需求本身也到待验证状态,但是一个待验证的需求是否能够关闭则必须是该需求下面所有的bug都解决完成后才能够关闭。
需求和缺陷状态的变化
开发人员首先是将需求或缺陷完成,并在本机进行自测通过,然后将代码check in到配置管理库。同时手工将需求或缺陷状态处理到待部署状态。
在流水线启动后,如果整个构建打包和部署成功,则在成功完成应用部署后,将待部署状态的需求或bug转到待验证状态。在部署完成后测试人员可以看到待验证的bug或需求,那么他进入当前的测试环境一定是最新的可以进行缺陷验证的环境。
应用部署和环境迁移
实际上我一直在思考如何理清这两者的关系,包括在流水线节点设计的时候是同种类型的一个节点还是不同的两个节点?应用来说是直接将编译打包后的镜像执行进行部署,前面有编译构建操作;而对于环境迁移来说则是直接从制品库里面使用已有镜像进行环境部署。
流水线模板和流水线实例应该是两个不同的概念,一个流水线模板每次运行都会产生一个实例,而每次实例都会形成一次构建版本号。
即每次打包后形成的镜像入库也会附带上这么一个流水线实例号。那么我们的应用部署操作相对就简单了,即对于应用部署活动节点编排在流水线上面,作用是进行应用部署,但是本质是从制品库拿到对应镜像去部署。如何拿到对应镜像,基于流水线实例号就可以很好的进行对接上。
在环境迁移中,我们设置两个环境,一个SIT环境供内部测试人员测试,一个UAT环境供客户方进行UAT测试,那么在SIT测试完成后可以将SIT环境的镜像包迁移到UAT环境进行部署。
注意并不是每一次流水线作业都涉及到环境迁移操作,而实际上需要测试人员去判断当前的测试版本是否需要迁移到UAT环境去供用户测试。同时测试人员在当前测试问题全部修复并关闭后可以对当前版本进行配置基线操作。
其次,对于用户测试提交的问题一般并不会在我们自己的缺陷管理系统进行Bug记录,因此用户反馈问题给测试人员,测试人员帮忙录入到缺陷管理系统,同时缺陷修改完成后测试先要验证没有问题,再通知用户进行验证,只有用户验证通过后缺陷才能够最终关闭。
同时基于前面谈到的,当一个传统的单体应用拆分为多个微服务后,由于我们本身向客户交付的还是单体应用本身,因此最佳的设计方式还是启用产品和项目两级流水线设计方式,将人工测试验证和环境迁移操作配置在产品级流水线上面。