前面我们讲解了使用 Jenkins 流水线来实现 Kubernetes 应用的 CI/CD,现在我们来将这个流水线迁移到 Tekton 上面来,其实整体思路都是一样的,就是把要整个工作流划分成不同的任务来执行,前面工作流的阶段划分了以下几个阶段:Clone 代码 -> 单元测试 -> Golang 编译打包 -> Docker 镜像构建/推送 -> Kubectl 部署服务。
在 Tekton 中我们就可以将这些阶段直接转换成 Task 任务,Clone 代码在 Tekton 中不需要我们主动定义一个任务,只需要在执行的任务上面指定一个输入的代码资源即可。下面我们就来将上面的工作流一步一步来转换成 Tekton 流水线,代码仓库同样还是 http://git.k8s.local/course/devops-demo.git。
Clone 代码
虽然我们可以不用单独定义一个 Clone 代码的任务,直接使用 git 类型的输入资源即可,由于这里涉及到的任务较多,而且很多时候都需要先 Clone 代码然后再进行操作,所以最好的方式是将代码 Clone 下来过后通过 Workspace 共享给其他任务,这里我们可以直接使用 Catalog git-clone 来实现这个任务,我们可以根据自己的需求做一些定制,对应的 Task 如下所示:
#task-clone.yamlapiVersion:tekton.dev/v1beta1kind:Taskmetadata:name:git-clonespec:workspaces:-name:outputdescription:ThegitrepowillbeclonedontothevolumebackingthisWorkspace.-name:basic-authoptional:truedescription:|AWorkspacecontaininga.gitconfigand.git-credentialsfile.Thesewillbecopiedtotheuser\’shomebeforeanygitcommandsarerun.AnyotherfilesinthisWorkspaceareignored.Itisstronglyrecommendedtousessh-directoryoverbasic-authwheneverpossibleandtobindaSecrettothisWorkspaceoverothervolumetypes.params:-name:urldescription:RepositoryURLtoclonefrom.type:string-name:revisiondescription:Revisiontocheckout.(branch,tag,sha,ref,etc…)type:stringdefault:\”\”-name:refspecdescription:Refspectofetchbeforecheckingoutrevision.default:\”\”-name:submodulesdescription:Initializeandfetchgitsubmodules.type:stringdefault:\”true\”-name:depthdescription:Performashallowclone,fetchingonlythemostrecentNcommits.type:stringdefault:\”1\”-name:sslVerifydescription:Setthe`http.sslVerify`globalgitconfig.Settingthisto`false`isnotadvisedunlessyouaresurethatyoutrustyourgitremote.type:stringdefault:\”true\”-name:subdirectorydescription:Subdirectoryinsidethe`output`Workspacetoclonetherepointo.type:stringdefault:\”\”-name:sparseCheckoutDirectoriesdescription:Definethedirectorypatternstomatchorexcludewhenperformingasparsecheckout.type:stringdefault:\”\”-name:deleteExistingdescription:Cleanoutthecontentsofthedestinationdirectoryifitalreadyexistsbeforecloning.type:stringdefault:\”true\”-name:verbosedescription:Logthecommandsthatareexecutedduring`git-clone`\’soperation.type:stringdefault:\”true\”-name:gitInitImagedescription:Theimageprovidingthegit-initbinarythatthisTaskruns.type:stringdefault:\”cnych/tekton-git-init:v0.24.1\”-name:userHomedescription:|Absolutepathtotheuser\’shomedirectory.Setthisexplicitlyifyouarerunningtheimageasanon-rootuserorhaveoverriddenthegitInitImageparamwithanimagecontainingcustomuserconfiguration.type:stringdefault:\”/root\”results:-name:commitdescription:TheprecisecommitSHAthatwasfetchedbythisTask.-name:urldescription:ThepreciseURLthatwasfetchedbythisTask.steps:-name:cloneimage:\”$(params.gitInitImage)\”env:-name:HOMEvalue:\”$(params.userHome)\”-name:PARAM_URLvalue:$(params.url)-name:PARAM_REVISIONvalue:$(params.revision)-name:PARAM_REFSPECvalue:$(params.refspec)-name:PARAM_SUBMODULESvalue:$(params.submodules)-name:PARAM_DEPTHvalue:$(params.depth)-name:PARAM_SSL_VERIFYvalue:$(params.sslVerify)-name:PARAM_SUBDIRECTORYvalue:$(params.subdirectory)-name:PARAM_DELETE_EXISTINGvalue:$(params.deleteExisting)-name:PARAM_VERBOSEvalue:$(params.verbose)-name:PARAM_SPARSE_CHECKOUT_DIRECTORIESvalue:$(params.sparseCheckoutDirectories)-name:PARAM_USER_HOMEvalue:$(params.userHome)-name:WORKSPACE_OUTPUT_PATHvalue:$(workspaces.output.path)-name:WORKSPACE_BASIC_AUTH_DIRECTORY_BOUNDvalue:$(workspaces.basic-auth.bound)-name:WORKSPACE_BASIC_AUTH_DIRECTORY_PATHvalue:$(workspaces.basic-auth.path)script:|#!/usr/bin/envshset-euif[\”${PARAM_VERBOSE}\”=\”true\”];thenset-xfiif[\”${WORKSPACE_BASIC_AUTH_DIRECTORY_BOUND}\”=\”true\”];thencp\”${WORKSPACE_BASIC_AUTH_DIRECTORY_PATH}/.git-credentials\”\”${PARAM_USER_HOME}/.git-credentials\”cp\”${WORKSPACE_BASIC_AUTH_DIRECTORY_PATH}/.gitconfig\”\”${PARAM_USER_HOME}/.gitconfig\”chmod400\”${PARAM_USER_HOME}/.git-credentials\”chmod400\”${PARAM_USER_HOME}/.gitconfig\”fiCHECKOUT_DIR=\”${WORKSPACE_OUTPUT_PATH}/${PARAM_SUBDIRECTORY}\”cleandir(){#Deleteanyexistingcontentsoftherepodirectoryifitexists.##Wedon\’tjust\”rm-rf${CHECKOUT_DIR}\”because${CHECKOUT_DIR}mightbe\”/\”#ortherootofamountedvolume.if[-d\”${CHECKOUT_DIR}\”];then#Deletenon-hiddenfilesanddirectoriesrm-rf\”${CHECKOUT_DIR:?}\”/*#Deletefilesanddirectoriesstartingwith.butexcluding..rm-rf\”${CHECKOUT_DIR}\”/.[!.]*#Deletefilesanddirectoriesstartingwith..plusanyothercharacterrm-rf\”${CHECKOUT_DIR}\”/..?*fi}if[\”${PARAM_DELETE_EXISTING}\”=\”true\”];thencleandirfi/ko-app/git-init-url=\”${PARAM_URL}\”-revision=\”${PARAM_REVISION}\”-refspec=\”${PARAM_REFSPEC}\”-path=\”${CHECKOUT_DIR}\”-sslVerify=\”${PARAM_SSL_VERIFY}\”-submodules=\”${PARAM_SUBMODULES}\”-depth=\”${PARAM_DEPTH}\”-sparseCheckoutDirectories=\”${PARAM_SPARSE_CHECKOUT_DIRECTORIES}\”cd\”${CHECKOUT_DIR}\”RESULT_SHA=\”$(gitrev-parseHEAD)\”EXIT_CODE=\”$?\”if[\”${EXIT_CODE}\”!=0];thenexit\”${EXIT_CODE}\”fiprintf\”%s\”\”${RESULT_SHA}\”>\”$(results.commit.path)\”printf\”%s\”\”${PARAM_URL}\”>\”$(results.url.path)\”
一般来说我们只需要提供 output 这个个用于持久化代码的 workspace,然后还包括 url 和 revision 这两个参数,其他使用默认的即可。
单元测试
单元测试阶段比较简单,正常来说也是只是单纯执行一个测试命令即可,我们这里没有真正执行单元测试,所以简单测试下即可,编写一个如下所示的 Task:
#task-test.yamlapiVersion:tekton.dev/v1beta1kind:Taskmetadata:name:testspec:steps:-name:testimage:golang:1.14-alpinecommand:[\’echo\’]args:[\’thisisatesttask\’] 编译打包
然后第二个阶段是编译打包阶段,因为我们这个项目的 Dockerfile 不是使用的多阶段构建,所以需要先用一个任务去将应用编译打包成二进制文件,然后将这个编译过后的文件传递到下一个任务进行镜像构建。
我们已经明确了这个阶段要做的事情,编写任务也就简单了,创建如下所的 Task 任务,首先需要通过定义一个 workspace 把 clone 任务里面的代码关联过来:
#task-build.yamlapiVersion:tekton.dev/v1beta1kind:Taskmetadata:name:buildspec:workspaces:-name:go-repomountPath:/workspace/reposteps:-name:buildimage:golang:1.14-alpineworkingDir:/workspace/reposcript:|gobuild-v-oappenv:-name:GOPROXYvalue:https://goproxy.cn-name:GOOSvalue:linux-name:GOARCHvalue:amd64
这个构建任务也很简单,只是我们将需要用到的环境变量直接通过 env 注入了,当然直接写入到 script 中也是可以的,或者直接使用 command 来执行任务都可以,然后构建生成的 app 这个二进制文件保留在代码根目录,这样也就可以通过 workspace 进行共享了。
Docker 镜像
接下来就是构建并推送 Docker 镜像了,前面我们介绍过使用 Kaniko、DooD、DinD 3种模式的镜像构建方式,这里我们直接使用 DinD 这种模式,我们这里要构建的镜像 Dockerfile 非常简单:
FROMalpineWORKDIR/home#修改alpine源为阿里云RUNsed-i\’s/dl-cdn.alpinelinux.org/mirrors.ustc.edu.cn/g\’/etc/apk/repositories&&apkupdate&&apkupgrade&&apkaddca-certificates&&update-ca-certificates&&apkadd–updatetzdata&&rm-rf/var/cache/apk/*COPYapp/home/ENVTZ=Asia/ShanghaiEXPOSE8080ENTRYPOINT./app
就行直接将编译好的二进制文件拷贝到镜像中即可,所以我们这里同样需要通过 Workspace 去获取上一个构建任务的制品,当然要使用 DinD 模式构建镜像,需要用到 sidecar 功能,创建一个如下所示的任务:
#task-docker.yamlapiVersion:tekton.dev/v1beta1kind:Taskmetadata:name:dockerspec:workspaces:-name:go-repoparams:-name:imagedescription:Referenceoftheimagedockerwillproduce.-name:registry_mirrordescription:Specificthedockerregistrymirrordefault:\”\”-name:registry_urldescription:privatedockerimagesregistryurlsteps:-name:docker-build#构建步骤image:docker:stableenv:-name:DOCKER_HOST#用TLS形式通过TCP链接sidecarvalue:tcp://localhost:2376-name:DOCKER_TLS_VERIFY#校验TLSvalue:\”1\”-name:DOCKER_CERT_PATH#使用sidecar守护进程生成的证书value:/certs/client-name:DOCKER_PASSWORDvalueFrom:secretKeyRef:name:harbor-authkey:password-name:DOCKER_USERNAMEvalueFrom:secretKeyRef:name:harbor-authkey:usernameworkingDir:$(workspaces.go-repo.path)script:|#docker构建命令dockerlogin$(params.registry_url)-u$DOCKER_USERNAME-p$DOCKER_PASSWORDdockerbuild–no-cache-f./Dockerfile-t$(params.image).dockerpush$(params.image)volumeMounts:#声明挂载证书目录-mountPath:/certs/clientname:dind-certssidecars:#sidecar模式,提供dockerdaemon服务,实现真正的DinD模式-image:docker:dindname:serverargs:—storage-driver=vfs—userland-proxy=false—debug—insecure-registry=$(params.registry_url)—registry-mirror=$(params.registry_mirror)securityContext:privileged:trueenv:-name:DOCKER_TLS_CERTDIR#将生成的证书写入与客户端共享的路径value:/certsvolumeMounts:-mountPath:/certs/clientname:dind-certsreadinessProbe:#等待dinddaemon生成它与客户端共享的证书periodSeconds:1exec:command:[\”ls\”,\”/certs/client/ca.pem\”]volumes:#使用emptyDir的形式即可-name:dind-certsemptyDir:{}
这个任务的重点还是要去声明一个 Workspace,当执行任务的时候要使用和前面构建任务同一个 Workspace,这样就可以获得上面编译成的 app 这个二进制文件了。
部署
接下来的部署阶段,我们同样可以参考之前 Jenkins 流水线里面的实现,由于项目中我们包含了 Helm Chart 包,所以直接使用 Helm 来部署即可,要实现 Helm 部署,当然我们首先需要一个包含 helm 命令的镜像,当然完全可以自己去编写一个这样的任务,此外我们还可以直接去 hub.tekton.dev 上面查找 Catalog,因为这上面就有很多比较通用的一些任务了,比如 helm-upgrade-from-source 这个 Task 任务就完全可以满足我们的需求了:
helm tekton
这个 Catalog 下面也包含完整的使用文档了,我们可以将该任务直接下载下来根据我们自己的需求做一些定制修改,如下所示:
#task-deploy.yamlapiVersion:tekton.dev/v1beta1kind:Taskmetadata:name:deployspec:params:-name:charts_dirdescription:Thedirectoryinsourcethatcontainsthehelmchart-name:release_namedescription:Thehelmreleasename-name:release_namespacedescription:Thehelmreleasenamespacedefault:\”\”-name:overwrite_valuesdescription:\”Specifythevaluesyouwanttooverwrite,commaseparated:autoscaling.enabled=true,replicas=1\”default:\”\”-name:values_filedescription:\”Thevaluesfiletobeused\”default:\”values.yaml\”-name:helm_imagedescription:\”helmimagetobeused\”default:\”docker.io/lachlanevenson/k8s-helm:v3.3.4@sha256:e1816be207efbd342cba9d3d32202e237e3de20af350617f8507dc033ea66803\”#tag:v3.3.4workspaces:-name:sourceresults:-name:helm-statusdescription:Helmdeploystatussteps:-name:upgradeimage:$(params.helm_image)workingDir:/workspace/sourcescript:|echocurrentinstalledhelmreleaseshelmlist–namespace\”$(params.release_namespace)\”echoinstallinghelmchart…helmupgrade–install–wait–values\”$(params.charts_dir)/$(params.values_file)\”–create-namespace–namespace\”$(params.release_namespace)\”$(params.release_name)$(params.charts_dir)–debug–set\”$(params.overwrite_values)\”status=`helmstatus$(params.release_name)–namespace\”$(params.release_namespace)\”|awk\’/STATUS/{print$2}\’`echo${status}|tr-d\” \”|tee$(results.helm-status.path)
因为我们的 Helm Chart 模板就在代码仓库中,所以不需要从 Chart Repo 仓库中获取,只需要指定 Chart 路径即可,其他可配置的参数都通过 params 参数暴露出去了,非常灵活,最后我们还获取了 Helm 部署的状态,写入到了 Results 中,方便后续任务处理。
回滚
最后应用部署完成后可能还需要回滚,因为可能部署的应用有错误,当然这个回滚动作最好是我们自己去触发,但是在某些场景下,比如 helm 部署已经明确失败了,那么我们当然可以自动回滚了,所以就需要判断当部署失败的时候再执行回滚,也就是这个任务并不是一定会发生的,只在某些场景下才会出现,我们可以在流水线中通过使用 WhenExpressions 来实现这个功能,之前版本中是使用 Conditions,不过已经废弃了。要只在满足某些条件时运行任务,可以使用 when 字段来保护任务执行,when 字段允许你列出对 WhenExpressions 的一系列引用。
WhenExpressions 由 Input、Operator 和 Values 几部分组成:
Input 是 WhenExpressions 的输入,它可以是一个静态的输入或变量(Params 或 Results),如果未提供输入,则默认为空字符串 Operator 是一个运算符,表示 Input 和 Values 之间的关系,有效的运算符包括 in、notin Values 是一个字符串数组,必须提供一个非空的 Values 数组,它同样可以包含静态值或者变量(Params、Results 或者 Workspaces 绑定)
当在一个 Task 任务中配置了 WhenExpressions,在执行 Task 之前会评估声明的 WhenExpressions,如果结果为 True,则执行任务,如果为 False,则不会执行该任务。
我们这里创建的回滚任务如下所示:
#task-rollback.yamlapiVersion:tekton.dev/v1beta1kind:Taskmetadata:name:rollbackspec:params:-name:release_namedescription:Thehelmreleasename-name:release_namespacedescription:Thehelmreleasenamespacedefault:\”\”-name:helm_imagedescription:\”helmimagetobeused\”default:\”docker.io/lachlanevenson/k8s-helm:v3.3.4@sha256:e1816be207efbd342cba9d3d32202e237e3de20af350617f8507dc033ea66803\”#tag:v3.3.4steps:-name:rollbackimage:$(params.helm_image)script:|echorollbackcurrentinstalledhelmreleaseshelmrollback$(params.release_name)–namespace$(params.release_namespace) 流水线
现在我们的整个工作流任务都已经创建完成了,接下来我们就可以将这些任务全部串联起来组成一个 Pipeline 流水线了,将上面定义的几个 Task 引用到 Pipeline 中来,当然还需要声明 Task 中用到的 resources 或者 workspaces 这些数据:
#pipeline.yamlapiVersion:tekton.dev/v1beta1kind:Pipelinemetadata:name:pipelinespec:workspaces:#声明workspaces-name:go-repo-pvcparams:#定义代码仓库-name:git_url-name:revisiontype:stringdefault:\”master\”#定义镜像参数-name:image-name:registry_urltype:stringdefault:\”harbor.k8s.local\”-name:registry_mirrortype:stringdefault:\”https://ot2k4d59.mirror.aliyuncs.com/\”#定义helmcharts参数-name:charts_dir-name:release_name-name:release_namespacedefault:\”default\”-name:overwrite_valuesdefault:\”\”-name:values_filedefault:\”values.yaml\”tasks:#添加task到流水线中-name:clonetaskRef:name:git-cloneworkspaces:-name:outputworkspace:go-repo-pvcparams:-name:urlvalue:$(params.git_url)-name:revisionvalue:$(params.revision)-name:testtaskRef:name:test-name:build#编译二进制程序taskRef:name:buildrunAfter:#测试任务执行之后才执行buildtask-test-cloneworkspaces:#传递workspaces-name:go-repoworkspace:go-repo-pvc-name:docker#构建并推送Docker镜像taskRef:name:dockerrunAfter:-buildworkspaces:#传递workspaces-name:go-repoworkspace:go-repo-pvcparams:#传递参数-name:imagevalue:$(params.image)-name:registry_urlvalue:$(params.registry_url)-name:registry_mirrorvalue:$(params.registry_mirror)-name:deploy#部署应用taskRef:name:deployrunAfter:-dockerworkspaces:-name:sourceworkspace:go-repo-pvcparams:-name:charts_dirvalue:$(params.charts_dir)-name:release_namevalue:$(params.release_name)-name:release_namespacevalue:$(params.release_namespace)-name:overwrite_valuesvalue:$(params.overwrite_values)-name:values_filevalue:$(params.values_file)-name:rollback#回滚taskRef:name:rollbackwhen:-input:\”$(tasks.deploy.results.helm-status)\”operator:invalues:[\”failed\”]params:-name:release_namevalue:$(params.release_name)-name:release_namespacevalue:$(params.release_namespace)
整体流程比较简单,就是在 Pipeline 需要先声明使用到的 Workspace、Resource、Params 这些资源,然后将声明的数据传递到 Task 任务中去,需要注意的是最后一个回滚任务,我们需要根据前面的 deploy 任务的结果来判断是否需要执行该任务,所以这里我们使用了 when 属性,通过 $(tasks.deploy.results.helm-status) 获取部署状态。
执行流水线
现在我们就可以来执行下我们的流水线,看是否符合我们自身的要求,首先我们需要先创建关联的其他资源对象,比如 Workspace 对应的 PVC、还有 GitLab、Harbor 的认证信息:
#other.yamlapiVersion:v1kind:Secretmetadata:name:gitlab-authannotations:tekton.dev/git-0:http://git.k8s.localtype:kubernetes.io/basic-authstringData:username:rootpassword:admin321—apiVersion:v1kind:Secretmetadata:name:harbor-authannotations:tekton.dev/docker-0:http://harbor.k8s.localtype:kubernetes.io/basic-authstringData:username:adminpassword:Harbor12345—apiVersion:v1kind:ServiceAccountmetadata:name:tekton-build-sasecrets:-name:harbor-auth-name:gitlab-auth—apiVersion:rbac.authorization.k8s.io/v1kind:ClusterRoleBindingmetadata:name:tekton-clusterrole-bindingroleRef:apiGroup:rbac.authorization.k8s.iokind:ClusterRolename:editsubjects:-kind:ServiceAccountname:tekton-build-sanamespace:default—apiVersion:v1kind:PersistentVolumeClaimmetadata:name:go-repo-pvcspec:resources:requests:storage:1GivolumeMode:FilesystemstorageClassName:nfs-storage#使用StorageClass自动生成PVaccessModes:-ReadWriteOnce
这些关联的资源对象创建完成后,还需要为上面的 ServiceAccount 绑定一个权限,因为在 Helm 容器中我们要去操作一些集群资源,必然需要先做权限声明,这里我们可以将 tekton-build-sa 绑定到 edit 这个 ClusterRole 上去。
我们接下来就可以创建一个 PipelineRun 资源对象来触发我们的流水线构建了:
#pipelinerun.yamlapiVersion:tekton.dev/v1beta1kind:PipelineRunmetadata:name:pipelinerunspec:serviceAccountName:tekton-build-sapipelineRef:name:pipelineworkspaces:-name:go-repo-pvcpersistentVolumeClaim:claimName:go-repo-pvcparams:-name:git_urlvalue:http://git.k8s.local/course/devops-demo.git-name:imagevalue:\”harbor.k8s.local/course/devops-demo:v0.1.0\”-name:charts_dirvalue:\”./helm\”-name:release_namevalue:devops-demo-name:release_namespacevalue:\”kube-ops\”-name:overwrite_valuesvalue:\”image.repository=harbor.k8s.local/course/devops-demo,image.tag=v0.1.0\”-name:values_filevalue:\”my-values.yaml\”
直接创建上面的资源对象就可以执行我们的 Pipeline 流水线了:
$kubectlapply-fpipelinerun.yaml$tknprdescribepipelinerunName:pipelinerunNamespace:defaultPipelineRef:pipelineServiceAccount:tekton-build-saTimeout:1h0m0sLabels:tekton.dev/pipeline=pipeline