全文8150字,预计阅读时长21分钟一、概述百度APP经过多年的DevOps建设,从规划、开发、测试、集成到交付已经形成了一套标准工作流和工具集。其中,持续集成(CI)是DevOps的核心流程之一,它通过频繁地将代码集成到骨干和生产环境中来执行预设的自动化任务。CI一直是我们的百度移动研发平台——Tekes,是支撑百度APP研发过程的重要切入点。我们的自动化研发流程(组件自动发布、接入等)基于CI的实践,已经支持百度APP的发布。50+次,访问400,000+组件和SDK。但是,当这些自动化的研发流程输出到其他产品线时,就会遇到一个问题:不同的产品线对研发流程有定制化的要求。当发生不兼容的变更时,启动人工审批;KankanAPP发布后只需要在报表中显示不兼容的变化,这使得我们预设的流水线模板无法直接复用。为了解决这个问题,一个可行的方法是让产品线用一种结构化的语言来描述他们研发过程中需要的一组功能或特性,然后根据描述自动生成相应的流水线。这个想法其实就是流水线即代码(PipelineasCode,PaC)。2.PipelineasCodePipelineasCode是一种“asCode”运动,引用Gitlab官网对PipelineasCode的解释:Pipelineascode是一种通过源代码定义部署管道的实践,比如Git。管道即代码是更大的“即代码”运动的一部分,其中包括基础设施即代码。团队可以在代码中配置构建、测试和部署,这些代码可跟踪并存储在集中式源存储库中。团队可以使用声明式YAML方法或特定于供应商的编程语言,例如Jenkins和Groovy,但前提保持不变。说到“即代码”,我们最有可能想到的是基础设施即代码(InfrastructureasCode,IaC)。IaC是使用DSL(DomainSpecifiedLanguage,领域特定语言)编码,例如Ansible的playbook就是基于YML的DSL。下面是一个在macOS系统上标准化Xcode安装的playbook的简单示例:#playbook-name:InstallXcodeblock:-name:checkthatthexcodearchiveisvalid|grep\"状态:签署苹果Software\"-名称:清理现有的Xcode安装文件:路径:/Applications/Xcode.app状态:不存在-名称:从XIP文件安装Xcode位置命令:xip--expand{{xcode_xip_location}}args:chdir:/Applicationspoll:5async:"{{xcode_xip_extraction_timeout}}"#防止SSH连接超时等待提取-name:AcceptLicenseAgreementcommand:"{{xcode_build}}-licenseaccept"become:true-name:RunXcodefirstlaunchcommand:"{{xcode_build}}-runFirstLaunch"become:truewhen:xcode_major_version|int>=13when:notxcode_installedorxcode_installed_versionisversion(xcode_target_version,'!=')与Ansible一样,我们也对管道DSL使用类似的解决方案coding和versioncontrol,除了解决我们遇到的不同产品线差异化配置的问题,它还有很多其他的优势,比如:“让产品线团队只需要关注pipeline的DSL的当前版本,这是方便内部团队成员共同维护和升级;管道本身的环境配置也是DSL的一部分,消除了由于配置混乱导致的管道环境的特殊性;DSL非常容易复制和链接代码片段,CI脚本组件化后可以作为组件使用一个可配置的DSL单元不过在介绍我们的解决方案之前,先介绍一下业界有代表性的两个解决方案:JenkinsPipeline和GithubActions。JenkinsPipeline是Jenkins2.0引入的一套GroovyDSL语法,以统一代码的形式管理和维护原本独立运行在多个Jobs或多个节点上的任务。GitHubActions是GitHub推出的一项自动化服务。通过在仓库中配置一个基于YML的DSL文件来创建工作流,当仓库触发事件时,工作流就会运行。下面以一个简单的Xcode项目编译为例,来了解一下这两种DSL语法的区别,包括三个步骤:"Checkout:从Git服务器上拉取源代码Build:执行xcodebuild编译命令UseMyLibrary:引用自定义脚本方法JenkinsPipeline的DSL如下://Jenkinsfile(DeclarativePipeline)@Library('my-library')_pipeline{agent{node{label'MACOS'}}stages{stage('Checkout'){steps{checkoutscm}}stage('Build'){steps{sh'xcodebuild-workspaceprojectname.xcworkspace-schemeschemename-destinationgeneric/platform=iOS'}}stage('UseMyLibrary'){steps{myCustomFunc'Helloworld'}}}}DSLforGithubActions如下:#.github/workflows/ios.ymlname:iOSworkflowon:[push]jobs:build:runs-on:macos-lateststeps:-name:Checkoutuses:actions/checkout@v3-name:构建运行:xcodebuild-workspaceprojectname.xcworkspace-schemeschemename-destinationgeneric/platform=iOS-名称:UseMyLibrary使用:my-library/my-custom-action@masterwith:args:Helloworld可以看到两种DSL的表达都非常清晰简洁,很多语法甚至可以互相转换。其实DSL并不是PaC的首要选择依据,而是要看业务使用的是哪种持续集成系统Jenkins是一个完全自托管的持续集成系统,使用Groovy脚本来定义Pipeline也提供了很大的灵活性。GithubActions与Github高度集成,可以直接使用,Workflow的YML是组件化设计,结构清晰,语法简单。可以说两者各有千秋。我们业务使用的持续集成系统是百度开发的iPipe。我们在实践PaC时,首要原则是利用公司的基础设施,避免重新发明轮子。因此,我们采用了创新的解决方案和系统——TekesActions。3.TekesActions从命名上可以看出,TekesActions是指GithubActions,DSL语法基本照搬GithubActions。例如百度APP组件发布流程的DSL如下:#baiduapp/ios/publish.ymlname:'iOSModulePublishWorkflow'author:'zhuyusong'description:'iOS组件发布流程'on:events:topic_merge:branches:['master','release/**']repositories:['baidu/baidu-app/*','baidu/third-party/*']jobs:publish:name:'publishmodulesusingEasybox'runs-on:macos-lateststeps:-name:'Checkout'uses:actions/checkout@v2-name:'SetupEasybox'使用:actions/setup-easybox@v2with:is_public_storage:true-name:'BuildTaskuseEasybox'uses:actions/easybox-ios-build@v1with:component_check:truequality_check:true-name:'PublishTaskuseEasybox'使用:actions/easybox-ios-publish@v1-name:'AccessTaskuseEasybox'uses:actions/easybox-ios-access@v1其实上一节提到DSL本身并不是PaC最关键的点。我们选择GithubActions的DSL的主要原因是因为它的组件标准化的工作流程可以和我们的Tekes平台以及公司的持续集成系统很好的集成。先介绍一下GithubActions官方文档的工作流程,附上一张示意图:工作流程包括一个或多个作业(Job),由事件(Event)决定,运行在Github-hosted或self-hostedrunners上。下面介绍这些核心组件:1.Job:每个作业由一组步骤(Step)组成,每个Step是一个单一的任务,可以运行一个动作(Action)或一个shell命令。Action可以看作是一个封装好的独立脚本。可定制或参考第三方。2.事件:事件是触发工作流的特定活动,比如推送代码、拉分支、创建问题等。本来Github提供了非常丰富的类型,可以很方便的作为Github的触发源行动。3.运行器:运行器是运行触发工作流的服务。GitHub提供了适用于Linux、Windows和macOS虚拟机环境的运行器,您还可以创建自托管运行器以在自定义环境中运行。动作是工作流的核心和最基本的元素。可以说,正是因为Action的可重用、可扩展的设计,才为GithubActions生态带来了强大的活力。Github不仅提供了很多官方的Actions,而且还设立了一个Action市场,可以搜索各种三方Actions,实现一个workflow非常容易。TekesActions在jobs、events和runners这三个组件上有自己独特的设计:在jobs上,Tekes通过Actions的可复用扩展的思想,将自己多年构建的CI脚本分解为Tekes官方Actions。允许产品线自由接入自身研发流程,实现定制流水线;同时开放CI能力,构建公共Action产品库,支持组件化、质量、性能等角色上传自己的Action,共同构建Tekes生态。在事件方面,由于Tekes在构建移动DevOps服务时抽象了自己的事件类型,我们将其作为TekesActions工作流的触发源。并且因为我们的事件和仓库不是一对一的,所以我们还设计了一个产品线流程编排服务和一个事件处理服务。前者用于帮助产品线管理工作流的DSL文件,后者用于判断哪个事件应该触发哪个产品线的哪个工作流。在runner上,我们完整实现了runner用来解释执行流程的DSL,包括监听触发事件、调度作业、下载动作、执行脚本、上传日志等,并支持本地调用Cli命令,即方便流水线开发者在自己的本地工作区Debug;同时,remoterunner在我们的虚拟机集群中运行。当工作流被触发时,我们通过iPipeAgent对其进行调度。iPipeAgent是一个基于iPipe的代理服务,可以直接调度到我们的虚拟机集群,分配一个全新的指定系统和运行器的虚拟机。整个TekesActions的工程架构如下图所示:在工程化方面,TekesActions采用完全基于百度云的Serverless服务。核心事件处理服务只是一个云函数服务。此云功能服务负责处理两类事件源:1。流程编排。当一条产品线被创建和更新时,会生成一个YML文件并上传到DSL文件服务中该产品线的目录中。DSL文件服务的文件添加和更新事件会通知云函数添加和更新数据库服务Stored触发规则;2.事件。例如,DevOps服务生成的组合事件将发布到消息服务的特定主题。云函数订阅该主题接收事件,用于匹配数据库服务中存储的各产品线的工作流触发规则,匹配成功时调度一个runner。4.TekesRunnerTekesRunner是运行TekesActions工作流程的工具。架构图如下:TekesRunner服务层由三个模块组成:Template、Worker和WebAPI,分别负责读取和验证DSL文件、管理工作流、与后端Service通信,其中WebAPI模块可以在配置文件中关闭。TekesRunner在交互层的命令模块提供了四个Cli命令,分别是Run、Pause、Unpause和Kill,都是管理工作流生命周期的命令。工作流生命周期如下图所示:当执行Run命令时,TekesRunner首先会根据配置初始化TekesActions工作流,包括读取对应工作流的YML、下载并验证依赖的Action、创建工作空间等,成功后进入初始化状态。然后,将根据解析的工作流对象创建并运行一组状态机。每个作业对应一个简单的有限状态机(FiniteStateMachine,FSM),此时进入运行状态。状态机的当前状态就是当前执行的阶段,触发状态转换的事件就是脚本运行的结果。当整个状态组的所有状态机都已经转换到结束状态(无论成功与否)时,工作流结束并进入停止状态。另外,如果工作流超时或者收到外部的Kill命令,也会进入停止状态。现在我们已经实现了一个简单的可以在本地运行脚本的runner,但是还有几个细节需要进一步细化:丨详解1.Action和Runner的交互Action和Runner的交互是一个很常见的行为,对于例如,从TheRunner获取输入并写入输出,或者将执行结果通知给Runner。Action和Runner交互的方式有以下三种:环境变量:Runner将需要传递给Action的参数写到环境变量中,一般包括Action需要的输入和一些context;工作区文件:Runner将需要传递给Action的文件放到工作区的特定文件夹中,一般是Action需要的中间产物;Action的打印:Runner会在Action的执行过程中持续监控Action的打印内容,Runner和Action约定了一套特殊的命令标识。打印语句,当Runner监听到这样的语句时,会解析并执行预设的命令,包括设置输出、打印日志、上传产品等。丨详解2.Pause/Unpause的作用工作流有时不可避免地会插入需要很长时间的任务-人工审批、代码审查等长期等待。例如,组件访问产品线需要产品线负责人的批准。如果工作流没有挂起,说明有一个Action一直在阻塞线程或者不断轮询,这无疑是一种巨大的资源浪费。当执行Pause命令时,Runner会持久化当前上下文并结束进程。相比之下,当执行Unpause命令时,Runner从持久化中恢复context,继续运行当前stage。丨详解3.WebAPI的作用大多数情况下,Runner是在远程虚拟机中执行的,由ipipe调度代理,并在工作流执行后回收。因此需要一种机制将本地日志和持久化上下文保存到一个服务中,这其实就是WebAPI的作用。回顾一下TekesActions的工程架构图,日志服务其实就是负责保存工作流日志和上下文的。其他下游服务也可以通过我们的日志服务查询工作流执行状态和详细日志。4.结束语PipelineasCode不仅是一种高效的流水线管理形式,也是CI/CD向DevOps转型的新趋势。在PaC的帮助下,为整个流水线带来的难以置信的灵活性,也为团队围绕流水线的构建、沟通和协作带来了有益的变化。构建一个好的PaC需要一些前置依??赖,包括云原生平台和持续集成工具。我们的Tekes也是基于公司强大的基础设施和自身丰富的持续集成实践,然后参考业界成熟的解决方案,站在巨人的肩膀上去摘星。希望我们的这篇文章能给大家在解决持续集成问题时提供一些参考。参考[1]持续集成维基百科https://zh.wikipedia.org/zh-s...[2]什么是CI/CDhttps://www.redhat.com/zh/top...[3]]什么是管道即代码https://about.gitlab.com/topi...[4]管道即代码https://www.jenkins.io/doc/bo...[5]管道即代码https://insights.thoughtworks...[6]基础设施即代码的解读https://insights.thoughtworks...[7]Ansible权威指南https://ansible-tran.readthed...\_intro.html[8]如何创建Jenkins共享库https//www.tutorialworks.com/jenkins-shared-library/[9]从Jenkins迁移到GitHubActionshttps://docs.github.com/cn/ac...[10]比较对比GitHubActions和AzrePipelineshttps://docs.microsoft.com/en...推荐阅读:Go语言使用MySQL常见故障分析及对策基于宽表的百度交易中心钱包系统架构分析百度点评数据建模应用百度点评平台的设计与探索如何正确评价基于宽表的视频质量数据可视化基于模板配置的小程序启动性能优化实践平台我们是如何闯过低代码“无人区”的:amis和loveQuickBuild中的KeyDesigns
