Tekton 的工作原理

结合源码和场景分析 Tekton 的工作原理。

作者 张晓辉 发表于 2020年5月23日

这篇文章是基于 Tekton Pipeline 的最新版本v0.12.1版本。

快速入门请参考:云原生 CICD: Tekton Pipeline 实战 ,实战是基于版本 v0.10.x。

Pipeline CRD 与核心资源的关系

$ k api-resources --api-group=tekton.dev
NAME                SHORTNAMES   APIGROUP     NAMESPACED   KIND
clustertasks                     tekton.dev   false        ClusterTask
conditions                       tekton.dev   true         Condition
pipelineresources                tekton.dev   true         PipelineResource
pipelineruns        pr,prs       tekton.dev   true         PipelineRun
pipelines                        tekton.dev   true         Pipeline
taskruns            tr,trs       tekton.dev   true         TaskRun
tasks                            tekton.dev   true         Task

Tekton Pipelines提供了上面的CRD,其中部分CRD与Kubernetes core中资源相对应

  • Task => Pod
  • Task.Step => Container

工作原理

(图片来自tekton.dev)

Tekton Pipeline 是基于 Knative 的实现,pod tekton-pipelines-controller 中有两个 Knative Controller的实现:PipelineRun 和 TaskRun。

Task的执行顺序

PipelineRun Controller 的 #reconcile()方法,监控到有PipelineRun被创建。然后从PipelineSpec的 tasks 列表,构建出一个图(graph),用于描述Pipeline中 Task 间的依赖关系。依赖关系是通过runAfterfrom,进而控制Task的执行顺序。与此同时,准备PipelineRun中定义的PipelineResources

// 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 Graph
type Graph struct {
	//Nodes represent map of PipelineTask name to Node in Pipeline Graph
	Nodes map[string]*Node
}

func Build(tasks Tasks) (*Graph, error) {
    ...
}

PipelineRun中定义的参数(parameters)也会注入到PipelineSpec中:

pipelineSpec = resources.ApplyParameters(pipelineSpec, pr)

接下来就是调用dag#GetSchedulable()方法,获取未完成(通过Task状态判断)的 Task 列表;

func GetSchedulable(g *Graph, doneTasks ...string) (map[string]struct{}, error) {
    ...
}

为 Task A 创建TaskRun,假如Task配置了Condition。会先为 condition创建一个TaskRun,只有在 condition 的TaskRun运行成功,才会运行 A 的TaskRun;否则就跳过。

Step的执行顺序

这一部分篇幅较长,之前的文章 控制 Pod 内容器的启动顺序 中提到过。

这里补充一下Kubernetes Downward API的使用,Kubernetes Downward API的引入,控制着 Task 的第一个 Step 在何时执行。

TaskRun Controller 在 reconciling 的过程中,在相应的 Pod 状态变为Running时,会将tekton.dev/ready=READY写入到 Pod 的 annotation 中,来通知第一个Step的执行。

Pod的部分内容:

spec:
  containers:
    - args:
      - -wait_file
      - /tekton/downward/ready
      - -wait_file_content
      - -post_file
      - /tekton/tools/0
      - -termination_path
      - /tekton/termination
      - -entrypoint
      - /ko-app/git-init
      - --
      - -url
      - ssh://git@gitlab.nip.io:8022/addozhang/logan-pulse.git
      - -revision
      - develop
      - -path
      - /workspace/git-source
      command:
      - /tekton/tools/entrypoint
      volumeMounts:
      - mountPath: /tekton/downward
        name: tekton-internal-downward
      
  volumes:
    - downwardAPI:
        defaultMode: 420
        items:
        - fieldRef:
            apiVersion: v1
            fieldPath: metadata.annotations['tekton.dev/ready']
          path: ready
      name: tekton-internal-downward

对原生的排序step container进一步处理:启动命令使用entrypoint提供,并设置执行参数:

entrypoint.go

func orderContainers(entrypointImage string, steps []corev1.Container, results []v1alpha1.TaskResult) (corev1.Container, []corev1.Container, error) {
	initContainer := corev1.Container{
		Name:         "place-tools",
		Image:        entrypointImage,
		Command:      []string{"cp", "/ko-app/entrypoint", entrypointBinary},
		VolumeMounts: []corev1.VolumeMount{toolsMount},
	}

	if len(steps) == 0 {
		return corev1.Container{}, nil, errors.New("No steps specified")
	}

	for i, s := range steps {
		var argsForEntrypoint []string
		switch i {
		case 0:
			argsForEntrypoint = []string{
				// First step waits for the Downward volume file.
				"-wait_file", filepath.Join(downwardMountPoint, downwardMountReadyFile),
				"-wait_file_content", // Wait for file contents, not just an empty file.
				// Start next step.
				"-post_file", filepath.Join(mountPoint, fmt.Sprintf("%d", i)),
				"-termination_path", terminationPath,
			}
		default:
			// All other steps wait for previous file, write next file.
			argsForEntrypoint = []string{
				"-wait_file", filepath.Join(mountPoint, fmt.Sprintf("%d", i-1)),
				"-post_file", filepath.Join(mountPoint, fmt.Sprintf("%d", i)),
				"-termination_path", terminationPath,
			}
		}
    ...
}

自动运行的容器

这些自动运行的容器作为 pod 的initContainer会在 step 容器运行之前运行

credential-initializer

用于将 ServiceAccount 的相关secrets持久化到容器的文件系统中。比如 ssh 相关秘钥、config文件以及know_hosts文件;docker registry 相关的凭证则会被写入到 docker 的配置文件中。

working-dir-initializer

收集Task内的各个StepworkingDir配置,初始化目录结构

place-scripts

假如Step使用的是script配置(与command+args相对),这个容器会将脚本代码(script字段的内容)持久化到/tekton/scripts目录中。

注:所有的脚本会自动加上#!/bin/sh\nset -xe\n,所以script字段里就不必写了。

place-tools

entrypoint的二进制文件,复制到/tekton/tools/entrypoint.

Task/Step间的数据传递

针对不同的数据,有多种不同的选择。比如WorkspaceResultPipelineResource。对于由于Task的执行是通过Pod来完成的,而Pod会调度到不同的节点上。因此Task间的数据传递,需要用到持久化的卷。

Step作为Pod中的容器来运行,

Workspace

工作区,可以理解为一个挂在到容器上的卷,用于文件的传递。

persistentVolumeClaim

引用已存在persistentVolumeClaim卷(volume)。这种工作空间,可多次使用,需要先进行创建。比如 Java 项目的 maven,编译需要本地依赖库,这样可以节省每次编译都要下载依赖包的成本。

workspaces:
- name: m2
  persistentVolumeClaim:
    claimName: m2-pv-claim
apiVersion: v1
kind: PersistentVolume
metadata:
  name: m2-pv
  labels:
    type: local
spec:
  storageClassName: manual
  capacity:
    storage: 10Gi
  accessModes:
    - ReadWriteMany
  hostPath:
    path: "/data/.m2"
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: m2-pv-claim
spec:
  storageClassName: manual
  # volumeName: m2-pv
  accessModes:
    - ReadWriteMany
  resources:
    requests:
      storage: 10Gi
volumeClaimTemplate

为每个PipelineRun或者TaskRun创建PersistentVolumeClaim卷(volume)的模板。比如一次构建需要从 git 仓库克隆代码,而针对不同的流水线代码仓库是不同的。这里就会用到volumeClaimTemplate,为每次构建创建一个PersistentVolumeClaim卷。(从0.12.0开始)

生命周期同PipelineRun或者TaskRun,运行之后释放。

workspaces:
- name: git-source
  volumeClaimTemplate:
    spec:
      accessModes:
      - ReadWriteMany
      resources:
        requests:
          storage: 1Gi

相较于persistantVolumeClain类型的workspace,volumeClaimTemplate不需要在每次在PipelineRun完成后清理工作区;并发情况下可能会出现问题。

emptyDir

引用emptyDir卷,跟随Task生命周期的临时目录。适合在TaskStep间共享数据,无法在多个Task间共享。

workspaces:
- name: temp
  emptyDir: {}
configMap

引用一个configMap卷,将configMap卷作为工作区,有如下限制:

使用场景,比如使用maven编译Java项目,配置文件settings.xml可以使用configMap作为工作区

workspaces:
- name: maven-settings
  configmap:
    name: maven-settings
secret

用于引用secret卷,同configMap工作区一样,也有限制:

results

results字段可以用来配置多个文件用来存储Tasks的执行结果,这些文件保存在/tekton/results目录中。

Pipeline中,可以通过tasks.[task-nanme].results.[result-name]注入到其他Task的参数中。

apiVersion: tekton.dev/v1beta1
kind: Task
metadata:
  name: print-date
  annotations:
    description: |
      A simple task that prints the date
spec:
  results:
    - name: current-date-unix-timestamp
      description: The current date in unix timestamp format
    - name: current-date-human-readable
      description: The current date in human readable format
  steps:
    - name: print-date-unix-timestamp
      image: bash:latest
      script: |
        #!/usr/bin/env bash
        date +%s | tee $(results.current-date-unix-timestamp.path)
    - name: print-date-humman-readable
      image: bash:latest
      script: |
        #!/usr/bin/env bash
        date | tee $(results.current-date-human-readable.path)
---
apiVersion: tekton.dev/v1beta1
kind: PipelineRun
metadata:
  name: pass-date
spec:
  pipelineSpec:
    tasks:
      - name: print-date
        taskRef:
          name: print-date
      - name: read-date
        runAfter: #配置执行顺序
          - print-date
        taskSpec:
          params:
            - name: current-date-unix-timestamp
              type: string
            - name: current-date-human-readable
              type: string
          steps:
            - name: read
              image: busybox
              script: |
                echo $(params.current-date-unix-timestamp)
                echo $(params.current-date-human-readable)
        params:
          - name: current-date-unix-timestamp
            value: $(tasks.print-date.results.current-date-unix-timestamp) # 注入参数
          - name: current-date-human-readable
            value: $(tasks.print-date.results.current-date-human-readable) # 注入参数      

执行结果:

┌──────Logs(tekton-pipelines/pass-date-read-date-rhlf2-pod-9b2sk)[all] ──────────                                                                       │
│ place-scripts stream closed                                                                                                                                                                                                                                                             ││ step-read 1590242170                                                                                                                                                                                                                                                                    │
│ step-read Sat May 23 13:56:10 UTC 2020                                                                                                                                                                                                                                                  ││ step-read + echo 1590242170                                                                                                                                                                                                                                                             │
│ step-read + echo Sat May 23 13:56:10 UTC 2020                                                                                                                                                                                                                                           │
│ place-tools stream closed                                                                                                                                                                                                                                                               │
│ step-read stream closed                                                                                                                                                                                                                                                                 │
│

PipelineResource

PipelineResource在最后提,因为目前只是alpha版本,何时会进入beta或者弃用目前还是未知数。有兴趣的可以看下这里:Why Aren’t PipelineResources in Beta?

简单来说,PipelineResource可以通过其他的方式实现,而其本身也存在弊端:比如实现不透明,debug有难度;功能不够强;降低了Task的重用性等。

比如git类型的PipelineResource,可以通过workspacegit-clone Task来实现;存储类型的,也可以通过workspace来实现。

这也就是为什么上面介绍workspace的篇幅比较大。个人也偏向于使用workspace,灵活度高;使用workspace的Task重用性强。

参考