Please enable Javascript to view the contents

一个通用流水线设计

 ·  ☕ 5 分钟

1. 解耦引擎释放流水线能力

在设计系统时,我们常面临两难。是内敛复杂度,对外提供单一易用的功能;还是释放复杂度,将灵活归还用户。这非常考验产品能力。

设计 CICD 系统时,我们可以直接将 Jenkinsfile、PipelineRun 等概念直接抛给用户,让用户自己学习相关领域的知识,再来使用产品。当然,也可以继续抽象,在人与系统之间建立模型,实现意识与指令的转换。我们想要更加易用的产品,因此选择屏蔽底层概念,继续抽象、建模。

从 Jenkins 、GitLab CI,再到 GitHub Actions、Tekton,新的基础设施总会有各种各样的基础组件涌现。我们想减少这种切换的成本,在各种引擎之间能够切换。技术在不断地更替,但我们想对用户保持一致。

虽然流水线相关技术在快速演进,但执行这件事的终究是人。人的知识是有传承的,无论技术怎样变化,做流水线引擎的社区是相对稳定的,都是有交集的一群人。这给解耦引擎,设计通用流水线提供了可能。

2. 流水线的数据模型

在很多 CICD 引擎中,能够找到相似的概念。

  • Jenkins

流水线包含很多个 Stage,而 Stage 中的 steps 包含多个串行执行的脚本。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
pipeline {
    agent any

    stages {
        stage('Build') {
            steps {
                echo 'Building-1..'
                echo 'Building-2..'
            }
        }
        stage('Test') {
            steps {
                echo 'Testing..'
            }
        }
        stage('Deploy') {
            steps {
                echo 'Deploying....'
            }
        }
    }
}
  • GitLab CI

流水线包含 build、test 两个 串行的 Stage,每个 Stage 包含若干并行执行的 Job 。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
stages:
  - build
  - test

build-code-job:
  stage: build
  script:
    - echo "Check the ruby version, then build some Ruby project files:"
    - ruby -v
    - rake

test-code-job1:
  stage: test
  script:
    - echo "If the files are built successfully, test some files with one command:"
    - rake test1

test-code-job2:
  stage: test
  script:
    - echo "If the files are built successfully, test other files with a different command:"
    - rake test2
  • GitHub Actions

流水线由 jobs 定义,一个流水线有很多个可能的 job(示例中的 build 构成),每个 job 又包含很多串行的 steps。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
name: Octo Organization CI

on:
  push:
    branches: [ $default-branch ]
  pull_request:
    branches: [ $default-branch ]

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v2

      - name: Run a one-line script
        run: echo Hello from Octo Organization

基于以上的示例,我们对 Pipeline 进行抽象。

1
2
3
4
5
6
7
- pipeline
  - stage-1
    - step-1-1
    - step-1-2
  - stage-2
    - step-2-1
    - ...

如下图,一个流水线包含若干个 Stage, Stage 可以并行或者串行。Stage 中包含若干个串行的 Step 执行脚本。而在 Tekton 中,Stage 对应着 Task 。

一个流水线的运行时,可以是一个 Kubernetes 集群、一个物理机、一个 Container 环境等。

一个 Stage 的运行时,可能是一个 Pod、一个物理机、一个 Container 环境等。

一个 Step 有一个工作空间,然后执行 Shell Script。

流水线并不需要复杂的定义,即使几个简单的脚本,也可以编排复杂的逻辑。但是对流水线进行抽象和建模,有利于插件(Step)扩展,流水线产品本身的开发和维护。

流水线会经过一系列的 Manager 处理,与特定的运行时、执行引擎、凭证等关联起来。最终渲染出引擎接受的流水线描述,例如 Jenkins 的 Jenkinsfile、Tekton 的 Yaml。

3. 代码层的数据模型

下面给出核心数据结构的主要字段:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
// 定义运行时
type Runtime struct {
	Name        string
	Provider    interface{}
}

// 定义引擎
type Engine string

// 定义作用域,分为三个层级。
type Scope string

const (
	ScopePipeline Scope = "Pipeline"
	ScopeStage    Scope = "Stage"
	ScopeStep     Scope = "Step"
)

// 定义参数结构
type ParamSpec struct {
	Name string `json:"name"`
	Scope ParamType `json:"type,omitempty"`
	Default interface{} `json:"default,omitempty"`
}

// 定义插件模板
type Step struct {
	Name        string      `json:"name"`
	Params      []ParamSpec `json:"params,omitempty"`
	Script      string      `json:"script,omitempty"`
  Engine      string      `json:"engine"`
	Workspace   string        `json:"workspace,omitempty"`
}

// 定义组装流水线之后,插件(Step)相关字段
type PipelineStep struct {
	Name    string  `json:"name,omitempty"`
	StepRef string  `json:"StageRef,omitempty"`
	Params  []Param `json:"params,omitempty"`
	Status  string  `json:"status,omitempty"`
	Workspace string  `json:"workspace,omitempty"`
}

// 定义 Stage ,主要是一系列的 Steps
type Stage struct {
	ObjectMeta `json:"metadata"`
	Spec       StageSpec `json:"spec"`
}

type StageSpec struct {
	Description string         `json:"description,omitempty"`
	Params      []ParamSpec    `json:"params,omitempty"`
	Steps       []PipelineStep `json:"steps,omitempty"`
	Workspace   string         `json:"workspace,omitempty"`
}

// 定义组装流水线之后,阶段(Stage)相关字段 
type PipelineStage struct {
	Name      string        `json:"name,omitempty"`
	StageRef  Stage         `json:"stageRef,omitempty"`
	Params    []Param       `json:"params,omitempty"`
	Workspace string        `json:"workspace,omitempty"`
	Status    string        `json:"status,omitempty"`
	Runtime   *Runtime
}

// 定义流水线的结构
type Pipeline struct {
	ObjectMeta `json:"metadata"`
	Spec       PipelineSpec `json:"spec"`
}

type PipelineSpec struct {
	Params      []ParamSpec     `json:"params,omitempty"`
	Stages      []PipelineStage `json:"stages,omitempty"`
	Workspace string `json:"workspace,omitempty"`
}

// 定义运行一条流水线相关的字段
type PipelineRun struct {
	ObjectMeta `json:"metadata,omitempty"`
	Spec       PipelineRunSpec `json:"spec,omitempty"`
	Status     string          `json:"status,omitempty"`
}

type PipelineRunSpec struct {
	PipelineRef string         `json:"pipelineRef,omitempty"`
	Params      []Param        `json:"params,omitempty"`
	Runtime     *Runtime
}

在代码层面,需要注意两点:

  • 模板与实例。模板是系统内置的引擎相关的框架或片段,例如 Step、Stage、Pipeline ;而实例是填充个性化参数之后的模板或片段,PipelienStep、PipelineStage、PipelineRun 。
  • 作用范围。参数中有一个字段 Scope ,用于表示参数在什么范围可见。实际上,真正使用参数的是 Step,但是组装之后 Stage 作用域的参数会被提升到 Stage 中。同理,Pipeline 作用域的参数会被提升到 Pipeline 中。参数在不同的层级会有不同的用处,从内向外抽取,从外向内注入。

4. 流水线运行时的数据与交互

上面是一个流水线的执行流程,可以对照着每个步骤查看,这里不再重复。下面主要以不同角色的视角描述流水线的运行。

4.1 系统内置 Step 插件模板

首先需要在系统中内置一些常用的插件 Script。

例如,Jenkins 的 Git Clone 插件

1
git(url: '${param.git_repo}', credentialsId: '${param.ssh-key}', branch: '${param.branch}', changelog: true, poll: false)

Jenkins 的构建并推送镜像。

1
2
3
4
5
withCredentials([usernamePassword(passwordVariable : 'DOCKER_PASSWORD' ,usernameVariable : 'DOCKER_USERNAME' ,credentialsId : "${param.credential_id}" ,)]) {
          sh 'echo "$DOCKER_PASSWORD" | docker login ${param.registry_server} -u "$DOCKER_USERNAME" --password-stdin'
          sh 'docker push  ${param.image_name}'
          sh 'docker logout'
        }

Jenkins 执行脚本。

1
sh '${param.script_content}'

当然也可以是其他引擎的插件片段,但主要是脚本片段 + 参数注入,因此不再列举。

4.2 创建流水线时,用户视角

如上图,用户首先会根据用户选择的引擎,得到一个 Step 模板列表。然后通过编排,将 Step 组装成一个流水线。

其中,Step-1、2、3 表示的是选择一个模板 Step 并初始化参数的实例。然后组装出 Pipeline 的数据结构保存在后端。

如果是使用模板进行创建,那么只需要提前帮用户初始化 Pipeline 数据结构即可。

4.3 创建流水线,开发人员视角

  • 前端研发

请求 Step 模板列表之后,根据用户输入的实例化参数,组装 Pipeline 结构。

  • 后端研发

将用户的 Pipeline 数据保存之后,根据 Step 模板信息,将 Pipeline 渲染成引擎需要的流水线描述。例如,生成 Jenkinsfile 文件,同步到 Jenkins 创建流水线。

4.4 运行时,数据流向及交互

  • 执行流水线

前端调用后端接口,获取 Pipeline 的定义,将 Pipeline 级别的参数弹框,让用户输入相关的参数。点击确认后,创建 PipelineRun 对象。

后端根据 PipelineRun 对象,触发引擎的执行接口,将 PipelineRun 中定制化的参数传入流水线执行。

  • 查看流水线

PipelineRun 即为流水线的执行历史,需要从引擎中查询流水线的状态,并写入 PipelineRun 对象。

  • 再次运行、暂停、继续、审批

在 PipelineRun 中记录有某次执行的全部记录,包括参数、Pipeline 定义。因此,只要引擎支持上述功能,都可以实现。


微信公众号
作者
微信公众号