Kubernetes原生CI/CD工具Tekton探秘與上手實踐

  • 2019 年 11 月 13 日
  • 筆記

如果有關注過Knative社區動態的同學,可能會知道最近發生了一件比較大的新聞,三大組件之一的build項目被人提了一個很殘忍的Proposal[1],並且專門在項目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的服務[2]。

其實Tekton的前身是Knative的build-pipeline項目,從名字可以看出這個項目是為了給build模塊增加pipeline的功能,但是大家發現隨着不同的功能加入到Knative build模塊中,build模塊越來越變得像一個通用的CI/CD系統,這已經脫離了Knative build設計的初衷,於是,索性將build-pipeline剝離出Knative,搖身一變成為Tekton,而Tekton也從此致力於提供全功能、標準化的原生Kubernetes CI/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之後,我們就可以開始動手實踐了,下面以構建一個Spring Boot工程為例。

假設我們新開發了一個名為ncs的Spring Boot項目,為了將該項目構建成鏡像並上傳至鏡像倉庫,我們可以梳理一個最簡單的CI流程如下:

  1. 從Git倉庫拉取代碼
  2. Maven編譯打包
  3. 構建鏡像
  4. 推送鏡像

當然在CI流程之前,我們先需要在項目中增加Dockerfile,否則構建鏡像無從談起。

添加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

從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要怎麼使用呢,我們接着往下看。

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

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鏡像[3],當然也可以採用一些其他的開源構建方式,例如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很容易入門和上手。

構建執行

在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://[email protected]/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://github.com/knative/build/issues/614
  2. https://cloud.google.com/Tekton/
  3. https://hub.docker.com/_/docker

原文鏈接:https://juejin.im/post/5d629c1a5188254628236b69

轉自:Docker公眾號