写点什么

Kubernetes 原生 CI/CD 构建框架 Tekton 详解

用户头像
火山引擎
关注
发布于: 2021 年 02 月 07 日
Kubernetes 原生 CI/CD 构建框架 Tekton 详解

作者:FogDong(字节跳动火山引擎

本系列第一篇:Kubernetes 原生 CI/CD 构建框架 Argo 详解!


在计算机中,流水线是把一个重复的过程分解为若干个子过程,使每个子过程与其他子过程并行进行的技术,也叫 Pipeline。由于这种工作方式与工厂中的生产流水线十分相似, 因此也被称为流水线技术。从本质上讲,流水线技术是一种时间并行技术。以“构建镜像”过程为例:

在每一次构建镜像中,我们都需要拉下代码仓库中的代码,进行代码编译,构建镜像,最后推往镜像仓库。在每一次代码更改过后,这一过程都是不变的。使用流水线工具可以极大的提升这一过程的效率,只需要进行简单的配置便可以轻松的完成重复性的工作。这样的过程也被称之为 CI。


上图流程中使用的是 Jenkins。Jenkins 作为老牌流水线框架被大家所熟知。在云原生时代,Jenkins 也推出了 Jenkins X 作为基于 Kubernetes 的新一代流水线,但今天本文主要介绍诞生于云原生时代的流水线框架——Tekton。

Tekton

Tekton 是一个基于 Kubernetes 的云原生 CI/CD 开源框架,属于 CD 基金会的项目之一。Tekton 通过定义 CRD 的方式,让用户以灵活的自定义流水线以满足自身 CI/CD 需求。

基本概念

四个基本概念

Tekton 最主要的四个概念为:Task、TaskRun、Pipeline 以及 PipelineRun。

  • Task: Task 为构建任务,是 Tekton 中不可分割的最小单位,正如同 Pod 在 Kubernetes 中的概念一样。在 Task 中,可以有多个 Step,每个 Step 由一个 Container 来执行。

  • Pipeline: Pipeline 由一个或多个 Task 组成。在 Pipeline 中,用户可以定义这些 Task 的执行顺序以及依赖关系来组成 DAG(有向无环图)。

  • PipelineRun: PipelineRun 是 Pipeline 的实际执行产物,当用户定义好 Pipeline 后,可以通过创建 PipelineRun 的方式来执行流水线,并生成一条流水线记录。

  • TaskRun: PipelineRun 被创建出来后,会对应 Pipeline 里面的 Task 创建各自的 TaskRun。一个 TaskRun 控制一个 Pod,Task 中的 Step 对应 Pod 中的 Container。当然,TaskRun 也可以单独被创建。


综上可知:Pipeline 由多个 Task 组成,每次执行对应生成一条 PipelineRun,其控制的 TaskRun 将创建实际运行的 Pod。下面以一个简单例子来展示这些概念。


首先,创建一个最简单的 Task,里面仅有一个 Step。在一个 ubuntu 镜像中执行 ls 命令。

apiVersion: tekton.dev/v1beta1kind: Taskmetadata:  name: task-examplespec:  steps:  - name: ls    image: ubuntu    command: ["ls"]
复制代码

接着创建一个 Pipeline,里面引用第一步中创建的 Task。

apiVersion: tekton.dev/v1beta1kind: Pipelinemetadata:  name: pipeline-examplespec:  tasks:  - name: task-example    taskRef:      name: task-example
复制代码

在 Pipeline 存在的前提下,就可以通过创建 PipelineRun 来运行 Pipeline。

apiVersion: tekton.dev/v1beta1kind: PipelineRunmetadata:  name: pipelinerun-example    # 或者可以直接使用 generateName,让 kubernetes 自动在名字后生成随机字符串  # generateName: pipelinerun-example-spec:  pipelineRef:    name: pipeline-example
复制代码

这样就完成了一个最简单的 Tekton 流水线案例。每一个 PipelineRun 的创建,都会遵循 Pipeline 中的顺序规则去启动 Task 的 Pod。下面引入另外一个概念 PipelineResource 来完成一个稍微复杂的例子,也是 DevOps 中最常见的场景:从代码仓库拉取镜像、进行代码构建、并最终将构建好的镜像推往镜像仓库。

PipelineResource

PipelineResource 代表着一系列的资源,主要承担作为 Task 的输入或者输出的作用。它有以下几种类型:

  • git:代表一个 git 仓库,包含了需要被构建的源代码。将 git 资源作为 Task 的 Input,会自动 clone 此 git 仓库。

  • pullRequest:表示来自配置的 url(通常是一个 git 仓库)的 pull request 事件。将 pull request 资源作为 Task 的 Input,将自动下载 pull request 相关元数据的文件,如 base/head commit、comments 以及 labels。

  • image:代表镜像仓库中的镜像,通常作为 Task 的 Output,用于生成镜像。

  • cluster:表示一个除了当前集群外的 Kubernetes 集群。可以使用 Cluster 资源在不同的集群上部署应用。

  • storage:表示 blob 存储,它包含一个对象或目录。将 Storage 资源作为 Task 的 Input 将自动下载存储内容,并允许 Task 执行操作。目前仅支持 GCS。

  • cloud event:会在 TaskRun z 执行完成后发送事件信息(包含整个 TaskRun) 到指定的 URI 地址,在与第三方通信的时候十分有用。


以上为 Tekton 目前支持的六大 PipelineResource 类型,具体的配置及使用方法详见 PipelineResource 文档


继续分析较复杂的流水线案例:从代码仓库拉取镜像、进行代码构建、并将构建好的镜像推往镜像仓库。从已有的 PipelineResource 类型可判断,可以使用 git 类型作为代码资源作为输入,再用 image 类型作为镜像资源作为输出。有了输入输出后,我们可以直接使用 Kaniko 来构建镜像。


Kaniko 是 Google 开源的项目之一,可在 Kubernetes 上无需特权模式地构建 docker 镜像。


首先创建这两个 PipelineResource。在这个例子中,git-input 对应输入,image-output 对应输出。params 中的参数均为该资源类型的固定参数:如 git 中可以通过 revision 指定版本号,image 中可以通过 url 指定镜像仓库地址。


Git-input:

apiVersion: tekton.dev/v1alpha1kind: PipelineResourcemetadata:  name: git-inputspec:  type: git  params:  # revision 指定版本号  - name: revision    value: v0.32.0  # 代码仓库地址,若为私有仓库,还需要配置 service account 以及 secret  - name: url      value: https://github.com/GoogleContainerTools/skaffold
复制代码

Image-output:

apiVersion: tekton.dev/v1alpha1kind: PipelineResourcemetadata:  name: image-outputspec:  type: image  params:    # 镜像仓库地址,若为私有仓库,还需要配置 service account 以及 secret    - name: url      value: gcr.io/<use your project>/leeroy-web
复制代码

在配置 PipelineResource 时,如果使用了私有仓库,还需要配置 Service Account,详见 configuring-authentication-for-docker

产物传递

创建完 PipelineResource 后,需要在 Task 中引入它们作为输入输出。那么,这些资源是如何在 Task 间传递的呢?


在 Tekton 的分区下,我们可以看到一个叫做 config-artifact-pvc 和一个叫做 config-artifact-bucket 的 Config Map。从命名就可以看出,这二者分别代表了产物存储的两种配置方式—— PVC 和存储桶(目前支持 GCS 和 S3)


以 PVC 为例,修改 config-artifact-pvc 需要填写两个值:size 以及 storageClassName。size 默认为 5GiB,storage class name 默认为 default。这也意味着当我们使用 PipelineResource 进行资源传递时,会自动创建一个 5GiB 的存储卷挂载在 Task 上,供 PipelineResource 使用。


在需要进行 Task 间的资源传递时,这个存储卷会被挂载在 Task 的 /pvc 目录下。当 Task 执行完成并且需要进行资源传递(通过 inputs/outputs 指定)后,TaskRun controller 会自动添加一个拷贝文件的步骤容器,并将输出产物统一放到 /pvc/task_name/resource_name 命名规范的目录下。


上面是针对产物需要进行传递的情况下,对于目前例子而言,由于只需要一个 Task,虽然指定了 Inputs 和 Outputs,但并没有另一个 Task 来引用这些结果。因此,在这个例子中并不会去挂载 PVC。


对于 git 以及 storage 类型的 input,资源下载后会被 放在 /workspace/task_resource_name 下;对于 output 则会放在 /workspace/output/resource_name 下。image 类型的资源则会直接上传到镜像仓库。


了解了这些前置知识后,我们可以来创建 Task 了。Kaniko 需要三个参数来完成镜像构建:Dockerfile 的地址,context 的地址以及镜像仓库的地址。在下面这个例子中,我们大量使用了 params 以及 Tekton 中的变量替换。Params 用于在 TaskRun 和 Task 中传递参数,而变量替换的格式为 $(xxx)。使用这些变量可以让 Tekton 在运行过程中根据规则进行赋值。值得注意的是,Tekton 并不会提前去检查这些变量的内容,这就要求着我们在写的时候需要多加注意。具体的变量编写规则详见:Tekton variables

apiVersion: tekton.dev/v1beta1kind: Taskmetadata:  name: build-docker-image-from-git-sourcespec:  params:    # 参数用于找到 Dockerfile 用于构建镜像    - name: pathToDockerFile      type: string      description: The path to the dockerfile to build    # 此处为 Tekton 的变量替换格式 $(xxx),    # 该变量会去找到 resources 中名为 docker-source 的 inputs 的目录    # 在这个场景下,即为 Dockefile 所在的目录      default: $(resources.inputs.docker-source.path)/Dockerfile    - name: pathToContext      type: string      description: |        The build context used by Kaniko        (https://github.com/GoogleContainerTools/kaniko#kaniko-build-contexts)     # 同上,context 的地址与 Dockerfile 地址一致      default: $(resources.inputs.docker-source.path)  # 申明了两个 resource,一个 input 一个 output  resources:    inputs:      - name: docker-source        type: git    outputs:      - name: builtImage        type: image  steps:    - name: build-and-push      image: gcr.io/kaniko-project/executor:v0.16.0      # 需要指定 DOCKER_CONFIG 来允许 kaniko 检测 docker credential      env:        - name: "DOCKER_CONFIG"          value: "/tekton/home/.docker/"      command:        - /kaniko/executor      args:      # 使用了变量替换,第一个和第三个从 params 中取值      # 第二个为 resource 中名为 builtImage 的 outputs 的 url      # 即镜像仓库地址        - --dockerfile=$(params.pathToDockerFile)        - --destination=$(resources.outputs.builtImage.url)        - --context=$(params.pathToContext)
复制代码

有了 Task 后,就能创建 TaskRun 来执行 Task。注意,在 spec 中申明了 serviceAccountName 用于指定私有仓库的权限。

apiVersion: tekton.dev/v1beta1kind: TaskRunmetadata:  name: build-docker-image-from-git-source-task-runspec:  serviceAccountName: tutorial-service  taskRef:    name: build-docker-image-from-git-source  params:    # 传递参数进入 Task    - name: pathToDockerFile      value: Dockerfile    - name: pathToContext      value: $(resources.inputs.docker-source.path)/examples/microservices/leeroy-web #configure: may change according to your source  resources:    inputs:      - name: docker-source        resourceRef:          name: git-input    outputs:      - name: builtImage        resourceRef:          name: image-output
复制代码

至此,一个更为复杂的流水线也构建完成了。

DAG

在 Tekton 中,DAG(有向无环图)的功能是原生支持的。只需要通过申明 runAfter 及 from 便可以便利的使 Pipeline 以 DAG 方式运行。

  • from:当 Task 的 Inputs 依赖于上一个 Task 的 Outputs 时,可以通过 from 参数来指定

  • runAfter:当 Task 间没有资源依赖,但需要使一个 Task 在另外一个 Task 之后运行的话,可以使用 runAfter 来指定。

- name: lint-repo  taskRef:    name: pylint  resources:    inputs:      - name: workspace        resource: my-repo- name: test-app  taskRef:    name: make-test  resources:    inputs:      - name: workspace        resource: my-repo- name: build-app  taskRef:    name: kaniko-build-app  runAfter:    - test-app  resources:    inputs:      - name: workspace        resource: my-repo    outputs:      - name: image        resource: my-app-image- name: build-frontend  taskRef:    name: kaniko-build-frontend  runAfter:    - test-app  resources:    inputs:      - name: workspace        resource: my-repo    outputs:      - name: image        resource: my-frontend-image- name: deploy-all  taskRef:    name: deploy-kubectl  resources:    inputs:      - name: my-app-image        resource: my-app-image        from:          - build-app      - name: my-frontend-image        resource: my-frontend-image        from:          - build-frontend
复制代码

例如在上面的例子中,任务会以下顺序运行:

        |            |        v            v     test-app    lint-repo    /        \   v          vbuild-app  build-frontend   \          /    v        v    deploy-all
复制代码
  • lint-repo 和 test-app 中的 Task 没有 from 或 runAfter 关键字,会同时开始执行。

  • 一旦 test-app 完成,build-app 和 build-frontend 都会开始同时执行,因为它们 runAfter 于 test-app

  • deploy-all 会在 build-app 和 build-frontend 都完成后才执行,因为它需要的资源 from 于这二者。


再来看看 Tekton 是怎么样来实现这段逻辑的:


在 Pipeline 的 Controller 中,一旦监听到 Pipeline 的创建,在创建对应的 TaskRun 之前,会先检测 Pipeline 中的依赖顺序并构建 DAG 图:

d, err := dag.Build(v1beta1.PipelineTaskList(pipelineSpec.Tasks))
...
// Build returns a valid pipeline Graph. Returns error if the pipeline is invalidfunc Build(tasks Tasks) (*Graph, error) { d := newGraph()
deps := map[string][]string{} // Add all Tasks mentioned in the `PipelineSpec` for _, pt := range tasks.Items() { if _, err := d.addPipelineTask(pt); err != nil { return nil, fmt.Errorf("task %s is already present in Graph, can't add it again: %w", pt.HashKey(), err) } deps[pt.HashKey()] = pt.Deps() } // Process all from and runAfter constraints to add task dependency for pt, taskDeps := range deps { for _, previousTask := range taskDeps { if err := addLink(pt, previousTask, d.Nodes); err != nil { return nil, fmt.Errorf("couldn't add link between %s and %s: %w", pt, previousTask, err) } } } return d, nil}
...
// Node represents a Task in a pipeline.type Node struct { // Task represent the PipelineTask in Pipeline Task Task // Prev represent all the Previous task Nodes for the current Task Prev []*Node // Next represent all the Next task Nodes for the current Task Next []*Node}
// Graph represents the Pipeline Graphtype Graph struct { //Nodes represent map of PipelineTask name to Node in Pipeline Graph Nodes map[string]*Node}
复制代码

Step 执行顺序

Pipeline 中可以进行对 Task 的顺序控制,那么 Task 中呢?


在 Kubernetes 中,Pod 里的 Container 是并行启动的。而在 Tekton 中,虽然 Task 对应 Pod,Task 中的 Step 对应 Container,但 Task 中的 Step 却是顺序执行的。要了解 Tekton 是怎么完成这样的顺序控制,首先我们来看一下一个 Tekton 的 Pod。


在这个 Pod 中,除了用户需要运行的 Container,还被注入了一个 InitContainer:

initContainers:  - command:    - cp    - /ko-app/entrypoint    - /tekton/tools/entrypoint    image: gcr.io/tekton-releases/github.com/tektoncd/pipeline/cmd/entrypoint    imagePullPolicy: IfNotPresent    name: place-tools    resources: {}    terminationMessagePath: /dev/termination-log    terminationMessagePolicy: File    volumeMounts:    - mountPath: /tekton/tools      name: tekton-internal-tools    - mountPath: /var/run/secrets/kubernetes.io/serviceaccount      name: default-token-t2mbw      readOnly: true
复制代码

这个 InitContainer copy 了一个 entrypoint 的二进制到 Pod 中。再看下用户的 container,我们可以看到 Pod 的执行命令被 Tekton 改写了一下:

- args:    - -wait_file    - /tekton/downward/ready    - -wait_file_content    - -post_file    - /tekton/tools/0    - -termination_path    - /tekton/termination    # 下面为用户原本的执行命令    - ...    - ...    command:    - /tekton/tools/entrypoint
复制代码
  • -post_file:指定了 Step 完成后的文件写入路径。如果 Step 失败,则写入到 {{post_file}}.err。可以看到上面的写入路径为 /tekton/tools/0,最后的这个数字即为 Step 的编号。

  • -wait_file:指定了在启动下一个 Step 之前要查看的文件路径。它将监听 {{wait_file}} 和 {{wait_file}}.err。若有错误则跳过执行写入 {{post_file}}.err 并返回错误(exitCode >= 0);若无错误则执行下一个 Step。如上例子为第一个 step,若为第二个 step,wait_ file 的地址会是 /tekton/tools/0,也就是上一个 step 的 post_file 地址。

资源控制

在 Kubernetes 中,一个 Pod 被调度需要节点满足 Pod 中的所有 Container 的资源。如下图:

这个 Pod 有 4 个容器,总共需要 9 个 CPU。Kubernetes 将把这个 Pod 调度到一个拥有 9 个可用 CPU 的节点上。如果没有节点有 9 个可用 CPU,Pod 将被调度失败并无法启动。

而对于 Tekton 而言,因为 Pod 中的 Container 会顺序执行,所以只需要满足这个 Pod 中资源最大的 Container 即可。对于同一个 TaskRun,Tekton 会获取最大请求,并让一个 Container 去请求这些资源,其他都设为 0。


如下,该 Pod 请求 4 个 CPU,而不是 9 个。这样的资源控制方式更为合理且所有的 Step 容器仍保留所需要的资源。


在有 LimitRange 限制 Container 必须有资源的的情况下,每个 Container 最小会设置为 LimitRange 的设置。

源码部分逻辑如下:

func resolveResourceRequests(containers []corev1.Container, limitRangeMin corev1.ResourceList) []corev1.Container {    max := allZeroQty()    resourceNames := []corev1.ResourceName{corev1.ResourceCPU, corev1.ResourceMemory, corev1.ResourceEphemeralStorage}    maxIndicesByResource := make(map[corev1.ResourceName]int, len(resourceNames))    for _, resourceName := range resourceNames {        maxIndicesByResource[resourceName] = -1    }
// Find max resource requests and associated list indices for // containers for CPU, memory, and ephemeral storage resources for i, c := range containers { for k, v := range c.Resources.Requests { if v.Cmp(max[k]) > 0 { maxIndicesByResource[k] = i max[k] = v } } }
// Use zeroQty if request value is not set for min ...
// Set all non max resource requests to 0. Leave max request at index // originally defined to account for limit of step. for i := range containers { if containers[i].Resources.Requests == nil { containers[i].Resources.Requests = limitRangeMin continue } for _, resourceName := range resourceNames { if maxIndicesByResource[resourceName] != i { containers[i].Resources.Requests[resourceName] = limitRangeMin[resourceName] } } }
return containers}
复制代码

数据传递

除了 PipelineResource 以外,Tekton 还提供了其他数据传递的方式。


PipelineResource 仍处于 Alpha 版本,它有可能会被重新设计、替换、弃用或者完全删除。Tekton 社区鼓励用户用 Task 代替 PipelineResources。

Workspace

Workspace 与 Kubernetes 中 Volume 概念几乎保持一致,只不过并不是 Pod 层级的而是作用于 Tekton 资源层级的。Workspace 在 Pipeline 中使用时是一个抽象的概念,实际的存储类型需要在 PipelineRun 中指定。详见:Workspaces。


Workspaces 地址:https://github.com/tektoncd/pipeline/blob/master/docs/workspaces.md

Results

Tekton 提供了一个固定目录用于存放 Task 的输出:/tekton/results

apiVersion: tekton.dev/v1alpha1kind: Taskmetadata:  name: print-date  annotations:    description:       A simple task that prints the date to make sure your cluster / Tekton is working properly.spec:  results:    - name: "current-date"      description: "The current date"  steps:    - name: print-date      image: bash:latest      args:        - "-c"        -           date > /tekton/results/current-date
复制代码

如上,该 task 将日期输出到了 /tekton/results/current-date 中。同时,也会被作为 Results 字段加到 TaskRun 的 Status 中。这样,其他的 Task 便可以通过 $(tasks..results.) 来获取到该 Task 的 results。(变量替换将会实际从 TaskRun 中获取到 Results 的值)

其他流程控制功能

条件判断

低版本可以使用 Conditions,高版本推荐使用 WhenExpressions(Conditions 将在不久后废弃,完全替换为 WhenExpressions)。WhenExpressions 由 Input、Operator、Values 三部分组成,其中 Input 可以使用 Tekton 的 Parameter 或者 Results,Operator 目前仅支持 in 和 notin:

tasks:  - name: first-create-file    when:      - input: "$(params.path)"        operator: in        values: ["README.md"]    taskRef:      name: first-create-file
复制代码
错误重尝

通过 retries 来指定任务失败后重新尝试的次数:

tasks:  - name: build-the-image    retries: 3    taskRef:      name: build-push
复制代码
退出处理

通过 finally 指定在 pipeline 结束时执行的 task,无论 pipeline 的结果是成功或失败。

spec:  tasks:    - name: tests      taskRef:        Name: integration-test  finally:    - name: cleanup-test      taskRef:        Name: cleanup
复制代码
取消执行

要取消当前正在执行的 PipelineRun,可以在其 Spec 中更新 Status 为取消。当 PipelineRun 被取消时,所有相关的 Pods 都被删除。例如:

apiVersion: tekton.dev/v1beta1kind: PipelineRunmetadata:  name: go-example-gitspec:  # […]  status: "PipelineRunCancelled"
复制代码

Pipeline 暂停的逻辑与之类似,但暂停 PR 尚未合入,暂停功能也在 Tekton 今年的 Roadmap 中。

Runs

Runs 是一个进行中的 feature,Run 允许实例化和执行一个 Custom Task,这个 Custom Task 可以通过用户自定义的 controller 来执行。这对于用户来说是一个非常实际的功能,可以通过自己写的 Controller 来定义 Task 的逻辑,而不再拘泥于 Tekton 定义的 Task。


发布于: 2021 年 02 月 07 日阅读数: 70
用户头像

火山引擎

关注

还未添加个人签名 2020.07.28 加入

火山引擎是字节跳动旗下的数字服务与智能科技品牌,基于公司服务数亿用户的大数据、人工智能和基础服务等能力,为企业客户提供系统化全链路解决方案,助力企业务实地创新,实现业务持续快速的增长。

评论

发布
暂无评论
Kubernetes 原生 CI/CD 构建框架 Tekton 详解