引子
如果有关注过 Knative 社区动态的同学,可能会知道最近发生了一件比较大的新闻,三大组件之一的 build 项目被人提了一个很残忍的 Proposal(https://github.com/knative/build/issues/614
),并且专门在项目 Readme 的开头加了个 NOTE:
???? NOTE: There is an open proposal to deprecate this component in favor of Tekton Pipelines. If you are a new user, consider using Tekton Pipelines, or another tool, to build and release. If you use Knative Build today, please give feedback on the deprecation proposal.
这个 Proposal 的目的是想要废弃 Knative 的 build 模块,Knative 只专注做 serverless,而将 build 模块代表的 CI/CD 功能全盘交出,让用户自己选择合适的 CI/CD 工具。Knative 只负责将镜像运行,同时提供 serverless 相关的事件驱动等功能,不再关心镜像的构建过程。
虽然目前为止,该 Proposal 还在开放征求社区的意见,不过,从留言来看,build 模块未来还是大概率会被 deprecate。因为 Knative build 的替代者 Tekton 已经展露头脚,表现出更强大的基于 kubernetes 的 CI/CD 能力,Tekton 的设计思路其实也是来源于 Knative build 的,现有用户也可以很方便的从 build 迁移至 Tekton。
Tekton 是什么
Tekton 是一个谷歌开源的 kubernetes 原生 CI/CD 系统,功能强大且灵活,开源社区也正在快速的迭代和发展壮大。google cloud 已经推出了基于 Tekton 的服务(https://cloud.google.com/Tekton/
)。
其实 Tekton 的前身是 Knative 的 build-pipeline 项目,从名字可以看出这个项目是为了给 build 模块增加 pipeline 的功能,但是大家发现随着不同的功能加入到 Knative build 模块中,build 模块越来越变得像一个通用的 CI/CD 系统,这已经脱离了 Knative build 设计的初衷,于是,索性将 build-pipeline 剥离出 Knative,摇身一变成为 Tekton,而 Tekton 也从此致力于提供全功能、标准化的原生 kubernetesCI/CD 解决方案。
Tekton 虽然还是一个挺新的项目,但是已经成为 Continuous Delivery Foundation (CDF) 的四个初始项目之一,另外三个则是大名鼎鼎的 Jenkins、Jenkins X、Spinnaker,实际上 Tekton 还可以作为插件集成到 JenkinsX 中。所以,如果你觉得 Jenkins 太重,没必要用 Spinnaker 这种专注于多云平台的 CD,为了避免和 Gitlab 耦合不想用 gitlab-ci,那么 Tekton 值得一试。
Tekton 的特点是 kubernetes 原生,什么是 kubernetes 原生呢?简单的理解,就是 all in kubernetes,所以用容器化的方式构建容器镜像是必然,另外,基于 kubernetes CRD 定义的 pipeline 流水线也是 Tekton 最重要的特征。
那 Tekton 都提供了哪些 CRD 呢?
- Task:顾名思义,task 表示一个构建任务,task 里可以定义一系列的 steps,例如编译代码、构建镜像、推送镜像等,每个 step 实际由一个 Pod 执行。
- TaskRun:task 只是定义了一个模版,taskRun 才真正代表了一次实际的运行,当然你也可以自己手动创建一个 taskRun,taskRun 创建出来之后,就会自动触发 task 描述的构建任务。
- Pipeline:一个或多个 task、PipelineResource 以及各种定义参数的集合。
- PipelineRun:类似 task 和 taskRun 的关系,pipelineRun 也表示某一次实际运行的 pipeline,下发一个 pipelineRun CRD 实例到 kubernetes 后,同样也会触发一次 pipeline 的构建。
- PipelineResource:表示 pipeline input 资源,比如 github 上的源码,或者 pipeline output 资源,例如一个容器镜像或者构建生成的 jar 包等。
他们大概有如下图所示的关系:
图片描述
上手实践
部署
Tekton 部署很简单,理论上只需下载 [官方的 yaml 文件](),然后执行 kubectl create -f 一条命令就可以搞定。但是由于在国内,我们无法访问 gcr.io 镜像仓库,所以需要自行替换官方部署 yaml 文件中的镜像。
运行起来后可以在 Tekton-pipelines namespace 下看到两个 deployment:
# kubectl -n Tekton-pipelines get deploy
NAME READY UP-TO-DATE AVAILABLE AGE
Tekton-pipelines-controller 1/1 1 1 10d
Tekton-pipelines-webhook 1/1 1 1 10d
这就是运行 Tekton 所需的所有服务,一个控制器 controller 用来监听上述 CRD 的事件,执行 Tekton 的各种 CI/CD 逻辑,一个 webhook 用于校验创建的 CRD 资源。
webhook 使用了 kubernetes 的 admissionwebhook 机制,所以,在我们 kubectl create 一个 taskRun 或者 pipelineRun 时,apiserver 会回调这里部署的 Tekton webhook 服务,用于校验这些 CRD 字段等的正确性。
构建一个 Java 应用
部署完 Tekton 之后,我们就可以开始动手实践了,下面以构建一个 springboot 工程为例。
假设我们新开发了一个名为 ncs 的 springboot 项目,为了将该项目构建成镜像并上传至镜像仓库,我们可以梳理一个最简单的 CI 流程如下:
- 从 git 仓库拉取代码
- maven 编译打包
- 构建镜像
- 推送镜像
当然在 CI 流程之前,我们先需要在项目中增加 dockerfile,否则构建镜像无从谈起。
0. 添加 dockerfile
FROM hub.c.163.com/qingzhou/tomcat:7-oracle-jdk-rev4
ENV TZ=Asia/Shanghai LANG=C.UTF-8 LANGUAGE=C.UTF-8 LC_ALL=C.UTF-8
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
WORKDIR /usr/local/tomcat
RUN rm -rf webapps/*
COPY setenv.sh $CATALINA_HOME/bin/
COPY ./target/*.war webapps/
ENTRYPOINT ["catalina.sh", "run"]
一个示例如上所示,dockerfile 的逻辑比较简单:引用一个 tomat 的基础镜像,然后把 maven 构建完生成的 war 包复制到 webapps 目录中,最后用脚本 catalina.sh 运行即可。
当然这里有个很有用的细节,我们会项目中添加一个名为 setenv.sh 的脚本,在 dockerfile 里会 COPY$CATALINA_HOME/bin/
。setenv.sh 脚本里可以做一些 tomcat 启动之前的准备工作,例如可以设置一些 JVM 参数等:
export NCE_JAVA_OPTS="$NCE_JAVA_OPTS -Xms${NCE_XMS} -Xmx${NCE_XMX} -XX:MaxPermSize=${NCE_PERM} -Dcom.netease.appname=${NCE_APPNAME} -Dlog.dir=${CATALINA_HOME}/logs"
如果你也研究过 catalina.sh 脚本,就会发现脚本里默认会执行 setenv.sh,实际上这也是官方推荐的初始化方式。
elif [-r "$CATALINA_HOME/bin/setenv.sh"]; then
. "$CATALINA_HOME/bin/setenv.sh"
fi
1. 从 git 仓库拉取代码
添加完 dockerfile 之后,我们可以正式开始研究如何使用 Tekton 构建这个 ncs 项目了。
首先第一步,需要将代码从远程 git 仓库拉下来。
Tekton 中可以使用 pipelineresource 这个 CRD 表示 git 仓库远程地址和 git 分支,如下所示:
apiVersion: Tekton.dev/v1alpha1
kind: PipelineResource
metadata:
name: ncs-git-source
spec:
type: git
params:
- name: url
value: https://github.com/ethfoo/test.git
- name: revision
value: master
其中的 revision 可以使用分支、tag、commit hash。
实际上 git 拉取代码这种通用的操作,只需要我们定义了 input 的 resource,Tekton 已经默认帮我们做好了,不需要在 task 中写 git pull 之类的 steps。目前我们的 task 可以写成如下所示:
apiVersion: Tekton.dev/v1alpha1
kind: Task
metadata:
name: ncs
spec:
inputs:
resources:
- name: gitssh
type: git
git 拉取代码还存在安全和私有仓库的权限问题,基于 kubernetes 原生的 Tekton 当然是采用 secret/serviceaccount 来解决。
对于每个项目组,可以定义一个公共的私有 ssh key,然后放到 secret 中,供 serviceaccount 引用即可。
apiVersion: v1
kind: ServiceAccount
metadata:
name: nce-qingzhou
namespace: Tekton-test
secrets:
- name: ncs-git-ssh
---
apiVersion: v1
kind: Secret
metadata:
name: ncs-git-ssh
namespace: Tekton-test
annotations:
Tekton.dev/git-0: g.hz.netease.com
type: kubernetes.io/ssh-auth
data:
ssh-privatekey: LS0tLS1CRUd...
known_hosts: W2cuaHoub...
最后,这个 serviceaccount 要怎么使用呢,我们接着往下看。
2. maven 编译打包
拉下来项目代码之后,开始进入使用 maven 编译打包阶段。而这个阶段就需要我们自己定义 task 的 steps 来实现各种 CI/CD 的步骤了。
实际的原理也很简单,定义的一个 steps 实际上就是新建一个 pod 去执行自定义的操作。
对于 maven 编译来说,我们首先需要找一个安装有 maven 的镜像,然后在容器的 command/args 里加上 mvn 编译的命令。示例如下:
spec:
inputs:
resources:
- name: ncs-git-source
type: git
params:
# These may be overridden, but provide sensible defaults.
- name: directory
description: The directory containing the build context.
default: /workspace/ncs-git-source
steps:
- name: maven-install
image: maven:3.5.0-jdk-8-alpine
workingDir: "${inputs.params.directory}"
args:
[
"mvn",
"clean",
"install",
"-D maven.test.skip=true",
]
volumeMounts:
- name: m2
mountPath: /root/.m2
由于 Tekton 会给每个构建的容器都挂载 /workspace 这个目录,所以每一个 steps 步骤里都可以在 /workspace 中找到上一步执行的产物。
git 拉取代码可以认为是一个默认的 steps,这个 steps 的逻辑里 Tekton 会把代码放到 /workspace/{resources.name}中。上面我们定义的 PipelineResource 名为 ncs-git-resource,所以 ncs 这个工程的代码会被放在 /workspace/ncs-git-resource 目录中。
所以在 maven-install 这个 steps 中,我们需要在 /workspace/ncs-git-resource 中执行 mvn 命令,这里我们可以使用 workingDir 字段表示将该目录设置为当前的工作目录。同时为了避免写死,这里我们定义为一个 input 的变量 params,在 workingDir 中使用 ${}
的方式引用即可。
实际的使用中,由于每次构建都是新起容器,在容器中执行 maven 命令,一般都是需要将 maven 的 m2 目录挂载出来,避免每次编译打包都需要重新下载 jar 包。
steps:
- name: maven-install
...
volumeMounts:
- name: m2
mountPath: /root/.m2
volumes:
- name: m2
hostPath:
path: /root/.m2
3. docker 镜像的构建和推送
Tekton 标榜自己为 kubernetes 原生,所以想必你也意识到了其中很重要的一点是,所有的 CI/CD 流程都是由一个一个的 pod 去运行。docker 镜像的 build 和 push 当然也不例外,这里又绕不开另外一个话题,即如何在容器中构建容器镜像。
一般我们有两种方式,docker in docker(dind)和 docker outside of docker(dood)。实际上两者都是在容器中构建镜像,区别在于,dind 方式下在容器里有一个完整的 docker 构建系统,可直接在容器中完成镜像的构建,而 dood 是通过挂载宿主机的 docker.sock 文件,调用宿主机的 docker daemon 去构建镜像。
dind 的方式可直接使用官方的 dind 镜像(https://hub.docker.com/_/docker
),当然也可以采用一些其他的开源构建方式,例如 kaniko,makisu 等。docker in docker 的方式对用户屏蔽了宿主机,隔离和安全性更好,但是需要关心构建镜像的分层缓存。
dood 的方式比较简单易用,只需要挂载了 docker.sock,容器里有 docker 客户端,即可直接使用宿主机上的 docker daemon,所以构建的镜像都会在宿主机上,宿主机上也会有相应的镜像分层的缓存,这样也便于加快镜像拉取构建的速度,不过同时也需要注意定时清理冗余的镜像,防止镜像 rootfs 占满磁盘。
如果是在私有云等内部使用场景下,可采用 dood 的方式。这里以 dood 的方式为例。
首先要在 task 中加一个 input param 表示镜像的名称。
spec:
inputs:
params:
- name: image
description: docker image
然后在 task 的 steps 中加入镜像的 build 和 push 步骤。
steps:
- name: dockerfile-build
image: docker:git
workingDir: "${inputs.params.directory}"
args:
[
"build",
"--tag",
"${inputs.params.image}",
".",
]
volumeMounts:
- name: docker-socket
mountPath: /var/run/docker.sock
- name: dockerfile-push
image: docker:git
args: ["push", "${inputs.params.image}"]
volumeMounts:
- name: docker-socket
mountPath: /var/run/docker.sock
volumes:
- name: docker-socket
hostPath:
path: /var/run/docker.sock
type: Socket
了解 kubernetes 的同学一定对这种 yaml 声明式的表述不会陌生,实际上上面的定义和一个 deployment 的 yaml 十分类似,这也使得 Tekton 很容易入门和上手。
4. 构建执行
在 Tekton 中 task 只是一个模版,每次需要定义一个 taskrun 表示一次实际的运行,其中使用 taskRef 表示引用的 task 即可。
apiVersion: Tekton.dev/v1alpha1
kind: TaskRun
metadata:
generateName: ncs-
spec:
inputs:
resources:
- name: gitssh
resourceRef:
name: ncs-git-source
taskRef:
name: ncs
这里的 taskrun 需要注意的是,inputs.resources 需要引用上文定义的 PipelineResource,所以 resourceRef.name=ncs-git-source,同时 reources.name 也需要和上文 task 中定义的 resources.name 一致。
这里还有另外一种写法,如果你不想单独定义 PipelineResource,可以将 taskrun 里的 resources 使用 resourceSpec 字段替换,如下所示。
inputs:
params:
- name: image
value: hub.c.163.com/test/ncs:v1.0.0
resources:
- name: ncs-git-source
resourceSpec:
params:
- name: url
value: ssh://git@netease.com/test/ncs.git
- name: revision
value: f-dockerfile
type: git
serviceAccount: nce-qingzhou
taskRef:
name: ncs
当然,别忘记把上面创建的 serviceaccount 放到 taskrun 中,否则无法拉取私有 git 仓库代码。
最后,我们可以把上面的文件保存,使用 kubectl create -f ncs-taskrun.yml 来开始一段 taskrun 的构建。
还需要提醒的是,taskrun 只表示一次构建任务,你无法修改 taskrun 中的字段让它重新开始,所以我们没有在 taskrun 的 metadata 中定义 name,只加了 generateName,这样 kubernetes 会帮我们在 taskrun name 中自动加上一个 hash 值后缀,避免每次手动改名创建。
pipeline 流水线
既然 Tekton 是一个 CI/CD 工具,我们除了用它来编译和构建镜像,还可以做更多,例如,加入一些自动化测试的流程,对接其他 kubernetes 集群实现容器镜像的更新部署。
当然,这一切都放到 task 里的 steps 也未尝不可,但是这样无法抽象出各种 task 进行组织和复用,所以 Tekton 提供了更高一级的 CRD 描述,Pipeline 和 PipelineRun,Pipeline 中可以引用很多 task,而 PipelineRun 可用来运行 Pipeline。Pipeline 的 yaml 模版和 task 大同小异,这里暂不详述,相信你看一遍官方文档也能很快上手。
总结
虽然 Tekton 还很年轻,我们网易云轻舟团队已经开始在内部尝试实践,使用 tekton 作为内部服务的镜像构建推送平台。
随着云原生浪潮的到来,Kubernetes 已经成为事实上的标准,Tekton 正脱胎于这股浪潮之中,基于 CRD、controller 设计思想从一出生就注定会更适合 kubernetes。相比其他老牌的 CI/CD 项目,Tekton 还没那么的成熟,不过套用一句现在流行的话:一代人终将老去,但总有人正年轻
。
看着目前的趋势,未来可期。
参考:1. https://kurtmadel.com/posts/cicd-with-kubernetes/Tekton-standardizing-native-kubernetes-cd/
2. https://developer.ibm.com/tutorials/knative-build-app-development-with-Tekton/