CI/CD平台的探索与实践
背景
在软件开发过程中CI/CD是不可或缺的环节,在提高交付效率的同时能规范整套流程。然而不同公司的产品不一,部署环境差异导致所需的编译环境,单一的saas平台不一定能满足,同时售价也较高。这时通过自研一套公司定制的CI/CD系统,能够灵活控制每个环境,结合公司所处环境进行定制化研发。能够带了效率上的提升与各个流程的规范。
调研
github
action定义
1 |
|
工作运行原理
根据yaml配置的job详细进行设置运行环境,同一个job会在一个runner中运行,所产生的数据每个step共享
在镜像中预先安装的相关软件,如 git 、maven、node、kubectl等等。
镜像中安装的软件详细数据:Ubuntu2204-Readme.md
action插件
参考github action自定义插件,支持自定义镜像和插件
- actions是通过js或者ts编写的,每次在执行pipeline时扫描当前配置文件引用了哪些actions进行下载后到容器里面在进行执行该插件的入口文件
- 在插件的仓库中有配置该插件的执行入口文件是哪个,同时插件会在文件中说明好所需的运行环境如下图 ‘node16’
- actions的执行日志通过调用
import * as core from '@actions/core'
core 库进行打印
runs-on支持self-hosted和Ubuntu镜像
self-hosted 表示使用我们自己的机器来跑 job,其实就是将 runner 安装到本地机器来跑任务
总结
优点:
- 基于插件的方式灵活到爆炸,同时插件生态丰富,基于社区一起进行维护。
- runner 独立部署,能够适应不同的平台环境跑特定的任务。
缺点:
- 不支持人工卡点。
- 权限管控不够完善,仓库所有者都能进行修改。
gitee
Pipeline定义
代码方式进行编辑
1 |
|
可视化方式进行编辑
工作运行原理
拉取代码直接通过shell脚本方式进行传递给容器进行执行,如下面看到的代码 clone 方式
日志模块是通过不断的从前端去请求拿到指定时间区间内的日志数据
构建后的产物会进行缓存(上传)到
总结分析
优点:
- 支持可视化界面编辑和yaml代码方式进行编辑、灵活方便。
- 支持人工卡点,消息通知支持的平台丰富,权限管控完善,每个阶段都可控制执行人。
- 构建产物能直接传递给下一个stage,作为下个任务的输入,因为gitee的job都是直接跑在容器中,没有指定机器架构的功能所以所有任务都可以在一个节点上运行,数据共享的比较方便(盲猜通过容器挂载方式实现或者上传到OSS在需要的节点再下载)。
缺点:
- 不runner 独立部署,能够适应不同的平台环境跑特定的任务。
云效
Pipeline定义
工作运行原理
1 |
|
总结分析
优点:
- 支持可视化界面编辑和yaml代码方式进行编辑、灵活方便。
- 支持人工卡点。
- 与阿里云绑定、各种资源能直接联系上,非常适合所有资源都在阿里云上的客户使用。
缺点:
- 不支持代码方式编辑流水线,全部需要在界面上配置
- 不runner 独立部署,能够适应不同的平台环境跑特定的任务。
- 企业使用费用较高。
总结
通过对以上三个平台的分析可知他们都有各自的优劣,github的runner能实现不同平台机器的运行能最大程度的满足自己所配置的环境,而后面两个平台提供了可视化方式进行编辑更加直观,上手难点更加低。
分析
需求
- 满足多环境适配,混合云、私有云部署、kubernetes、docker等
- 权限划分明确,严格限制每个人只能管理自己的应用。
- 安全可靠,能够保证秘钥安全,对普通用户将敏感数据进行加密防止泄露。
- 效率优先,能够支撑大量的应用并行发布,随时进行扩容伸缩。
- All in one 原则,能够极大降低维护成本,提高使用效率。
- 个别应用的构建需要特定的环境来执行,如election、mac应用等,所以需要提供runner。
- 在pipeline执行过程中能够实时查看日志,中断和重启。
- 执行效率高、支持的并发执行数量能够满足当业务快速发展时带来膨胀的构建量。
- 能够完全打通CI/CD整套流程,从APP创建开始到生产上线运行。
设计
平台的各个模块详细设计
总体设计
架构图
模块划分
项目模块
- 项目包含应用。
应用模块
- 应用模块主要负责应用管理,隶属项目,项目是根据现有适配的模板进行渲染创建的。
流水线模块
- 负责调度workflow的主要模块,为这个平台的核心模块。
通知模块
- 支持飞书、钉钉等主流通知。
基础模块
- 用户管理、权限管理、基础配置等各个系统运行必须模块。
基础模块
- 项目权限管控: 分为两种角色、参与者和负责人、负责人具有修改权限、参与者只能进行查看,无法修改。
- 应用权限管控:
详细设计
对系统的各个模块进行细化设计,包含模型、领域对象详细设计
应用模板
统一化各类应用模板,在项目管理模块中,创建app时自动根据选择的应用类型和必填参数进行渲染模板,同时配置各个基础项,如gitlab代码仓库、CI/CD平台必填参数等。最后将渲染完成的项目基础代码模板下载给用户。
凭证管理、运行pipeline所必须的相关凭证需要有个模块来集中管理如git、k8sconfig、服务器秘钥等
任务触发
事件监听
事件监听为监听git仓库的变化事件,如gitlab当发生代码提交事件、tag事件、push事件会触发webhook从而进行接收请求
- 分支匹配:使用正则表达式进行匹配推送的分支名称
- tag匹配:tag 关键字正则表达式匹配
- 提交关键字匹配:git commit 关键字正则匹配
手动触发
手动点击运行按钮进行触发流水线
定时触发
cron方式进行触发
webhook触发
使用uuid作为链接提供给外部进行触发
gitlab适配
通过在gitlab的代码仓库进行注册回调地址,当仓库有人进行推送或者打tag时可以触发
1 |
|
添加webhook响应
1 |
|
推送代码响应
1 |
|
tag推送响应
1 |
|
流程引擎
基于容器进行任务调度,执行完成容器销毁,资源自动回收。运行用户自定义镜像,多平台runner满足不同任务环境需求。
pipeline
核心流程
- 服务端解析配置文件,根据定义的文件进行配置 触发器,运行触发器进行监控触发的时机。
- 触发器进行触发pipeline,将配置文件描述的pipeline进行实例化,绑定对应的step,根据step的配置选择runner发送
- runner 接收到 需要执行的 step 进行反序列化 进行条件准备完成后运行,将运行日志通过rpc传递给服务端,服务端检查是否有人订阅日志,有则通过websocket进行连接发送。
- runner运行step完成后进行本地环境清理,如清除容器实例、workspace清理等。
context如何传递 ?
- 序列化后对象传递(不同的step的配置不一样,rpc不支持未知类型的传递)
- 传递定义的 yaml 配置,由 runner 自己进行解析(runner运行的当前step可能会依赖前面的一些产物数据,需要传递给当前runner)
领域模型
DDL:
DDL:
1 |
|
1 |
|
Pipeline Context 定义
1 |
|
代码实现
待解决的问题:
如何实现实时日志:发布订阅模式,与前端使用websocket方式,服务端与runner使用grpc。如何实现内置功能:不采用github actions 适配难点大、后期在考虑兼容。run-on的选择是针对整条流水线还是stage还是step,参考jenkins和github action。
一共三种粒度:
- 流水线级别的粒度:最粗粒度,无法满足在一条流水线中不同环境的step运行,如果需要不同环境只能新建流水线选择对应的机器环境。
- stage级别的粒度:中等粒度,介于1、3之间两者的利弊都没有完全解决,不采用该方式。
- step级别的粒度:最细粒度,可为每个step显示指定节点运行环境,能够精确把控每个step,会带来产物同步的高效性降低,因为只能通过OSS方式共享。
run-on的环境支持镜像和宿主机,如果在宿主机如何保证安全问题。默认认为环境是安全的且构建机器不会存储任何秘钥
run-on运行环境选择机制:
- 不指定的话使用当前负载最低的节点运行Pipeline。
- 指定的话则根据指定的label来选择对应的机器。
支持三种环境运行step:
- 裸机运行
- 容器运行(默认)
- 自定义镜像运行
每个step需要做的工作:
- 运行前环境准备
- 运行用户输入的command
- 构建后产物存储
- 缓存构建
- 实时日志同步给客户端
日志服务
在执行任务过程中需要实时传输日志给前端进行展示,采用websocket方式进行实时传输(实时运行状态),如果是已经完成的
- 在执行命令时还需要读取 命令执行后返回的流数据进行回显
- 秘钥类型的数据使用 **** 来代替显示。
- 基于发布订阅模块进行日志传输。
Step 生命周期
- 实例化:根据 yaml 定义文件反序列化其到step配置实例。
- 前置准备:根据 yaml 定义的环境准备对应的执行环境,如主机、容器(默认为容器),注入环境变量和秘钥。
- 执行 step:根据 指定的step去执行特定操作、command等。
- 释放资源:释放容器和清理执行step过程中worksapce产生的内容。
- 存储结果:将执行结果状态、记录进行存储(人工卡点是需要进行介入的)。
Step 可视化编辑
流程:
- 从后端获取step列表
- 选择 step 去后端请求 该 step 的表单配置(使用该step需要那些输入配置供前端进行展示),根据step的配置进行数据渲染(后端)再返回给前端
- 前端拿到step的配置后进行渲染表单
配置的定义:
该定义位于step的记录中(content字段)主要是记录使用该节点需要用户进行输入那些数据项,在用户进行选择该s
1 |
|
以golang构建为案例
1 |
|
- 在可视化视图编辑时需要将表单数据进行转为yaml数据提交给服务器。
- 在切换图形视图和代码视图的tab中能将其数据进行转换。
1 |
|
1 |
|
1 |
|
server
server端负责进行pipeline基础管理,通过rpc通信派发流水线给runner进行执行,同时将日志从runner转发给前端和存储到日志中心。
runner
runners 的设计原理主要包含以下几个方面:
基于客户端-服务器架构:Runner 作为客户端,连接到 devops-platform 服务端。客户端轮询请求服务器获取任务,然后在本地执行,并将结果返回给服务器。
用 Docker 隔离执行环境:Runner 在 Docker 容器内执行任务,实现了执行环境的隔离。默认的虚拟环境保证了每个步骤的一致性。
支持跨平台:Runner 支持 Linux、Windows 和 macOS,可以使工作流跨平台执行。
高度可扩展:Runner 支持横向扩展,可以根据需要增加运行实例的数量。
状态共享:Runner 和服务端通信可以共享状态,如工作流的当前进度。
事件驱动subtotal更新:Runner 将在配置发生变化时从服务端获取更新,确保工作流是最新的。
8.安全加密通信:Runner 和服务端之间的通信是通过安全的 TLS 加密的。
必填参数:
- label用于server运行step时可选择节点。
启动可选参数:
- 服务端 Endpoint。
- 工作目录 workspace(部分任务需要在主机上运行)。
Docker API :https://docs.docker.com/engine/api/v1.43/
执行任务的详细流程
启动时对环境工具进行检查,必需工具链不存在不给予运行
缓存机制
部分step的运行可能每次需要下载很多依赖,例如 npm 的 node-module ,maven的jar文件等等,如果每次都重新下载的话效率低下,如果能将其进行缓存起来则可以加快构建的速度。
runner 选择算法
- 按照label选择runner。
- 同label下多个节点,选择负载最低的runner。
状态上报
- 定时向服务端上报当前runner的负载情况,健康状态等必要信息
images
基础镜像库,在运行job是依赖于一些基础工具链的,这些工具会被打包进镜像里面,如git、maven、golang等。
运行原理
容器方式运行
将当前step必备的组件通过-v进行挂载到容器中去,缓存的实现也是一样
- 日志实时读取
- 格式化日志(带上时间)
- 日志驱动
项目构建的时候需要拉取依赖(多个私服仓库依赖),如果是那种私服的仓库如何配置权限 ?
- 从代码源中获取配置的access token来访问但是这个token的权限级别特别高,存在用户破坏数据的可能性。
- 通过保存代码源的时候去创建一个仅仅带有只读权限的access token,在拉取代码的时候注入这个token,因为只有只读权限,还是比较安全的。
- 基于2如果还要更加精细的控制权限,可以拿到当前仓库用户的 access token(没有则创建)可以进一步进行管控。
gitee的实现方式
1 |
|
上面的问题其实本质上就是credential的设计问题
秘钥模块设计
- 在脚本中能使用秘钥,但是打印出来的是 ‘******’
直接在控制台输出的日志进行过滤,如果存在则替换成 ‘******’,但是正在方式如果通过
github测试获取秘钥
可以拿到数据
实时证明,只是屏蔽掉返回给前端 秘钥 使用 ”****“ 代替
主机方式运行
主机运行方式可以通过对不同的step设置所需的环境变量来配置对应的工具版本
命令执行
以act为例:
- 将命令直接复制到容器里面
1 |
|
我们要的效果:
Action实现
Checkout Action
- 在主机模式下如何能做到不污染其他pipeline拉取
Exec Action
通信协议
https://docs.gitea.com/zh-cn/usage/actions/design
https://gitea.com/gitea/actions-proto-def/src/branch/main/proto
https://gitea.com/gitea/actions-proto-go/src/branch/main/runner/v1/messages.pb.go
落地
记录系统开发落地过程中的问题记录
mac下docker启动 Ubuntu
1
2
3
4docker pull --platform=linux/amd64 ubuntu:20.04
docker run -d -i -t --name ubuntu ubuntu:20.04
-i:这个参数是"interactive"的简写,意味着即使没有附加到容器,仍保持STDIN开启。安装 git、nvm
1 |
|
1 |
|
可以通过这种方式来实现表头
1 |
|
step日志:
总共两种状态进行查询日志:1.任务运行中 2.任务已完成
step运行中:
- 客户端获取当前step的执行日志group状态,发送 执行记录uuid+**step定位三件套 **到服务端,服务端返回该step的执行步骤group(日志组),前端进行展示。
- 客户端获取具体的日志信息,点击具体的group再将其传递给后端进行获取实时的日志信息(第一次打开使用首个group去获取)
step运行已完成:
- 获取的状态都是已经完成,无需缓存websocket,直接返回全部group的最终执行状态。
- 客户端获取具体的日志信息,点击具体的group再将其传递给后端进行获取已经记录下的数据。
问题:
- 在哪里存储 action 呢 ?step 配置里 ?
actions 存储在 step 实现里,Action 作为 step更加细化的动作,step只是负责装配action,当step在runner启动时先将所有actions的状态回传给server
runner响应的信息类型
- step执行action状态更新(成功、失败)
- action日志(追加)
- step执行结果数据(状态&数据)
如何确定代码源分支:
- gitee/github是基于gitops将配置文件放到指定的分支的,也就是说一个分支一个Pipeline。
- 云效的方式是在任务编排的时候提供一个代码源的选择,指定分支。
先来看看需要在线上跑的场景
- 针对于toc项目一般都是线上环境是单个分支,比如说是release分支。
- 一下tob项目可能会多个线上版本(本质上不是线上是交付给B端的版本,但是开发环境是需要多个)
最好的方案是:
- 支持多个开发环境的部署
实现跳过/取消Step的方案
- step可能处于的状态,因为前端控制只能是跳过已经在运行中的step,所以可以确定是处于”运行中状态”
跳过当前step直接执行下一个step,如果没有step直接结束当前row
- 使用context进行分发子context来维护各个执行路径,可以统一控制结束(中断)
在进行跳过和取消时需要注意并发取消的情况
实现人工卡点的方案
- 要求指定用户点击通过后才能继续往下执行
- 不能一直驻留内存占用资源和worker
实现方案:
- 在遇到人工卡点的step时先驻留内存一段时间(具体再定),当超过该时间时,将当前的Pipeline的状态进行持久化。
- 用户点击通过或者不通过时,先判断当前的pipelin是否驻留在内存中,如果在内存中则直接进行继续调度,如果不在说明Pipeline已经超时被挂起,需要从持久化的Pipeline中重新load到内存中进行执行。
在对人工卡点超时的情况下,进行存储状态到数据库需要保证:
可执行节点已经执行完成了,如果还有可执行节点正在执行中需要等待其执行完在进行持久化,那么如何判断可执行节点是否已经都执行完
- 对waitGroup进行判断,如果剩下1则表示只剩当前这个”人工卡点“的节点未执行完,但是如果同一个stage存在多个”人工卡点“则不能使用这种方法判断了。
- 在执行时遇到人工卡点的step、记录人工卡点的坐标和个数,当执行到人工卡点step时进行判断,如果当前只剩下人工卡点待执行且到达超时时间则直接进行存储。超时时间应该以最后一个人工卡点计算。
进阶设计:
- 可选触发人
- 触发人and/or的实现
- 已审批过的人disable掉审批按钮
失败策略的实现
1 |
|