1. 程式人生 > 其它 >雲原生Jenkins實踐

雲原生Jenkins實踐

本文總結了我在工作中在雲原生環境下對jenkins的使用,主要介紹了jenkins和kubernetes、docker、helm的結合,並使用一個angular專案和maven專案作為案例,分別展示了前後端的構建流程。

由於整理倉促,涉及到的內容時間跨度較長,難免有些細節不夠嚴謹,僅供參考。

部署Jenkins

按照順序依次建立以下k8s資源,在叢集中快速部署一套比較簡單的jenkins環境。

名稱空間

apiVersion: v1
kind: Namespace
metadata:
  name: jenkins

service account

apiVersion: v1
kind: ServiceAccount
metadata:
  name: jenkins
  namespace: jenkins
---
kind: ClusterRole
apiVersion: rbac.authorization.k8s.io/v1beta1
metadata:
  name: jenkins
rules:
  - apiGroups: ["extensions", "apps"]
    resources: ["deployments"]
    verbs: ["create", "delete", "get", "list", "watch", "patch", "update"]
  - apiGroups: [""]
    resources: ["services"]
    verbs: ["create", "delete", "get", "list", "watch", "patch", "update"]
  - apiGroups: [""]
    resources: ["pods"]
    verbs: ["create", "delete", "get", "list", "patch", "update", "watch"]
  - apiGroups: [""]
    resources: ["pods/exec"]
    verbs: ["create", "delete", "get", "list", "patch", "update", "watch"]
  - apiGroups: [""]
    resources: ["pods/log"]
    verbs: ["get", "list", "watch"]
  - apiGroups: [""]
    resources: ["secrets"]
    verbs: ["get"]
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRoleBinding
metadata:
  name: jenkins
  namespace: jenkins
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: jenkins
subjects:
  - kind: ServiceAccount
    name: jenkins
    namespace: jenkins

service

apiVersion: v1
kind: Service
metadata:
  name: jenkins
  namespace: jenkins
  labels:
    app: jenkins
spec:
  selector:
    app: jenkins
  type: NodePort
  ports:
    - name: web
      port: 8080
      targetPort: web
      # nodeport方式暴露訪問地址
      nodePort: 30020
    - name: agent
      port: 50000
      targetPort: agent

pvc

kind: PersistentVolumeClaim
apiVersion: v1
metadata:
  name: jenkins
  namespace: jenkins
spec:
  # 使用cephfs作為pvc的動態提供者
  storageClassName: cephfs
  accessModes:
    - ReadWriteMany
  resources:
    requests:
      storage: 5G

deployment

apiVersion: apps/v1
kind: Deployment
metadata:
  name: jenkins
  namespace: jenkins
spec:
  selector:
    matchLabels:
      app: jenkins
  template:
    metadata:
      labels:
        app: jenkins
    spec:
      terminationGracePeriodSeconds: 10
      serviceAccount: jenkins
      # 用initContainer修復pvc資料目錄的許可權,確保容器程序可寫
      initContainers:
        - name: permission-fixer
          image: alpine
          command: ["sh", "-c", "chown -R 1000 /cephfs"]
          volumeMounts:
            - name: jenkinshome
              mountPath: /cephfs
      containers:
        - name: jenkins
          image: jenkins/jenkins:lts
          imagePullPolicy: IfNotPresent
          ports:
            - containerPort: 8080
              name: web
              protocol: TCP
            - containerPort: 50000
              name: agent
              protocol: TCP
          resources:
            limits:
              cpu: 1000m
              memory: 1Gi
            requests:
              cpu: 500m
              memory: 512Mi
          livenessProbe:
            httpGet:
              path: /login
              port: 8080
            initialDelaySeconds: 60
            timeoutSeconds: 5
            failureThreshold: 12
          readinessProbe:
            httpGet:
              path: /login
              port: 8080
            initialDelaySeconds: 60
            timeoutSeconds: 5
            failureThreshold: 12
          volumeMounts:
            - name: jenkinshome
              subPath: jenkins2
              mountPath: /var/jenkins_home
      volumes:
        - name: jenkinshome
          persistentVolumeClaim:
            claimName: jenkins

確認jenkins pod狀態為running,且探針正常後,訪問node節點上的http://node-ip:30020,進入jenkins web控制檯。

初始化Jenkins

從pod 標準輸出中找到解鎖碼,輸入。

選擇預設外掛配置即可,開始外掛安裝過程。

最後設定賬號密碼資訊,完成基本配置登入jenkins控制檯。

換源

如果叢集所在內網沒有配置路由器實現自由訪問某些網路資源,可以考慮換源以加速外掛下載速度

  1. 根據jenkins deployment持久化的情況,用shell進入pvc中的jenkins配置目錄中,修改hudson.model.UpdateCenter.xml

    <?xml version='1.1' encoding='UTF-8'?>
    <sites>
      <site>
        <id>default</id>
        <url>http://mirrors.tuna.tsinghua.edu.cn/jenkins/updates/update-center.json</url>
      </site>
    </sites>
    
  2. 還需要替換兩處

    sed -i 's/https:\/\/updates.jenkins.io\/download/http:\/\/mirrors.tuna.tsinghua.edu.cn\/jenkins/g ./updates/default.json
    sed -i 's/http:\/\/www.google.com/https:\/\/www.baidu.com/g' ./updates/default.json
    

整合k8s

  • 目標:實現jenkins呼叫k8s介面,操作docker、k8s,在動態生成的pod中完成ci、cd流程,最後銷燬動態生成的pod。
  1. 在jenkins中安裝k8s外掛。

  2. 選擇可選外掛,並輸入kubernetes搜尋,找到如下圖叫做kubernetes的外掛(安裝後在已安裝外掛列表里名字是kubernetes plugin,可以用簡介和版本號進行區分)。

  3. 等待安裝完成。

  4. 增加slave的叢集資訊:系統配置拉到頁面尾部,在cloud專案下點超連結進入新頁面配置叢集資訊。新增cloud,選擇kubernetes。

  5. 配置jenkins構建用的slave pod所在k8s叢集的連線方式:kubernetes地址為叢集自帶的default名稱空間的kubernetes service,直接按圖填即可。最後測試連線,確保連線成功。

  6. 根據jenkins部署的名稱空間和service名字填寫jenkins地址。

  7. 增加一個pod template,標籤列表需要記住,以後的ci/cd需要增加該標籤才會呼叫該叢集進行構建。其他按圖填即可,該容器映象是預配置好的映象,包含了docker和kubectl客戶端程式。

  8. 增加host path型別的卷,分別為host上的docker socket和kubeconfig

  9. 填入部署時的sa,使模板pod有許可權操作相應的資源

驗證動態pod構建

  1. jenkins左側新建item,選擇free style,注意標籤寫入上一步裡的標籤,進行匹配,方能觸發動態構建。

  2. 增加實際構建時的操作,此處執行shell命令,證明動態構建的pod可以操作docker和k8s。在構建中選擇執行shell,輸入以下shell命令

    echo "測試 Kubernetes 動態生成 jenkins slave"
    echo "==============docker in docker==========="
    docker info
    
    echo "=============kubectl============="
    kubectl get pods
    
  3. 儲存後,進入該構建的主頁面,點選左側的立即構建,手動觸發一次構建。此時觀察pod的動態,可以看到新的pod被自動建立、執行、最終被銷燬。

  4. build完成後,左側build history中會出現可點選的連結,左側點選控制檯輸出可以看到本次構建的標準輸出,可以看到動態構建pod成功運行了docker info獲取了host上的docker資訊,成功用kubectl獲取了叢集pod資訊。

至此完成了jenkins和k8s整合的初步配置。接下來可以開始針對具體的工作任務進行詳細的應用和進一步的優化配置。

實際應用

內網及代理配置

  1. 私有服務儘量使用ip,可避免叢集中域名解析的困擾
  2. 私有服務如果需要用域名解析,可以通過hostAlias修改jenkins pod及jenkins slave pod的hosts
  3. 如果配置了上述的內部域名,同時配置了訪問公網的代理,記得再no_proxy里加入內部域名

外掛

  1. Config File Provider:管理配置檔案,並在jenkinsfile中引用,實際構建時會對映到工程目錄裡
  2. Pipeline Utility Steps:讀取yaml檔案,在jenkinsfile中引用。使用該外掛可以方便地整合helm,實現以helm chart的values.yaml為唯一的配置檔案

構建專案配置

本文構建專案以流水線型別的專案為主,並可以建立資料夾,進行分類管理。

在流水線型別的專案中,主要需要關注的幾個點如下:

  1. 原始碼倉庫相關:先在系統管理-安全-credentials裡儲存git使用者名稱和密碼

  2. 構建引數:勾選引數化構建,可以設定構建時的輸入引數。例如:

    1. 分支:在程式碼倉庫配置中使用*/${branch}引用該引數

        					<img src="https://cdn.wubw.fun/typora/211201-101428-image-20211201101428440.png" alt="image-20211201101428440" style="zoom:50%;" />
      
      1. 名稱空間:同1,可以設定namespace引數,使使用者可以指定部署到哪個名稱空間

jenkinsfile編寫

經過眾多配置,終於來到了核心的jenkinsfile的編寫。本文展示我對jenkinsfile應用的幾個案例,以供學習參考。

由於slave pod執行在k8s中,所以通過配置不同的pod template,可以實現對各種各樣構建任務的支援。

在本文的整合k8s部分,配置了一個基礎的pod template,基於該pod template生成的動態pod使用了cnych/jenkins:jnlp6映象,其中包含了kubect和docker客戶端,因此在jenkinsfile中就可以使用kubectl和docker完成構建任務。

接下來分別展示前後端構建使用的pod template和jenkinsfile,注意二者需要配合使用。

前端

技術棧:angular->docker image->k8s helm release

pod template:

上圖中幾個關鍵點解釋:

  1. 配置了雙容器,一個容器使用了node:alpine映象,用於angular的編譯;另一個容器使用了我自制的映象ccr.ccs.tencentyun.com/lwabish/cloud-native-clients,其中包含了kubect、docker、helm三個客戶端,使用者將編譯好的程式碼打包、部署到叢集
  2. Pod retention配置為on failure,在構建出問題時保留slave pod方便檢視日誌
  3. 如果需要對slave pod的yaml檔案進行一些額外的配置,而表單裡沒有,那麼可以在raw yaml for the pod中填寫,並選擇merge。注意yaml層級要寫全,不是隻寫需要的部分
  4. 同樣使用hostPath volume將node上的docker socket和kubeconfig對映給slave pod直接使用

jenkinsfile:

pipeline{
    agent{
      	// 這裡對應pod template中的標籤列表
        label "node"
    }
		
  	// 整合helm chart,從chart的values.yaml以及構建引數獲取輸入,結合一些簡單的邏輯確定構建任務的各項環境引數
    environment {
        // 從chart的values.yaml中讀入所有配置
        config=readYaml (file: 'chart/values.yaml')
      	
      	// 構建引數裡的namespace override chart裡的namespace
        namespace="${namespace=='chart'?config.global.namespace:namespace}"
      
      	// harbor倉庫直接在chart裡寫死,通常不變
        harbor="${config.global.registry}"
        
      	// 構建映象的tag,和jenkins全域性變數繫結,方便排查問題
      	tag="${branch}-${BUILD_NUMBER}"
      	
      	// 完整的image tag
        image="${harbor}/${JOB_BASE_NAME}:${tag}"

    }

    stages{
        stage('顯示環境變數'){
            steps{
              	// 具體的step要指定pod中的容器名稱
                container('node'){
                    sh """
                            echo ${namespace}
                            echo ${harbor}
                            echo ${tag}
                            echo ${image}
                    """
                }
            }
        }
        stage('獲取依賴'){
            steps{
                container('node'){
                    sh """
                        // yarn 公網代理
                        yarn config set proxy http://x.x.x.x:3128
                        yarn config set https-proxy http://x.x.x.x:3128
                        yarn config set registry https://registry.npm.taobao.org/
                        yarn config set sass_binary_site https://npm.taobao.org/mirrors/node-sass/
                        yarn
                    """
                }
            }
        }

        stage('編譯打包'){
            steps{
                container('node'){
                    sh """
                        node --max_old_space_size=4096 ./node_modules/@angular/cli/bin/ng build --prod --configuration=production
                    """
                }
            }
        }

        stage('構建映象'){
            steps{
                withCredentials([[$class: 'UsernamePasswordMultiBinding',
                    // config file provider外掛中配置的config file id
                    credentialsId: '24d2a878-3f88-4648-b354-3123ca0d0e3',
                    usernameVariable: 'DOCKER_HUB_USER',
                    passwordVariable: 'DOCKER_HUB_PASSWORD']]) {
                    container('clients') {
                        sh """
                            cp Dockerfile dist/
                            echo ${DOCKER_HUB_PASSWORD} | docker login ${harbor} -u ${ DOCKER_HUB_USER } --password-stdin
                            docker build -t ${image} --build-arg=HTTP_PROXY="http://x.x.x.x:3128" --build-arg=HTTPS_PROXY="http://x.x.x.x:3128" ./dist/
                            docker push ${image}
                            docker rmi ${image}
                        """
                    }
                }
            }
        }

        stage('helm upgrade'){
            steps{
                container('clients') {
                    sh """
                    		// helm 不允許release名稱有/,替換成-
                        releaseName=`echo ${JOB_NAME} | sed 's/\\//-/'`
                        // helm chart中使用appVersion作為映象tag,此處進行替換
                        sed -i 's/appVersion: \\S*/appVersion: ${tag}/' ./chart/Chart.yaml
                        helm upgrade -i -n ${namespace} --create-namespace --reuse-values --set global.registry=${harbor} \${releaseName} ./chart
                    """
                }
            }
        }
}

jenkinsfile中的幾個關鍵點:

  1. readYaml方法是由Pipeline Utility Steps外掛提供的
  2. 以上展示中配合使用的dockerfile未使用多階段構建,所以在jenkinsfile中需要先yarn獲取依賴,再node編譯程式碼。也可以在dockerfile中使用多階段構建,jenkinsfile直接一個step即可完成依賴拉取,程式碼編譯和映象構建。
  3. 如果slave pod所在叢集需要使用代理訪問公網,可以在docker build時增加--build-arg=HTTP_PROXY="http://x.x.x.x:3128"引數使用代理訪問公網構建映象

後端

技術棧:spring->maven->docker image->k8s helm release

pod template:

後端的pod template同樣是雙容器:上圖展示了maven容器,另一個容器和前端部分的一樣,同樣集成了docker/kubectl/helm客戶端用於部署。

jenkinsfile:

container('maven'){
                            configFileProvider([configFile(fileId: "040a7123-40c0-466f-acf0-526de4aa8144", targetLocation: "settings.xml")]){
                                sh """
                                    mvn clean package -U -Dmaven.test.skip=true --settings settings.xml
                                """
                            }
                        }

大部分和前端部分相同,唯一的區別是maven構建部分,同樣使用的也是未採用多階段構建的dockerfile,並使用config file provider預配置了maven的settings檔案,注入到slave pod中,以使用私有的maven倉庫。

排坑

  • 專案配置,scm的輕量級檢出要取消勾選
  • 快速查詢jenkins內建方法及變數:進入某個構建專案,左側流水線語法
  • jenkinsfile有兩種書寫格式:指令碼化流水線和宣告式流水線,兩種語法有區別,需要統一,不可交叉使用。
  • jenkinsfile除錯效率不高,可通過以下幾個方式嘗試改進
    1. 失敗的構建可以在build詳情左側點選回放,快速修改jenkinsfile後快速重試
    2. 我在使用idea和vscode兩種編輯器編輯jenkinsfile時,發現其對groovy的lint等支援較少,可以在vscode中搜索jenkins相關外掛,嘗試remote lint或者remote debug的方式提高效率。這部分由於我司的jenkinsfile變更不算太多,就沒有深入實踐。

多使用者及許可權管理

安裝外掛Matrix Authorization Strategy,在系統設定-安全-管理使用者中新建使用者,在全域性安全配置-授權策略中選擇安全矩陣,即可以按照使用者分配許可權。

參考

文章目錄-jenkins | 超級小豆丁 (mydlq.club)

基於 Jenkins、Gitlab、Harbor、Helm 和 Kubernetes 的 CI/CD(一)-陽明的部落格|Kubernetes|Istio|Prometheus|Python|Golang|雲原生 (qikqiak.com)

流水線語法 (jenkins.io)