发布时间:2024-01-27 08:30
前面我们的两个任务 test 和 build-and-push 都已经完成了,我们还可以创建一个流水线来将这两个任务组织起来,形成一个流水线,这里就是我们要使用的 Pipeline 这个 CRD 对象。
流水线流程为先运行 test 任务,如果通过了再执行后面的 build-and-push 这个任务,那么我们可以创建一个名为 test-pipeline.yaml 的资源对象
# test-pipeline.yaml
apiVersion: tekton.dev/v1beta1
kind: Pipeline
metadata:
name: test-pipeline
spec:
resources: # 为 Tasks 提供输入和输出资源声明
- name: demo-git
type: git
- name: harbor-image
type: image
tasks: # 添加task到流水线中
# 运行应用测试
- name: test
taskRef:
name: test
resources:
inputs:
- name: repo # Task 输入名称
resource: demo-git # Pipeline 资源名称
# 构建并推送 Docker 镜像
- name: build-and-push
taskRef:
name: build-and-push
runAfter:
- test # 测试任务执行之后
resources:
inputs:
- name: repo # 指定输入的git仓库资源
resource: demo-git
outputs: # 指定输出的镜像资源
- name: builtImage
resource: harbor-image
kubectl apply -f test-pipeline.yaml
定义流水线需要哪些资源,可以是输入或者输出的资源,在这里我们只有一个输入,那就是命名为 repo 的应用程序源码的 GitHub 仓库。接下来定义任务,每个任务都通过 taskRef 进行引用,并传递任务需要的输入参数。
前面我们提到过和通过创建 TaskRun 去触发 Task 任务类似,我们可以通过创建一个 PipelineRun 对象来运行流水线。这里我们创建一个名为 test-pipelinerun.yaml 的 PipelineRun 对象来运行流水线
apiVersion: tekton.dev/v1beta1
kind: PipelineRun
metadata:
name: test-pipelinerun
spec:
serviceAccountName: build-sa
pipelineRef:
name: test-pipeline
resources:
- name: demo-git # 指定输入的git仓库资源
resourceRef:
name: demo-git
- name: harbor-image # 指定输出的镜像资源
resourceRef:
name: harbor-image
kubectl apply -f test-pipelinerun.yaml
通过 serviceAccountName 属性指定 ServiceAccount 对象,pipelineRef 关联流水线对象。创建后就会触发我们的流水线任务
kubectl get pods | grep test-pipelinerun
[root@master1 ~]# tkn pipelinerun describe test-pipelinerun
Name: test-pipelinerun
Namespace: default
Pipeline Ref: test-pipeline
Service Account: build-sa
Timeout: 1h0m0s
Labels:
tekton.dev/pipeline=test-pipeline
️ Status
STARTED DURATION STATUS
1 minute ago --- Running
Resources
NAME RESOURCE REF
∙ demo-git demo-git
∙ harbor-image harbor-image
Taskruns
NAME TASK NAME STARTED DURATION STATUS
∙ test-pipelinerun-test-7lvcn test 1 minute ago --- Running
到这里证明我们的流水线执行成功了。我们将 Tekton 安装在 Kubernetes 集群上,定义了一个 Task,并通过 YAML 清单和 Tekton CLI 创建 TaskRun 对其进行了测试。我们创建了由两个任务组成的 Tektok 流水线,第一个任务是从 GitHub 克隆代码并运行应用程序测试,第二个任务是构建一个 Docker 镜像并将其推送到 Docker Hub 上。
上面我们在构建镜像的时候可以看到镜像的 TAG 我们是写死的,或者需要在每次执行的时候通过参数传递进去,比较麻烦,那么有没有什么办法可以自动生成镜像 TAG 呢?比如根据时间戳来生成一个构建的ID。
这里我们可以通过定义一个 Task 任务,然后通过 script 脚本去获取到数据后传入到 results 中去,我们可以把这些 results 数据传递到流水线中的其他任务中去,比如我们想要获取 git commit 的 SHA 值(唯一性),或者生成一个随机的 ID 来作为镜像 TAG,比如这里我们创建一个名为 generate-build-id 的 Task 任务,定义了 get-timestamp 和 get-buildid 两个 Steps,一个用于生成时间戳,一个用于生成一个包含基本版本的结果值,将结果添加到 results 中去。
# generate-build-id.yaml
apiVersion: tekton.dev/v1beta1
kind: Task
metadata:
name: generate-build-id
spec:
description: >-
Given a base version, this task generates a unique build id by appending
the base-version to the current timestamp.
params:
- name: base-version
description: Base product version
type: string
default: \"1.0\"
results:
- name: timestamp
description: Current timestamp
- name: build-id
description: ID of the current build
steps:
- name: get-timestamp
image: bash:5.0.18
script: |
#!/usr/bin/env bash
ts=`date \"+%Y%m%d-%H%M%S\"`
echo \"Current Timestamp: ${ts}\"
echo ${ts} | tr -d \"\\n\" | tee $(results.timestamp.path)
- name: get-buildid
image: bash:5.0.18
script: |
#!/usr/bin/env bash
ts=`cat $(results.timestamp.path)`
buildId=$(inputs.params.base-version)-${ts}
echo ${buildId} | tr -d \"\\n\" | tee $(results.build-id.path)
kubectl apply -f generate-build-id.yaml
创建完成后,现在我们就可以在 Pipeline 中来使用这个 Task 了,用来生成构建 ID,修改 test-pipeline.yaml,增加 generate-build-id 任务
# test-pipeline.yaml
apiVersion: tekton.dev/v1beta1
kind: Pipeline
metadata:
name: test-pipeline
spec:
resources: # 为 Tasks 提供输入和输出资源声明
- name: demo-git
type: git
- name: harbor-image
type: image
params:
- name: image-tag
type: string
tasks: # 添加task到流水线中
# 运行应用测试
- name: test
taskRef:
name: test
resources:
inputs:
- name: repo # Task 输入名称
resource: demo-git # Pipeline 资源名称
- name: get-build-id
taskRef:
name: generate-build-id
params:
- name: base-version
value: $(params.image-tag)
# 构建并推送 Docker 镜像
- name: build-and-push
taskRef:
name: build-and-push
runAfter:
- test # 测试任务执行之后
- get-build-id
resources:
inputs:
- name: repo # 指定输入的git仓库资源
resource: demo-git
outputs: # 指定输出的镜像资源
- name: builtImage
resource: harbor-image
params:
- name: imageTag
value: \"$(tasks.get-build-id.results.build-id)\"
然后在 build-and-push 任务中通过 “$(tasks.get-build-id.results.build-id)” 获取构建的 ID,将这个 ID 作为参数传入任务中去,所以我们也需要在 build-and-push 任务中增加 build-id 这个参数
# task-build-push.yaml
apiVersion: tekton.dev/v1beta1
kind: Task
metadata:
name: build-and-push
spec:
resources:
inputs: # 定义输入资源
- name: repo #输入资源,就是github的那个仓库
type: git
outputs: # 定义输出资源
- name: builtImage # 输出镜像名字
type: image
params:
- name: pathToDockerfile #指明 dockerfile 在仓库中的哪个位置
type: string
default: $(resources.inputs.repo.path)/Dockerfile # repo资源的路径
description: The path to the dockerfile to build
- name: pathToContext #指明 dockerfile 在仓库中的哪个位置
type: string
default: $(resources.inputs.repo.path) # repo资源的路径
description: the build context used by docker daemon
- name: imageTag
type: string
default: \"v0.2.0\"
description: the docker image tag
steps:
- name: build-and-push
image: docker:stable
script: |
#!/usr/bin/env sh
docker login harbor.k8s.local
docker build -t $(resources.outputs.builtImage.url):$(params.imageTag) -f $(params.pathToDockerfile) $(params.pathToContext)
docker push $(resources.outputs.builtImage.url):$(params.imageTag) # 这边的参数都是在 input 和 output 中定义的
volumeMounts:
- name: dockersock #将docker.sock文件挂载进来,使用宿主机docker daemon 构建镜像
mountPath: /var/run/docker.sock
volumes:
- name: dockersock
hostPath:
path: /var/run/docker.sock
然后需要将 builtImage 这个 output 资源的 url 定义中将镜像 tag 去掉,在 PipelineRun 对象中新增 image-tag 的参数
# test-pipelinerun.yaml
apiVersion: tekton.dev/v1beta1
kind: PipelineRun
metadata:
name: test-pipelinerun
spec:
serviceAccountName: build-sa
pipelineRef:
name: test-pipeline
resources:
- name: demo-git # 指定输入的git仓库资源
resourceRef:
name: demo-git
- name: harbor-image # 指定输出的镜像资源
resourceRef:
name: harbor-image
params:
- name: image-tag
value: \"v0.3.0\"
所有修改完成后,重新执行我们的整个流水线即可
[root@master1 ~]# kubectl apply -f testrpipeline.yaml
pipeline.tekton.dev/test-pipeline configured
[root@master1 ~]# kubectl apply -f test-pipelinerun.yaml
pipelinerun.tekton.dev/test-pipelinerun configured
[root@master1 ~]# tkn pipelinerun logs test-pipelinerun
[test : git-source-demo-git-fsnpn] {\"level\":\"info\",\"ts\":1654411371.3292887,\"caller\":\"git/g it.go:136\",\"msg\":\"Successfully cloned https://github.com/cnych/tekton-demo @ 5e1e3a1d0f167 b9b639df5b802a0f0f81064d21e (grafted, HEAD, origin/master) in path /workspace/repo\"}
[test : git-source-demo-git-fsnpn] {\"level\":\"info\",\"ts\":1654411372.4005253,\"caller\":\"git/g it.go:177\",\"msg\":\"Successfully initialized and updated submodules in path /workspace/repo\" }
[test : run-test] PASS
[test : run-test] ok _/workspace/repo 0.053s
[build-and-push : git-source-demo-git-6p7bj] {\"level\":\"info\",\"ts\":1654411510.421371,\"calle r\":\"git/git.go:136\",\"msg\":\"Successfully cloned https://github.com/cnych/tekton-demo @ 5e1e 3a1d0f167b9b639df5b802a0f0f81064d21e (grafted, HEAD, origin/master) in path /workspace/rep o\"}
[build-and-push : git-source-demo-git-6p7bj] {\"level\":\"info\",\"ts\":1654411511.9356167,\"call er\":\"git/git.go:177\",\"msg\":\"Successfully initialized and updated submodules in path /works pace/repo\"}
[build-and-push : build-and-push] Authenticating with existing credentials...
[build-and-push : build-and-push] Login Succeeded
[build-and-push : build-and-push] WARNING! Your password will be stored unencrypted in /te kton/home/.docker/config.json.
[build-and-push : build-and-push] Configure a credential helper to remove this warning. Se e
[build-and-push : build-and-push] https://docs.docker.com/engine/reference/commandline/log in/#credentials-store
[build-and-push : build-and-push]
[build-and-push : build-and-push] Sending build context to Docker daemon 154.1kB
[build-and-push : build-and-push] Step 1/6 : FROM golang:1.14-alpine
[build-and-push : build-and-push] ---> 32dc91e030ac
[build-and-push : build-and-push] Step 2/6 : WORKDIR /go/src/app
[build-and-push : build-and-push] ---> Using cache
[build-and-push : build-and-push] ---> d55d4b17275b
[build-and-push : build-and-push] Step 3/6 : COPY . .
[build-and-push : build-and-push] ---> bf87460d41aa
[build-and-push : build-and-push] Step 4/6 : RUN go get -d -v ./...
[build-and-push : build-and-push] ---> Running in 332980136df7
[build-and-push : build-and-push] Removing intermediate container 332980136df7
[build-and-push : build-and-push] ---> 2f865b9f80d8
[build-and-push : build-and-push] Step 5/6 : RUN go install -v ./...
[build-and-push : build-and-push] ---> Running in c7ea52e6e794
[build-and-push : build-and-push] app
[build-and-push : build-and-push] Removing intermediate container c7ea52e6e794
[build-and-push : build-and-push] ---> 41d2311b69e0
[build-and-push : build-and-push] Step 6/6 : CMD [\"app\"]
[build-and-push : build-and-push] ---> Running in 0cd0817a4df7
[build-and-push : build-and-push] Removing intermediate container 0cd0817a4df7
[build-and-push : build-and-push] ---> 279dd5b728cd
[build-and-push : build-and-push] Successfully built 279dd5b728cd
[build-and-push : build-and-push] Successfully tagged harbor.k8s.local/course/tekton-demo: v0.1.0
[build-and-push : build-and-push] The push refers to repository [harbor.k8s.local/course/t ekton-demo]
[build-and-push : build-and-push] 6b6135795221: Preparing
[build-and-push : build-and-push] 1bd56846a2f1: Preparing
[build-and-push : build-and-push] 44f838d755fe: Preparing
[build-and-push : build-and-push] 8ba069651a4b: Preparing
[build-and-push : build-and-push] 9986a8f6a81b: Preparing
[build-and-push : build-and-push] 9f9d00b69565: Preparing
[build-and-push : build-and-push] f97a68b932e1: Preparing
[build-and-push : build-and-push] 6bdf11a0a4c7: Preparing
[build-and-push : build-and-push] 1119ff37d4a9: Preparing
[build-and-push : build-and-push] 9f9d00b69565: Waiting
[build-and-push : build-and-push] f97a68b932e1: Waiting
[build-and-push : build-and-push] 6bdf11a0a4c7: Waiting
[build-and-push : build-and-push] 1119ff37d4a9: Waiting
[build-and-push : build-and-push] 9986a8f6a81b: Layer already exists
[build-and-push : build-and-push] 8ba069651a4b: Layer already exists
[build-and-push : build-and-push] 9f9d00b69565: Layer already exists
[build-and-push : build-and-push] f97a68b932e1: Layer already exists
[build-and-push : build-and-push] 6bdf11a0a4c7: Layer already exists
[build-and-push : build-and-push] 1119ff37d4a9: Layer already exists
[build-and-push : build-and-push] 44f838d755fe: Pushed
[build-and-push : build-and-push] 1bd56846a2f1: Pushed
[build-and-push : build-and-push] 6b6135795221: Pushed
[build-and-push : build-and-push] v0.1.0: digest: sha256:98607e6705563dbabffc5881cdbf44160 0eb975e06f1625a746a7ca8ca8aee5b size: 2198
[build-and-push : image-digest-exporter-6lgnv] {\"level\":\"info\",\"ts\":1654411637.244523,\"log ger\":\"fallback-logger\",\"caller\":\"imagedigestexporter/main.go:59\",\"msg\":\"No index.json foun d for: harbor-image\",\"commit\":\"ab391e7\"}
在 get-build-id 任务中为我们生成了 v0.3.0-时间-随机数 这样的镜像 TAG,最后也通过 results 传递到了下面的构建任务中去,镜像的 TAG 也更新了
当然这些任务其实都具有一定的通用性的,为此 Tekton 官方提供了一个 Catalog 的服务,用来专门提供一些通用的任务,比如我们想要获取 Git Commit 的相关信息,可以使用 https://artifacthub.io/packages/tekton-task/tekton-catalog-tasks/git-clone 这个 Catalog,文档中也包含相关的使用说明。Tekton 官方也提供了一个 Tekton Hub网站来分享一些 Catalog 服务。
到这里我们就完成了使用 Tekton 创建 CI/CD 流水线的一个简单示例,不过这个示例还比较简单,接下来我们再通过一个稍微复杂点的应用来完成我们前面的 Jenkins 流水线。
在 Tekton 中还有一项功能就是 Sidecar,和 Pod 中的 Sidecar 类似,它也是一个容器,用于和 Task 任务的 Steps 中指定的容器一起运行,为这些 Steps 的执行提供一些辅助支持,比如 Sidecar 可以运行一个 logging daemon、更新共享 volume 上的文件或者提供网络代理等功能。
Tekton 会将 Sidecar 注入属于 TaskRun 的 Pod,一旦 Task 中的所有 Steps 完成执行,Pod 内运行的每一个 Sidecar 就会终止掉,如果 Sidecar 成功退出,kubectl get pods 命令会将 Pod 的状态返回为 Completed,如果 Sidecar 退出时出现了错误,则返回 Error,而忽略实际执行 Pod 内部 Steps 的容器镜像的退出码值。
上面我们在构建容器镜像的时候是通过挂载宿主机的 docker.sock 文件到容器中来执行的,严格意义上来说这种方式叫 Dood - Docker Outside of Docker,DooD 通过绑定安装 Docker 套接字来使用其底层宿主机的 Docker Daemon,而真正的 DinD 是在其中包含一个完整的 Docker 服务。显然 DooD 这种方式更快,因为可以利用它的缓存机制,而 DinD 显然更加安全、更加干净,对宿主机产生的影响更小,而且支持并行运行,因为每个容器里面都是一个独立的 Docker Daemon,互相不受影响,当然 DooD 更加简单易用。这里我们就来使用 Sidecar 的方式为 Tekton 中的容器构建提供一个 DinD 模式的构建服务。
新建一个如下所示的 Task 任务,专门用来构建 Docker 镜像
# task-docker-build.yaml
apiVersion: tekton.dev/v1beta1
kind: Task
metadata:
name: docker-build-push
spec:
resources:
inputs: # 定义输入资源
- name: source # 源代码仓库
type: git
params:
- name: image
description: Reference of the image docker will produce.
- name: builder_image
description: The location of the docker builder image.
default: docker:stable
- name: dockerfile
description: Path to the Dockerfile to build.
default: ./Dockerfile
- name: context
description: Path to the directory to use as context.
default: .
- name: build_extra_args
description: Extra parameters passed for the build command when building images.
default: \"\"
- name: push_extra_args
description: Extra parameters passed for the push command when pushing images.
default: \"\"
- name: insecure_registry
description: Allows the user to push to an insecure registry that has been specified
default: \"\"
- name: registry_mirror
description: Specific the docker registry mirror
default: \"\"
- name: registry_url
description: private docker images registry url
steps:
- name: docker-build # 构建步骤
image: $(params.builder_image)
env:
- name: DOCKER_HOST # 用 TLS 形式通过 TCP 链接 sidecar
value: tcp://localhost:2376
- name: DOCKER_TLS_VERIFY # 校验 TLS
value: \'1\'
- name: DOCKER_CERT_PATH # 使用 sidecar 守护进程生成的证书
value: /certs/client
workingDir: $(resources.inputs.source.path)
script: | # docker 构建命令
docker login $(params.registry_url)
docker build \\
$(params.build_extra_args) \\
--no-cache \\
-f $(params.dockerfile) -t $(params.image) $(params.context)
volumeMounts: # 声明挂载证书目录
- mountPath: /certs/client
name: dind-certs
- name: docker-push #
image: $(params.builder_image)
env:
- name: DOCKER_HOST
value: tcp://localhost:2376
- name: DOCKER_TLS_VERIFY
value: \'1\'
- name: DOCKER_CERT_PATH
value: /certs/client
workingDir: $(resources.inputs.source.path)
script: | # 推送 docker 镜像
docker push $(params.push_extra_args) $(params.image)
volumeMounts:
- mountPath: /certs/client
name: dind-certs
sidecars: # sidecar 模式,提供 docker daemon服务,实现真正的 DinD 模式
- image: docker:dind
name: server
args:
- --storage-driver=vfs
- --userland-proxy=false
- --debug
- --insecure-registry=$(params.insecure_registry)
- --registry-mirror=$(params.registry_mirror)
securityContext:
privileged: true
env:
- name: DOCKER_TLS_CERTDIR # 将生成的证书写入与客户端共享的路径
value: /certs
volumeMounts:
- mountPath: /certs/client
name: dind-certs
readinessProbe: # 等待 dind daemon 生成它与客户端共享的证书
periodSeconds: 1
exec:
command: [\'ls\', \'/certs/client/ca.pem\']
volumes: # 使用 emptyDir 的形式即可
- name: dind-certs
emptyDir: {}
上面的 Task 最重要的就是其中的 sidecars 部分,使用了一个 docker:dind 镜像来提供 docker 服务端,由于是 sidecar 模式,所以它和上面构建的 steps 中的容器是共享 network namespace 的,所以在构建的时候我们可以通过 tcp://localhost:2376 和 docker 服务端进行通信,由于还使用的是 TLS 证书模式,所以需要将证书目录进行声明挂载。
(这里容易出现readinessProbe错误,你可以设置事件宽松写或者注释掉探针)
(github.com有事登录很慢,拉取别人项目时容易出错,记得给自己的github账户加上当前的主机的ssh)
(流水线运行往往会因为访问github.com不成功,多运行几次,或者本地给github.com做个解析)
(这里容易有一个错误,就是怎么也连不上harbor.k8s.local,一般是build-and-push步骤,其实就是该容器解析不了该域名,我们可以在这个容器中写域名解析,或者对容器的dns服务器即coredns进行解析,下面这两种方法都演示一下)
(记得做好主机之间或者某些域名的解析)
kubectl edit cm coredns -n kube-system
data:
Corefile: |
.:53 {
errors
health {
lameduck 5s
}
hosts {
192.168.23.196 harbor.k8s.local
fallthrough
}
ready
kubernetes cluster.local in-addr.arpa ip6.arpa {
pods insecure
fallthrough in-addr.arpa ip6.arpa
ttl 30
主要就是加上hosts{}的内容,有时候可能需要重启下coredns的pod
或者修改对应的pod的,对应的build-and-push
是修改spec下的内容
hostAliases:
- ip: \"192.168.23.196\"
hostnames:
- \"harbor.k8s.local\"
containers:
- name: ...
接着重新修改我们的 Pipeline 流水线
# test-pipeline.yaml
apiVersion: tekton.dev/v1beta1
kind: Pipeline
metadata:
name: test-sidecar-pipeline
spec:
resources: # 为 Tasks 提供输入和输出资源声明
- name: demo-git
type: git
params:
- name: image
type: string
- name: image-tag
type: string
default: \"v0.4.0\"
- name: registry_url
type: string
default: \"harbor.k8s.local\"
- name: registry_mirror
type: string
default: \"https://ot2k4d59.mirror.aliyuncs.com/\"
- name: insecure_registry
type: string
default: \"harbor.k8s.local\"
tasks: # 添加task到流水线中
# 运行应用测试
- name: test
taskRef:
name: test
resources:
inputs:
- name: repo # Task 输入名称
resource: demo-git # Pipeline 资源名称
- name: get-build-id
taskRef:
name: generate-build-id
params:
- name: base-version
value: $(params.image-tag)
# 构建并推送 Docker 镜像
- name: build-and-push
taskRef:
name: docker-build-push # 使用上面定义的镜像构建任务
runAfter:
- test # 测试任务执行之后
resources:
inputs:
- name: source # 指定输入的git仓库资源
resource: demo-git
params:
- name: image
value: \"$(params.image):$(tasks.get-build-id.results.build-id)\"
- name: registry_url
value: $(params.registry_url)
- name: insecure_registry
value: $(params.insecure_registry)
- name: registry_mirror
value: $(params.registry_mirror)
这里的流水线最重要的就是将镜像构建的任务替换成上面的 docker-build-push 这个 Task,然后传入几个需要的参数,接着修改 PipelineRun
apiVersion: tekton.dev/v1beta1
kind: PipelineRun
metadata:
name: test-sidecar-pipelinerun
spec:
serviceAccountName: build-sa
pipelineRef:
name: test-sidecar-pipeline
resources:
- name: demo-git # 指定输入的git仓库资源
resourceRef:
name: demo-git
params:
- name: image
value: harbor.k8s.local/course/tekton-demo
这里就比较简单了,只需要引用上面的 Pipeline 流水线,然后提供需要的几个参数即可,直接创建上面的几个资源对象即可执行我们的流水线了
[root@master1 ~]# tkn pr list
NAME STARTED DURATION STATUS
test-sidecar-pipelinerun 34 seconds ago --- Running
test-pipelinerun 22 minutes ago 5 minutes Succeeded
tkn pr describe test-sidecar-pipelinerun
在 Harbor 中也可以看到我们刚刚推送的镜像版本**(其实生产环境待久了的工作人员会建议我们集成使用这些devops流程完成即可,最后的交付最好还是自己手动去搞)**
这种方式还可以避免在宿主机上产生大量无用的构建过程产生的镜像,因为每次构建完成就销毁掉了,这才是真正的 Docker IN Docker,也是 Tekton 中的 Sidecar 的一个使用场景。
在实际工作中,我们经常需要的一个功能是能够在任务之间共享制品,以便缓存构建工具(比如 Maven 和 NPM)的依赖项,在 Tekton 0.10 版本就发布增加了对 Workspaces 的支持,这使得流水线中的任务可以更加轻松地使用 PV 来共享数据了,Workspaces 允许指定一个或多个 pipeline 中 task 运行时需要的 volume。
Tekton Pipelines 中的 Workspaces 是指流水线运行时需要的共享卷的声明,在流水线定义中,Workspaces 可以作为共享卷传递给相关任务,这样当为多个任务提供相同的 Workspaces 的时候,它们就可以从相同的 Volumes 中读取和写入数据。当然 Workspaces 的 Volumes 卷除了可以是 PVC,也可以是 ConfigMap,或者是在任务之间挂载和共享的 Secret 资源。
接下来让我们看看在实践中如何使用 Workspaces 来缓存 Maven 依赖,加速流水线的构建,这里我们使用的项目为:https://github.com/cnych/spring-petclinic。
要在流水线中构建 Maven 项目,当然需要定义一个 Maven 的 Task 任务,其实在 Tekton Catalog 里面就已经包含了这样的通用的 Task 了,但是这里我们需要对其进行一些修改来为 Maven 的依赖项添加 Workspaces 支持。
# workspace-mvn-task.yaml
apiVersion: tekton.dev/v1beta1
kind: Task
metadata:
name: mvn-task
spec:
workspaces:
- name: maven-repo
resources:
inputs:
- name: source
type: git
params:
- name: GOALS
description: The Maven goals to run
type: array
default: [\"package\"]
steps:
- name: mvn
image: cnych/cloud-builders-mvn:tekton
workingDir: /workspace/source
command: [\"/usr/bin/mvn\"]
args:
- -Dmaven.repo.local=$(workspaces.maven-repo.path)
- \"$(inputs.params.GOALS)\"
上面的任务中我们新增了一个名为 maven-repo 的 Workspace,该工作区规定无论何时运行该任务,都应该提供并配置一个卷来充当本地的 Maven 存储库,然后将工作区的路径传递给 Maven 命令,以便通过 -Dmaven.repo.local=$(workspaces.maven-repo.path) 命令将工作区的路径作为本地的 Maven 库,当然也可以配置 Workspace 挂载的路径,这里我们使用的是默认的路径。
接着我们来定义一个使用 Maven 任务构建 Java 应用程序的流水线 Pipeline,为了演示 Maven 依赖的缓存效果,这里的流水线我们运行3个 Maven 任务来执行构建、集成测试,并生成测试结果和代码覆盖率等报告。
流水线定义
# workspace-mvn-pipeline.yaml
apiVersion: tekton.dev/v1beta1
kind: Pipeline
metadata:
name: mvn-pipeline
spec:
workspaces: # 声明 workspaces
- name: local-maven-repo
resources: # 声明使用的资源
- name: app-git
type: git
tasks:
- name: build # 构建任务
taskRef:
name: mvn-task # 引用上面的 mvn 任务
resources: # 传递 resources 资源
inputs:
- name: source
resource: app-git
params: # 传递 params 参数
- name: GOALS
value: [\"package\"]
workspaces: # 传递 workspaces
- name: maven-repo
workspace: local-maven-repo
- name: int-test # 测试任务
taskRef:
name: mvn-task
runAfter: [\"build\"] # 需要 build 任务执行完成后
resources:
inputs:
- name: source
resource: app-git
params:
- name: GOALS
value: [\"verify\"]
workspaces:
- name: maven-repo
workspace: local-maven-repo
- name: gen-report # 测试报告
taskRef:
name: mvn-task
runAfter: [\"build\"] # 需要 build 任务执行完成后
resources:
inputs:
- name: source
resource: app-git
params:
- name: GOALS
value: [\"site\"]
workspaces:
- name: maven-repo
workspace: local-maven-repo
需要注意流水线中的 local-maven-repo 工作区的声明,它指出,当此流水线运行时,应提供一个卷并将其用作此工作区,然后将此工作区提供给此流水线中的每个任务,以便它们都共享相同的工作区。然后我们根据传入的 GOALS 参数来决定应该执行的任务。
流水线 Pipeline 声明完成后,现在我们就可以运行这个流水线来构建 Spring PetClinic 这个示例应用了,在启动流水线之前,需要先创建一个 PVC 来提供一个 Workspace 对 Maven 依赖项进行缓存。
# workspace-mvn-pvc.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: mvn-repo-pvc
spec:
resources:
requests:
storage: 5Gi
volumeMode: Filesystem
storageClassName: nfs-storage # 使用 StorageClass 自动生成 PV
accessModes:
- ReadWriteOnce
如果要手动加pv:
apiVersion: v1
kind: PersistentVolume
metadata:
name: workspace
spec:
storageClassName: \"nfs-storage\"
capacity:
storage: 20Gi
accessModes:
- ReadWriteOnce
persistentVolumeReclaimPolicy: Retain
nfs:
path: /data/work # 指定nfs的挂载点
server: 192.168.23.213 # 指定nfs服务地址,这是我node1的主机ip
---
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: nfs-storage
provisioner: kubernetes.io/no-provisioner
这里我们使用了一个名为 nfs-storage 的 StorageClass,这样就可以自动生成一个对应的 PV 进行绑定,如果你没有需要自行创建一个对应的静态 PV。
现在我们就可以创建一个使用上述 PVC 作为流水线工作区的 PipelineRun 来执行流水线了
# workspace-mvn-pipelinerun.yaml
apiVersion: tekton.dev/v1beta1
kind: PipelineRun
metadata:
name: mvn-pipelinerun
spec:
pipelineRef:
name: mvn-pipeline
resources:
- name: app-git
resourceSpec:
type: git
params:
- name: url
value: https://github.com.cnpmjs.org/cnych/spring-petclinic
workspaces:
- name: local-maven-repo
persistentVolumeClaim:
claimName: mvn-repo-pvc
请注意 PVC 和为缓存 maven 依赖项而声明的工作区之间的映射,mvn-repo-pvc 被传递到流水线和相应的任务作为缓存文件和制品的共享卷。
第一次流水线运行将需要一些时间来下载依赖项执行任务,直接创建上面声明的几个资源对象,观察 PipelineRun 的执行过程:
kubectl apply -f workspace-mvn-task.yaml
kubectl apply -f workspace-mvn-pipeline.yaml
kubectl apply -f workspace-mvn-pvc.yaml
kubectl apply -f workspace-mvn-pipelinerun.yaml
当第一次执行流水线的时候会在执行 mvn 命令的时候消耗大量的时间,因为需要下载依赖包,我这里的环境差不多等了20分钟左右
然后在执行后面的两个任务的时候就非常快了,因为前面任务执行完成后会把依赖项存入到 Workspace 声明的 PVC 中去,后面的任务直接使用了这个 Workspace,我们可以重新执行一次 PipelineRun,对比下前后两次的时间,在我的环境中,执行时间由37分钟减少到大约两分钟。
tkn pr list
tkn pr describe mvn-pipelinerun
tkn pr describe mvn-pipelinerun-r-fgwf2
测试任务运行没有受到太大影响,因为它使用了大部分在构建任务运行中下载的依赖项,即使在第一次流水线运行中也是如此。
我们可以看到利用 Workspaces 功能可以对我们的流水线构建进行大幅度的优化,特别是对于依赖包特别大的应用,比如 Maven、NPM、Go Modules 等。
整个的devops的流程可以在tekton得web详细查看