作者 | 乔克
博客 | https://www.coolops.cn
分享 | 运维开发故事(ID:mygsdcsf)
上一篇文章我们介绍了 Tekton 的安装并且做了简单的测试,但是我们并不知其所以然,而这篇文章主要带大家来了解以及学习所以然。
Tekton 是开源的云原生 CI/CD 项目,是基于 Kubernetes CRD 来定义 Pipeline,功能强大并且很容易扩展。
在上篇文章中,我们安装完 Tekton 之后,可以看到安装的 CRD 如下:
# kubectl get crd | grep tekton
clustertasks.tekton.dev 2022-02-28T06:15:38Z
conditions.tekton.dev 2022-02-28T06:15:38Z
extensions.dashboard.tekton.dev 2022-02-28T06:18:40Z
pipelineresources.tekton.dev 2022-02-28T06:15:38Z
pipelineruns.tekton.dev 2022-02-28T06:15:38Z
pipelines.tekton.dev 2022-02-28T06:15:38Z
runs.tekton.dev 2022-02-28T06:15:38Z
taskruns.tekton.dev 2022-02-28T06:15:38Z
tasks.tekton.dev 2022-02-28T06:15:38Z
其中 Task
、TaskRun
、Pipeline
、PipelineRun
、PipelineResource
、Condition
作为其核心 CRD,这里主要介绍它们。
- Task:定义构建任务,它由一系列有序 steps 构成。每个 step 可以定义输入和输出,且可以将上一个 step 的输出作为下一个 step 的输入。每个 step 都会由一个 container 来执行。
- TaskRun:Task 用于定义具体要做的事情,并不会真正的运行,而 TaskRun 就是真正的执行者,并且会提供执行所需需要的参数,一个 TaskRun 就是一个 Pod。
- Pipeline:顾名思义就是流水线,它由一系列 Tasks 组成。就像 Task 中的 step 一样,上一个 Task 的输出可以作为下一个 Task 的输入。
- PipelineRun:Pipeline 的实际执行,创建后会创建 Pod 来执行 Task,一个 PipelineRun 中有多个 Task。
- PipelineResource:主要用于定义 Pipeline 的资源,常见的如 Git 地址、Docker 镜像等。
- Condition:它主要是在 Pipeline 中用于判断的,Task 的执行与否通过 Condition 的判断结果来决定。
Tips:PipelineResource 和 Condition 都会被废弃。但是在低版本中还是会继续使用,所以这里会简单介绍一下。
图片来自官网(tekton.dev)
如上图所示,一个 Pipeline 是由许多 Task 组成,每个 Task 又由许多 step 组成。在实际工作中,我们可以灵活定义各种 Task,然后根据需要任意组合 Task 形成各类 Pipeline 来完成不同的需求。
实现原理
上面大致介绍了 Tekton 的主要 CRD 以及它们所具备的能力,那么,Tekton 是如何把这些 CRD 串联起来的呢?
我们在安装完 Tekton 后,可以看到如下两个 Pod。
# kubectl get po -n tekton-pipelines
NAME READY STATUS RESTARTS AGE
tekton-pipelines-controller-75c456df85-qxvq2 1/1 Running 0 2d22h
tekton-pipelines-webhook-5bc8d6b7c4-w6pdn 1/1 Running 0 2d22h
一个是 tekton-pipelines-controller,一个是 tekton-pipelines-webhook。其实从命名方式就可以看出,一个是 tekton 的控制器,用于监听 CRD 对象,一个是 tekton 的网络钩子,用于做 CRD 校验,其中 tekton-pipelines-controller 就是 Tekton 的核心实现 Pod。
tekton-pipelines-controller 在启动的时候会初始化两个 Controller:PipelineRunController 以及 TaskRunController。我们可以通过 main.go(cmd/controller/main.go)看到,如下:
......
go func() {
// start the web server on port and accept requests
log.Printf("Readiness and health check server listening on port %s", port)
log.Fatal(http.ListenAndServe(":"+port, mux))
}()
ctx = filteredinformerfactory.WithSelectors(ctx, v1beta1.ManagedByLabelKey)
sharedmain.MainWithConfig(ctx, ControllerLogKey, cfg,
taskrun.NewController(opts, clock.RealClock{}),
pipelinerun.NewController(opts, clock.RealClock{}),
)
}
如上所示会通过 taskrun.NewController
和 pipelinerun.NewController
来进行初始化,然后通过 sharedmain.MainWithConfig
调用 controller.StartAll
来启动所有 Controller。
PipelineRunController 通过监听 PipelineRun 对象的变化,然后从 PipelineSpec 中获取 Task 列表并构建成一张有向无环图(DAG),然后通过遍历 DAG 找到可被调度的 Task 节点创建对应的 TaskRun 对象。具体可以通过(pkg/reconciler/pipelinerun/pipelinerun.go)中的 reconcile
方法进行查看。
TaskRunController 监听到 TaskRun 对象的变化,就会将 TaskRun 中的 Task 转化为 Pod,由 Kubernetes 调度执行。可以通过(pkg/reconciler/taskrun/taskrun.go)中的 reconcile
方法进行查看。
利用 Kubernetes 的 OwnerReference 机制, PipelineRun Own TaskRun、TaskRun Own Pod、Pod 状态变更时,触发 TaskRun 的 reconcile 逻辑, TaskRun 状态变更时触发 PipelineRun 的 reconcile 逻辑。
图片来自网络
当 TaskRun 的 Pod 变成 running 过后,就会通知第一个 step 容器来执行(通过一个名叫 entrypoint
的二进制文件来完成)。
当然这个 entrypoint
二进制文件也有运行条件的,当且仅当 pipeline 的状态的 annotation 通过Kubernetes Download Api
以文件的方式注入到 step container 后才会启动提供的命令。这句话是不是有点绕?按照官方的说法是:Tekton Pipeline 是通过 Kubernetes Annotation 来跟踪 Pipeline 的状态,而且这些 annotations 会通过 Kubernetes Download Api
以文件的方式注入到 Step Container 中,Step Container 中的 entrypoint
会监听着这些文件,当特定的 annotation 以文件的形式注入进来过后,entrypoint
才会去执行命令。比方说,一个 Task 中有两个 step,第二个 step 中的 entrypoint
会等待,直到 annotation 以文件的形式告诉它第一个 step 已经完成。
我们来梳理一下整体的流程,如下:
- 用户通过 client 创建 PipelineRun 资源
- PipelineRunController 监听到 PipelineRun 资源,就把里面的 Task 组成 DAG(有向无环图),遍历 DAG 得到 Task,并创建 TaskRun
- TaskRunController 监听到 TaskRun 资源,就会通过 Kubernetes 将 Task 转化为 Pod 启动(Task 受 Condition 条件控制)
- Pod 启动后会运行 Task 中的每一个 Step 完成具体的指令
- 运行完成后 Pod 会变成
Completed
状态,同时也会更新 PipelineRun 的状态
到此一个 Pipeline 就运行完成了。
PipelineResources
这里将 PipelintResource 提到最前面来说明,主要是后面的操作有需要它的地方。
PipelineResource 用于定义资源的信息,虽然会被弃用,但是在旧版本中依然会使用。
PipelineResource 的定义很简单,如下:
apiVersion: tekton.dev/v1alpha1
kind: PipelineResource
metadata:
name: hello-word-resource
spec:
type: git
params:
- name: url
value: https://gitee.com/coolops/springboot-helloworld.git
在 TaskRun 中就可以引用 hello-word-resource 资源得到具体的 git 地址。
Tasks
Task 就是一个任务模板,Task 的定义中可以包含变量,在真正执行的时候需要给变量赋值。
Task 通过 input.params 定义入参,每一个入参还可以指定默认值,在每一个 step 中可以 $(params.A) 引用变量。steps 字段表示当前 Task 有哪些步骤组成,每一个 step 都会通过定义一个 container 来执行具体的操作。
Task 主要包括以下元素:
- Parameters:用于定义 params 参数
- Resources:定义输入、输出资源,老版本由 PipelineResources 定义,不过在新版本中 PipelineResources 将被弃用
- Steps:定义具体的操作步骤
- Workspaces:定义工作区,Task 可以共享工作区
- Results:定义结果输出,可以用于展示或者给另外的 Task 使用
Task 的定义如下:
apiVersion: tekton.dev/v1beta1
kind: Task
metadata:
name: maven-build
spec:
resources:
inputs:
- name: repo
type: git
steps:
- name: build
image: maven:3.3-jdk-8
command:
- mvn clean package
workingDir: /workspace/repo
再定义一个构建 Dokcer 镜像并推送到 Hub 的 Task,如下:
apiVersion: tekton.dev/v1beta1
kind: Task
metadata:
name: build-and-push-image
spec:
params:
- name: pathToDockerfile
type: string
default: /workspace/repo/Dockerfile
description: define Dockerfile path
- name: pathToContext
type: string
default: /workspace/repo
description: Docker deamon build context
- name: imageRepo
type: string
default: registry.cn-hangzhou.aliyuncs.com
description: docker image repo
resources:
inputs:
- name: repo
type: git
outputs:
- name: builtImage
type: image
steps:
- name: build-image
image: docker:stable
scripts: |
#!/usr/bin/env sh
docker login $(params.imageRepo)
docker build -t $(resources.outputs.builtImage.url) -f $(params.pathToDockerfile) $(params.pathToContext)
docker push $(resources.outputs.builtImage.url)
volumeMounts:
- name: dockersock
mountPath: /var/run/docker.sock
volumes:
- name: dockersock
hostPath:
path: /var/run/docker.sock
如上,我们可以通过直接编写 shell 脚本的方式来实现需求,而且使用 docker 构建镜像需要 sock 文件,可以像 pod 挂载那样挂载需要的东西。
step 还有其他的配置,比如为某个 step 设置超时时间,如下:
steps:
- name: sleep-then-timeout
image: ubuntu
script: |
#!/usr/bin/env bash
echo "I am supposed to sleep for 60 seconds!"
sleep 60
timeout: 5s
更多的操作可以通过(https://tekton.dev/docs/pipelines/tasks/)进行学习研究。
TaskRuns
Task 在定义好之后,并不会被执行,就像我们定义了一个函数,如果没被调用的话,这个函数就不会被执行一样。而 TaskRun 就可以就好似调用方,用它来执行 Task 里的具体内容。
TaskRun 会设置 Task 需要的参数,并通过 taskRef 字段来引用 Task,如下:
apiVersion: tekton.dev/v1beta1
kind: TaskRun
metadata:
name: build-and-push-image
spec:
params:
- name: imageRepo
value: registry.cn-zhangjiakou.aliyuncs.com
taskRef:
name: build-and-push-image # 关联定义好的task
resources:
inputs:
- name: repo # 指定输入的仓库资源
resourceRef:
name: hello-word-resource
outputs: # 指定输出的镜像资源
- name: builtImage
resourceRef:
name: hello-word-image
通过如上的定义,就将 build-and-push-image 的 Task 进行关联,并且通过 resources 定义 Task 需要的 sources 参数,然后通过 parms 来定义参数,该参数会替代掉 Task 中的默认参数。
在实际中,基本不会去定义 TaskRun,除非自己去测试某个 Task 是否正常。
Pipelines
一个 TaskRun 只能执行一个 Task,当我们需要同时编排许多 Task 的时候,就需要使用 Pipeline 了,就像使用 Jenkinsfile 来编排不同的任务一样。
Pipeline 是一个编排 Task 的模板,通过 spec.params 来声明执行时需要的入参,通过 spec.tasks 来编排具体的 task,除此之外还可以通过 runAfter 来控制 Task 的先后顺序。
先定义一个简单的 Pipeline,如下:
apiVersion: tekton.dev/v1beta1
kind: Pipeline
metadata:
name: build-and-push-image
spec:
resources:
- name: repo
type: git
- name: builtImage
type: image
tasks:
# 构建并推送 Docker 镜像
- name: build-and-push-image
taskRef:
name: build-and-push-image
resources:
inputs:
- name: repo # Task 输入名称
resource: repo # Pipeline 资源名称
outputs:
- name: builtImage
resource: builtImage
上面定义的 Pipeline 关联了 build-and-push-image
Task,该 Task 所需要的输入输出参数,通过 Pipeline 的 spec.resources
定义,这里的 spec.resources
依然依赖 PipelineResources 中定义的具体资源。
上面提到过,如果要在 Pipeline 中控制 Task 顺序,则要使用 runAfter 参数,如下:
- name: test-app
taskRef:
name: make-test
resources:
inputs:
- name: workspace
resource: my-repo
- name: build-app
taskRef:
name: kaniko-build
runAfter:
- test-app
resources:
inputs:
- name: workspace
resource: my-repo
如上 build-app 的 Task 依赖 test-app 的 Task。
除此之外,还可以将上个 Task 的输出作为下一个 Task 的输入,如下。
- name: build-app
taskRef:
name: build-push
resources:
outputs:
- name: image
resource: my-image
- name: deploy-app
taskRef:
name: deploy-kubectl
resources:
inputs:
- name: image
resource: my-image
from:
- build-app
如上通过 from
关键字来引入其他 Task 的输出。
如果要在 Pipeline 中使用条件判断,也可以像以下方式使用 when
关键字。
tasks:
- name: deploy-to-dev
when:
- input: "$(params.branch)"
operator: in
values: ["dev"]
taskRef:
name: deploy-to-dev
---
tasks:
- name: deploy-to-test
when:
- input: "$(params.branch)"
operator: in
values: ["test"]
taskRef:
name: deploy-to-test
注意:when 和 condition 不能同时在一个 Task 中使用,不然会被认定为无效。
还有一个关键字和 when 效果一样,就是 condition。
condition 的作用就是用一些条件来保护 Task,只有在满足条件的情况下才会运行 Task。在 Task 运行之前,会对所有的条件进行判断,只有全部条件成功,才会运行 Task,否则不会允许。
如下定义一个简单的条件语句。
tasks:
- name: deploy-if-branch-is-master
conditions:
- conditionRef: is-master-branch
params:
- name: branch-name
value: my-value
taskRef:
name: deploy
当然条件约束仅针对当前的 Task,如果其他 Task 不受当前 Task 影响,则不受约束。
更多的使用方式见(https://tekton.dev/docs/pipelines/pipelines/)。
PipelineRuns
Pipeline 和 Task 一样,单纯的定义完并不会运行,Pipeline 需要借助 PipelineRun 来完成真正的执行。
PipelineRun 会自动为 Pipeline 中定义的 Task 创建对应的 TaskRun。
下面定义一个简单的 PipelineRun。
apiVersion: tekton.dev/v1beta1
kind: PipelineRun
metadata:
name: build-and-push-image
spec:
pipelineRef:
name: build-and-push-image
resources:
- name: repo
resourceRef:
name: demo-git
- name: builtImage
resourceRef:
name: harbor-image
其中 spec.pipelineRef
用来关联定义的 Pipeline,spec.resources
用来给 Pipeline 传递参数。
上面的 repo 和 builtImage 参数依然需要通过 PipelineResources
定义。不过在新版本,也可以通过 resourceSpec
来进行定义,如下。
apiVersion: tekton.dev/v1beta1
kind: PipelineRun
metadata:
name: build-and-push-image
spec:
pipelineRef:
name: build-and-push-image
resources:
- name: repo
resouorceSpec:
type: git
params:
- name: url
value: https://gitee.com/coolops/springboot-helloworld.git
- name: builtImage
resouorceSpec:
type: image
params:
- name: url
value: registry.cn-hangzhou.aliyuncs.com/coolops/helloworld:latest
Conditions
condition 用于在 Pipeline 中进行条件判断,不过在新版本中会被废弃,使用上面介绍的 when
替代,这里不再做多的介绍了。
鉴权管理
上面介绍了主要的 CRD 以及它们的使用方式,但是还有一种是需要我们关注的,比如代码仓库的密码怎么管理?镜像仓库的密码怎么管理?因为这些都是在实际工作中需要使用的。
Tekton 通过在 PipelineRun
中指定 ServiceAccount
来实现。不过 Tekton 要求定义的每个 Secret 都需要指定对应的 annotation。目前支持的 annotation 有以下两种:
- Git:
tekton.dev/git-**0:** https**:**//github.com
- Docker:
tekton.dev/docker-**0:** https**:**//gcr.io
目前这两种分别支持以下类型。
kubernetes.io/dockercfg
kubernetes.io/dockerconfigjson |
Tekton 到底是如何使用到这些 secret 的呢?
原来,为了使用这些 Secret,Tekton 在实例化 Pod 的时候就会执行凭证初始化, Tekton 会将具体的 Secret 进行关联并聚合到 /tekton/creds 目录中,之后才会执行具体的 Task 步骤。
下面我们具体操作一下,以镜像仓库为例。
(1)创建 secret
apiVersion: v1
kind: Secret
metadata:
name: docker-registry-secret
annotations:
tekton.dev/docker-0: https://gcr.io # Described below
type: kubernetes.io/basic-auth
stringData:
username: <cleartext username>
password: <cleartext password>
其中 tekton.dev/docker-0: [https://gcr.io](https://gcr.io)
用来指定对应的仓库地址。
(2)创建 seviceaccount
apiVersion: v1
kind: ServiceAccount
metadata:
name: docker-registry-sa
secrets:
- name: docker-registry-secret
(3)在 PipelineRun 中引用
apiVersion: tekton.dev/v1beta1
kind: PipelineRun
metadata:
name: demo-pipeline
namespace: default
spec:
serviceAccountName: docker-registry-sa
pipelineRef:
name: demo-pipeline
如果需要同时使用多个 serviceaccount 怎么办呢?比如我们在一条完成的 Pipeline 中,在拉取代码的时候会用到 Git 的账户,在推送镜像的时候会用到镜像仓库的账户。
这时候我们就不能用 serviceAccountName
了,而是需要使用 serviceAccountNames
。serviceAccountNames
是一个 List,可以指定 Task 关联具体的 serviceaccount,如下。
apiVersion: tekton.dev/v1beta1
kind: PipelineRun
metadata:
name: demo-pipeline
namespace: default
spec:
serviceAccountNames:
- taskName: build-app
serviceAccountName: gitlab-sa
- taskName: push-image
serviceAccountName: docker-registry-sa
pipelineRef:
name: demo-pipeline
到这里基本的资源以及介绍完了,弄懂这篇文章,写一个简单的 Pipeline 应该不成问题,后续的文章会分享具体的实践。
参考文档
【1】https://www.infoq.cn/article/aRAYxTo19Bd6AVBmXFQz
【2】https://cloudnative.to/blog/how-tekton-works/