百度APP 基于Pipeline as Code的持续集成实践

1,559 阅读13分钟

图片

全文8150字,预计阅读时间21分钟

一、概述

百度APP 经过多年 DevOps 的建设,已经形成了一套从计划、开发、测试、集成到交付的标准工作流和工具集。其中,持续集成(Continuous integration,简称 CI)作为 DevOps 最核心的流程之一,通过频繁地将代码集成到主干和生产环境以执行预置的自动化任务。

CI一直是我们百度移动研发平台——Tekes,支撑百度APP研发流程的重要的切入点,我们的自动化研发流程(组件自动发布、准入等)都是基于CI的实践,已经支持 百度APP 发版50+次,准入40w+组件和SDK。但当这些自动化研发流程输出到其他产品线时,却遇到了一个问题:不同产品线对研发流程存在定制化需求,例如 百度APP 在中台化组件发布前需要检查API及依赖的变更情况,并在有不兼容变更的情况发生时发起人工审批;而 好看APP 只需要将不兼容变更展示在发布完成后的报表之中,这就导致了我们预置的流水线模板无法直接复用。为了解决这个问题,一种可行的方法是让产品线用结构化的语言去描述他们研发流程需要的一组功能或特性,然后根据描述自动化地生成对应的流水线,这种思想其实就是 Pipeline as Code(流水线即代码,PaC)。

图片

二、Pipeline as Code

Pipeline as Code 是 “as Code” 运动的一种 ,引用 Gitlab 官网对 Pipeline as Code 的解释:

Pipeline as code is a practice of defining deployment pipelines through source code, such as Git. Pipeline as code is part of a larger “as code” movement that includes infrastructure as code. Teams can configure builds, tests, and deployment in code that is trackable and stored in a centralized source repository. Teams can use a declarative YAML approach or a vendor-specific programming language, such as Jenkins and Groovy, but the premise remains the same.

图片

提到 “as Code” ,我们最容易想到 Infrastructure as Code (基础设施即代码,IaC) , IaC 是将基础设施、资源及环境使用 DSL(Domain Specified Language,领域专有语言)编码,例如 Ansible 的 playbook 就是一种基于YML 的 DSL, 一个在 macOS 系统上标准化安装 Xcode 的 playbook 简单示例如下:

# playbook
- name: Install Xcode
  block:
    - name: check that the xcode archive is valid
      command: >
        pkgutil --check-signature {{ xcode_xip_location }} |
        grep \"Status: signed Apple Software\"
    - name: Clean up existing Xcode installation
      file:
        path: /Applications/Xcode.app
        state: absent
    - name: Install Xcode from XIP file Location
      command: xip --expand {{ xcode_xip_location }}
      args:
        chdir: /Applications
      poll: 5
      async: "{{ xcode_xip_extraction_timeout }}" # Prevent SSH connections timing out waiting for extraction
    - name: Accept License Agreement
      command: "{{ xcode_build }} -license accept"
      become: true
    - name: Run Xcode first launch
      command: "{{ xcode_build }} -runFirstLaunch"
      become: true
      when: xcode_major_version | int >= 13
  when: not xcode_installed or xcode_installed_version is version(xcode_target_version, '!=')

如同 Ansible,我们也使用了类似的方案对流水线进行 DSL 编码并纳入版本控制,除了能解决我们遇到的不同产品线差异化配置的问题,他还有很多其他优点,例如:

  1. 让产品线团队只需要关注流水线当前版本的DSL,方便团队内部成员共同维护和升级;    

  2. 流水线本身的环境配置也是DSL的一部分,消除流水线环境由于配置混乱造成的特异性;

  3. DSL非常容易复制和链接代码片段,可以将CI脚本组件化之后作为一种可配置的DSL单元。

不过在介绍我们的方案之前,我们先介绍下业内比较有代表性的两种方案:Jenkins Pipeline Github Actions

Jenkins Pipeline 是 Jenkins2.0 推出的一套Groovy DSL语法,将原本独立运行于多个Job或者多个节点的任务统一使用代码的形式进行管理和维护。

图片

GitHub Actions 是 GitHub 推出的自动化服务,通过在仓库配置一个基于 YML 的 DSL 文件来创建一个工作流,当仓库触发某个事件的时候,工作流就会运行。

图片

我们举一个简单的 Xcode 工程 编译的例子来体会两者 DSL 语法的区别,包含三个步骤:

  1. Checkout:从 Git 服务器下拉源码

  2. Build:执行 xcodebuild 编译命令

  3. UseMyLibrary:引用自定义的脚本方法

Jenkins Pipeline 的 DSL 如下:

// Jenkinsfile(Declarative Pipeline)
@Library('my-library') _
pipeline {
  agent {
    node {
      label 'MACOS'
    }
  }
  stages {
    stage('Checkout') {
      steps {
        checkout scm
      }
    }
    stage('Build') {
      steps {
        sh 'xcodebuild -workspace projectname.xcworkspace -scheme schemename -destination generic/platform=iOS'
      }
    }
    stage('UseMyLibrary') {
      steps {
        myCustomFunc 'Hello world'
      }
    }
  }
}

Github Actions 的 DSL 如下:

# .github/workflows/ios.yml
name: iOS workflow
on: [push]
jobs:
  build:
    runs-on: macos-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v3
      - name: Build
        run: xcodebuild -workspace projectname.xcworkspace -scheme schemename -destination generic/platform=iOS
      - name: UseMyLibrary
        uses: my-library/my-custom-action@master
        with:
            args: Hello world

可以看到两者 DSL 的表述都非常清晰简练,很多语法甚至可以相互转换,实际上 DSL 不是 PaC 首要的选型依据,而是看业务使用何种持续集成系统。

Jenkins 是一个完全自托管的持续集成系统,使用 Groovy 脚本来定义 Pipeline 也提供了极大的灵活性。而 Github Actions 与 Github 高度整合,可以直接使用,并且 Workflow 的 YML 是一种组件化设计 ,结构清晰,语法简单。可以说两者各有千秋。我们业务使用的持续集成系统是百度自研的 iPipe,我们在做 PaC 的实践时第一原则是使用公司的基础设施,避免重复造轮子,因此我们采取了一种创新的方案和系统——Tekes Actions。

三、Tekes Actions

从命名可以看出 Tekes Actions 参照了 Github Actions ,DSL语法也基本照搬 Github Actions,例如 百度APP 组件发布流程的 DSL如下:

# baiduapp/ios/publish.ymlname: 'iOS Module Publish Workflow'author: 'zhuyusong'description: 'iOS 组件发布流程'on:  events:    topic_merge:      branches: ['master', 'release/**']      repositories: ['baidu/baidu-app/*', 'baidu/third-party/*']jobs:  publish:    name: 'publish modules using Easybox'    runs-on: macos-latest    steps:    - name: 'Checkout'      uses: actions/checkout@v2    - name: 'Setup Easybox'      uses: actions/setup-easybox@v2      with:        is_public_storage: true    - name: 'Build Task use Easybox'        uses: actions/easybox-ios-build@v1with:          component_check: true          quality_check: true    - name: 'Publish Task use Easybox'        uses: actions/easybox-ios-publish@v1    - name: 'Access Task use Easybox'        uses:actions/easybox-ios-access@v1

其实上一节提到了 DSL 本身不是 PaC 最关键的点,我们选用 Github Actions 的 DSL 主要原因是因为其组件化的工作流程可以很好地与我们Tekes平台以及公司的持续集成系统进行融合。

我们先介绍 Github Actions 官方文档的工作流程,附上示意图:

图片

工作流程包含一个或者多个作业(Job),由事件(Event)触发,在Github托管或自托管的运行器(Runner)上运行**。**下面介绍这几个核心组件:

1. 作业:每个作业由一组步骤(Step)组成,每个Step是可以运行操作(Action)或者 Shell命令的单个任务,Action 可以看作封装的独立脚本,可以自定义或引用第三方。

2. 事件:事件是触发工作流程的特定活动,例如推送代码、拉取分支、创建问题等。本来Github就提供了非常丰富类型,可以很方便地作为Github Actions的触发源。

3. 运行器:运行器是运行触发后的工作流程的服务,GitHub 提供 Linux、Windows 和 macOS 虚拟机环境的运行器,也可以创建自托管运行器运行在自定义的环境中。

Action 是组成工作流程最核心最基础的元素,可以说正是因为有了 Action 这种可复用扩展的设计,给 Github Actions 生态带来了极大的活力,Github 不仅提供了许多官方的 Action, 并且还搭建了一个 Action 市场,可以搜索到各种三方 Action,让实现一个工作流程变得非常简单。

Tekes Actions 在作业、事件和运行器这三个组件上都有自己特色的设计:

作业上,Tekes 通过 Action 可复用扩展的这种思想,把自己多年建设的CI脚本分解成Tekes官方的 Action,让产品线可以自由插拔到自己的研发流程中,实现定制化流水线;同时开放出 CI 的能力,搭建了一个公开的 Action制品库,支持 保障组件化、质量、性能等角色 可以上传自己的Action,共同建立Tekes生态。

事件上,由于Tekes 在建设移动DevOps服务时抽象了自己的事件类型,我们将此作为 Tekes Actions 工作流程的触发源。并且由于我们的事件并不是和仓库一对一的关系,我们还设计了一个产品线流程编排的服务和事件处理的服务,前者用来帮助产品线管理工作流程的DSL文件,后者用来裁决何种事件应该触发何种产品线的何种工作流程。

运行器上,我们完全实现了运行器用来解释执行工作流程的DSL,包括监听触发事件,调度Job,下载Action,执行脚本,上传日志等功能,并支持 Cli 命令本地调用,方便流水线开发者在自己本地的工作区中进行调试;同时远端的运行器跑在我们虚拟机集群,当工作流程触发时我们通过 iPipe Agent 去调度。

iPipe Agent 是一个基于 iPipe 的代理服务,可以直接调度到我们的虚拟机集群并分配一台全新的包含指定系统和运行器的虚拟机。

整个 Tekes Actions 的工程架构如下图所示:

图片

工程上,Tekes Actions 采用了一个完全基于百度云的 Serverless 服务,核心的事件处理服务只是一个云函数服务,这个云函数服务负责处理两种事件源:

1. 流程编排。产品线创建和更新工作流程时会生成一个 YML 文件,上传到 DSL文件服务 的该产品线的目录下,DSL文件服务 的文件新增和更新事件会通知云函数,用来新增和更新数据库服务存储的触发规则;

2. 事件。例如DevOps服务产生的组合入事件,这些事件会发布到消息服务特定的 topic 中,云函数订阅这个topic以接收事件,用来匹配数据库服务存储的各个产品线工作流程触发规则,并在匹配成功时调度一个运行器。

四、Tekes Runner

Tekes Runner 是运行 Tekes Actions 工作流程的工具,架构图如下:

图片

Tekes Runner 服务层由 Template、Worker 和 WebAPI 三个模块组成,分别负责读取和校验DSL文件、管理工作流程以及与后端服务通信,其中 WebAPI 模块是可以在配置文件中关闭的。

Tekes Runner 在交互层的命令模块提供了四种 Cli 命令,分别是 Run、Pause、Unpause 和 Kill, 都是管理工作流程生命周期的命令。工作流程生命周期如下图所示:

图片

当执行 Run 命令时, Tekes Runner 首先会根据配置来初始化 Tekes Actions 工作流程,包括读取对应工作流程的YML,下载并校验依赖的 Action,创建工作区等,成功之后进入 initialized 状态。

接着会根据解析后的 工作流程对象 创建一组状态机并运行,每一个作业对应一个简单的有限状态机(Finite State Machine, FSM),此时进入到 running 状态。

图片

状态机的现态是当前执行的阶段,触发状态迁移的事件是脚本运行的结果。

当整个状态机组的所有状态机都迁移到结束状态(无论是否成功),工作流程就会结束,进入 stopped 状态。此外,如果工作流程超时或者接收到外部的 Kill 命令,也会进入 stopped 状态。

现在我们已经实现了一个可以在本地运行脚本的简单运行器了,但还有几个细节需要进一步阐述:

丨细节1. Action和 Runner 的交互方式

Action 和 Runner 交互是一个非常常见的行为,例如从Runner获取输入写入输出,又或者将执行结果通知Runner。

Action和 Runner 有三种交互方式:

  1. 环境变量:Runner将需要传递给Action的参数写入环境变量中,一般包括Action所需的输入以及一些上下文;

  2. 工作区文件:Runner将需要传递给Action的文件放入工作区特定的文件夹中,一般是Action所需的中间产物;

  3. Action 的打印:Runner 在执行 Action 的过程中会不断监听 Action 的打印内容,Runner 和 Action 约定了一套带有特殊命令标识符的打印语句,当Runner监听到此类语句时会解析并执行预设的命令,包括设置输出,打印日志和上传产物等。

丨细节2. Pause/Unpause 的作用

工作流程有时候不可避免会插入人工审批、代码评审等需要长期等待的任务,例如组件准入产品线需要产品线负责人审批。如果工作流程没有暂停状态,那就意味着有一个 Action 一直阻塞着线程或者不断地轮询,这对资源无疑是一个巨大的浪费。

当执行 Pause 命令时, Runner 会持久化当前的上下文并结束进程。相对的,当执行 Unpause 命令时,Runner会从持久化中恢复上下文,然后继续运行当前的阶段。

丨细节3. WebAPI 的作用

在大多数情况下,Runner 是执行在远端的虚拟机中,由 ipipe agent调度,并在执行完工作流程后被回收。因此需要一个机制将本地的日志和持久化上下文保存到一个服务中,这其实就是 WebAPI 的作用。

回顾一下 Tekes Actions 的工程架构图,日志服务其实就担任着保存工作流程日志和上下文的角色,其他的下游服务还可以通过我们的日志服务去查询工作流程执行的情况和详细的日志。

四、结语

Pipeline as Code 既是一种高效的流水线管理形式,也是 CI/CD 转变成 DevOps 的一种新的趋势。借助于 PaC,给整个流水线带来的不可思议的灵活性,也给团队围绕流水线的建设、沟通和协作带来了有益的变化。

建设好 PaC 需要一些前置的依赖,包括云原生平台和持续集成工具,我们 Tekes 也是基于公司强大的基础设施加上自身丰富的持续集成实践,然后参考了业内成熟的方案,站在巨人的肩膀上伸手摘星。希望我们的这篇文章可以给大家在解决持续集成问题时带来一定借鉴意义。

参考

[1] 持续集成维基百科

zh.wikipedia.org/zh-sg/%E6%8…

[2] What is CI/CD

www.redhat.com/zh/topics/d…
[3] What is pipeline as code

about.gitlab.com/topics/ci-c…
[4] Pipeline as Code

www.jenkins.io/doc/book/pi…
[5] 流水线即代码

insights.thoughtworks.cn/pipeline-as…
[6] 解读基础设施即代码

insights.thoughtworks.cn/nfrastructu…
[7] Ansible权威指南

ansible-tran.readthedocs.io/en/latest/d…
[8] How to Create a Jenkins Shared Library

https//www.tutorialworks.com/jenkins-sha…
[9] Migrating from Jenkins to GitHub Actions

docs.github.com/cn/actions/…
[10] Compare and contrast GitHub Actions and Azre Pipelines

docs.microsoft.com/en-us/dotne…

推荐阅读:

Go 语言使用 MySQL 的常见故障分析和应对方法

百度交易中台之钱包系统架构浅析

基于宽表的数据建模应用

百度评论中台的设计与探索

基于模板配置的数据可视化平台

如何正确的评测视频画质

小程序启动性能优化实践

我们是如何穿过低代码 “⽆⼈区”的:amis与爱速搭中的关键设计