1. 程式人生 > 其它 >Kubernetes DevOps: Jenkins

Kubernetes DevOps: Jenkins

提到基於 Kubernete 的CI/CD,可以使用的工具有很多,比如 Jenkins、Gitlab CI 以及新興的 drone 之類的,我們這裡會使用大家最為熟悉的 Jenkins 來做 CI/CD 的工具。

安裝

既然要基於 Kubernetes 來做 CI/CD,我們這裡最好還是將 Jenkins 安裝到 Kubernetes 叢集當中,安裝的方式也很多,我們這裡仍然還是使用手動的方式,這樣可以瞭解更多細節,對應的資源清單檔案如下所示:

apiVersion: v1
kind: PersistentVolume
metadata:
  name: jenkins-pv
spec:
  storageClassName: local  # Local PV
  capacity:
    storage: 2Gi
  volumeMode: Filesystem
  accessModes:
  - ReadWriteOnce
  local:  
    path: /data/k8s/jenkins
  nodeAffinity:
    required:
      nodeSelectorTerms:
      - matchExpressions:
        - key: kubernetes.io/hostname
          operator: In
          values:
          - ydzs-node6
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: jenkins-pvc
  namespace: kube-ops
spec:
  storageClassName: local
  accessModes:
  - ReadWriteOnce
  resources:
    requests:
      storage: 2Gi
---
apiVersion: v1
kind: ServiceAccount
metadata:
  name: jenkins
  namespace: kube-ops
---
kind: ClusterRole
apiVersion: rbac.authorization.k8s.io/v1beta1
metadata:
  name: jenkins
rules:
  - apiGroups: ["extensions", "apps"]
    resources: ["deployments", "ingresses"]
    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", "events"]
    verbs: ["get","list","watch"]
  - apiGroups: [""]
    resources: ["secrets"]
    verbs: ["get"]
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRoleBinding
metadata:
  name: jenkins
  namespace: kube-ops
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: jenkins
subjects:
  - kind: ServiceAccount
    name: jenkins
    namespace: kube-ops
---
apiVersion: v1
kind: ConfigMap
metadata:
  name: jenkins-mirror-conf
  namespace: kube-ops
data:
  nginx.conf: |
    user nginx;
    worker_processes  3;
    error_log  /dev/stderr;
    events {
      worker_connections  10240;
    }
    http {
      log_format main '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for" $request_time';
      access_log    /dev/stdout main;
      server {
          listen 80;
          server_name mirrors.jenkins-ci.org;
          location / {
            proxy_redirect off;
            proxy_pass https://mirrors.tuna.tsinghua.edu.cn/jenkins/;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header Accept-Encoding "";
            proxy_set_header Accept-Language "zh-CN";
          }
          index index.html index.htm index.php;
          location ~ /\. {
            deny all;
          }
      }
    }
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: jenkins
  namespace: kube-ops
spec:
  selector:
    matchLabels:
      app: jenkins
  template:
    metadata:
      labels:
        app: jenkins
    spec:
      serviceAccount: jenkins
      hostAliases:
      - ip: "127.0.0.1"
        hostnames:
        - "mirrors.jenkins-ci.org"
      initContainers:
      - name: fix-permissions
        image: busybox
        command: ["sh", "-c", "chown -R 1000:1000 /var/jenkins_home"]
        securityContext:
          privileged: true
        volumeMounts:
        - name: jenkinshome
          mountPath: /var/jenkins_home
      containers:
      - name: mirror
        image: nginx:1.7.9
        ports:
        - containerPort: 80
        volumeMounts:
        - mountPath: /etc/nginx 
          readOnly: true
          name: nginx-conf
      - name: jenkins
        image: jenkins/jenkins:lts
        imagePullPolicy: IfNotPresent
        ports:
        - containerPort: 8080
          name: web
          protocol: TCP
        - containerPort: 50000
          name: agent
          protocol: TCP
        resources:
          limits:
            cpu: 1500m
            memory: 2048Mi
          requests:
            cpu: 1500m
            memory: 2048Mi
        readinessProbe:
          httpGet:
            path: /login
            port: 8080
          initialDelaySeconds: 60
          timeoutSeconds: 5
          failureThreshold: 12
        volumeMounts:
        - name: jenkinshome
          mountPath: /var/jenkins_home
      volumes:
      - name: jenkinshome
        persistentVolumeClaim:
          claimName: jenkins-pvc
      - name: nginx-conf
        configMap:
          name: jenkins-mirror-conf
          items:
          - key: nginx.conf
            path: nginx.conf
---
apiVersion: v1
kind: Service
metadata:
  name: jenkins
  namespace: kube-ops
  labels:
    app: jenkins
spec:
  selector:
    app: jenkins
  ports:
  - name: web
    port: 8080
    targetPort: web
  - name: agent
    port: 50000
    targetPort: agent
# ---
# apiVersion: extensions/v1beta1
# kind: Ingress
# metadata:
#   name: jenkins
#   namespace: kube-ops
# spec:
#   rules:
#   - host: jenkins.k8s.local
#     http:
#       paths:
#       - backend:
#           serviceName: jenkins
#           servicePort: web
---
apiVersion: traefik.containo.us/v1alpha1
kind: IngressRoute
metadata:
  name: jenkins
  namespace: kube-ops
spec:
  entryPoints:
  - web   
  routes:
  - kind: Rule
    match: Host(`jenkins.k8s.local`)
    services:
    - name: jenkins
      port: 8080

我們這裡使用一個名為 jenkins/jenkins:lts 的映象,這是 jenkins 官方的 Docker 映象,然後也有一些環境變數,當然我們也可以根據自己的需求來定製一個映象,比如我們可以將一些外掛打包在自定義的映象當中,可以參考文件:https://github.com/jenkinsci/docker,我們這裡使用預設的官方映象就行,另外一個還需要注意的資料的持久化,將容器的 /var/jenkins_home 目錄持久化即可,同樣為了效能考慮,我們這裡使用 Local PV,將 Pod 排程到固定的節點上。

由於我們這裡使用的映象內部執行的使用者 uid=1000,所以我們這裡掛載出來後會出現許可權問題,為解決這個問題,我們同樣還是用一個簡單的 initContainer 來修改下我們掛載的資料目錄。

另外我們這裡還需要使用到一個擁有相關許可權的 serviceAccount:jenkins,我們這裡只是給 jenkins 賦予了一些必要的許可權,當然如果你對 serviceAccount 的許可權不是很熟悉的話,我們給這個 sa 繫結一個 cluster-admin 的叢集角色許可權也是可以的,當然這樣具有一定的安全風險。

除此之外,這裡我們還添加了一個額外的名為 mirror 的容器,新增這個容器的目的是使用一個 nginx 容器來反向代理 Jenkins 外掛的官方源到清華大學的源上面,因為官方源實在是太慢了,我們這裡將官方的映象地址 mirrors.jenkins-ci.org 通過 hostAlias 對映到了 127.0.0.1 這個地址上,而這個地址恰好就是 mirror 這個 nginx 容器,我們通過一個 ConfigMap 來配置 Nginx,將 mirros.jenkins-ci.org 反向代理到了 proxy_pass

https://mirrors.tuna.tsinghua.edu.cn/jenkins/;,這樣當我們在 Jenkins 中要下載外掛的時候實際上會被代理到清華的源上面去,這樣就大大加快了外掛下載的速度。

最後就是通過 IngressRoute 來暴露我們的服務,這個比較簡單。

我們直接來建立 jenkins 的資源清單即可:

$ kubectl apply -f jenkins.yaml
$ kubectl get pods -n kube-ops -l app=jenkins
NAME                       READY   STATUS    RESTARTS   AGE
jenkins-5b957d4b8f-t22gp   2/2     Running   0          18m
$ kubectl logs -f jenkins-5b957d4b8f-t22gp jenkins -n kube-ops
......
2019-12-16 13:26:03.756+0000 [id=39]    INFO    hudson.model.AsyncPeriodicWork$1#run: Finished Download metadata. 28,073 ms
2019-12-16 13:26:03.760+0000 [id=26]    INFO    jenkins.InitReactorRunner$1#onAttained: Completed initialization
2019-12-16 13:26:03.863+0000 [id=19]    INFO    hudson.WebAppMain$3#run: Jenkins is fully up and running

看到上面的 run: Jenkins is fully up and running 資訊就證明我們的 Jenkins 應用以前啟動起來了。

然後我們可以通過 IngressRoute 中定義的域名 jenkins.k8s.local(需要做 DNS 解析或者在本地 /etc/hosts 中新增對映)來訪問 jenkins 服務:

然後可以執行下面的命令獲取解鎖的管理員密碼:

$ kubectl exec -it jenkins-5b957d4b8f-t22gp -c jenkins -n kube-ops -- cat /var/jenkins_home/secrets/initialAdminPassword
35b083de1d25409eaef57255e0da481a   # jenkins啟動日誌裡面也有

然後選擇安裝推薦的外掛即可,由於我們已經做了外掛的反向代理了,所以理論上安裝速度會比較快

安裝完成後新增管理員帳號即可進入到 jenkins 主介面:

架構

Jenkins 安裝完成了,接下來我們不用急著就去使用,我們要了解下在 Kubernetes 環境下面使用 Jenkins 有什麼好處。

我們知道持續構建與釋出是我們日常工作中必不可少的一個步驟,目前大多公司都採用 Jenkins 叢集來搭建符合需求的 CI/CD 流程,然而傳統的 Jenkins Slave 一主多從方式會存在一些痛點,比如:

  • 主 Master 發生單點故障時,整個流程都不可用了
  • 每個 Slave 的配置環境不一樣,來完成不同語言的編譯打包等操作,但是這些差異化的配置導致管理起來非常不方便,維護起來也是比較費勁
  • 資源分配不均衡,有的 Slave 要執行的 job 出現排隊等待,而有的 Slave 處於空閒狀態
  • 資源有浪費,每臺 Slave 可能是物理機或者虛擬機器,當 Slave 處於空閒狀態時,也不會完全釋放掉資源。

正因為上面的這些種種痛點,我們渴望一種更高效更可靠的方式來完成這個 CI/CD 流程,而 Docker 虛擬化容器技術能很好的解決這個痛點,又特別是在 Kubernetes 叢集環境下面能夠更好來解決上面的問題,下圖是基於 Kubernetes 搭建 Jenkins 叢集的簡單示意圖:

從圖上可以看到 Jenkins Master 和 Jenkins Slave 以 Pod 形式執行在 Kubernetes 叢集的 Node 上,Master 執行在其中一個節點,並且將其配置資料儲存到一個 Volume 上去,Slave 執行在各個節點上,並且它不是一直處於執行狀態,它會按照需求動態的建立並自動刪除。

這種方式的工作流程大致為:當 Jenkins Master 接受到 Build 請求時,會根據配置的 Label 動態建立一個執行在 Pod 中的 Jenkins Slave 並註冊到 Master 上,當執行完 Job 後,這個 Slave 會被登出並且這個 Pod 也會自動刪除,恢復到最初狀態。

那麼我們使用這種方式帶來了哪些好處呢?

  • 服務高可用,當 Jenkins Master 出現故障時,Kubernetes 會自動建立一個新的 Jenkins Master 容器,並且將 Volume 分配給新建立的容器,保證資料不丟失,從而達到叢集服務高可用。
  • 動態伸縮,合理使用資源,每次執行 Job 時,會自動建立一個 Jenkins Slave,Job 完成後,Slave 自動登出並刪除容器,資源自動釋放,而且 Kubernetes 會根據每個資源的使用情況,動態分配 Slave 到空閒的節點上建立,降低出現因某節點資源利用率高,還排隊等待在該節點的情況。
  • 擴充套件性好,當 Kubernetes 叢集的資源嚴重不足而導致 Job 排隊等待時,可以很容易的新增一個 Kubernetes Node 到叢集中,從而實現擴充套件。 是不是以前我們面臨的種種問題在 Kubernetes 叢集環境下面是不是都沒有了啊?看上去非常完美。

配置

接下來我們就需要來配置 Jenkins,讓他能夠動態的生成 Slave 的 Pod。

第1步. 我們需要安裝 kubernetes 外掛, 點選 Manage Jenkins -> Manage Plugins -> Available -> Kubernetes 勾選安裝即可。

第2步. 安裝完畢後,點選 Manage Jenkins —> Configure System —> (拖到最下方),如果有 Add a new cloud —> 選擇 Kubernetes,然後填寫 Kubernetes 和 Jenkins 配置資訊即可,但是最新版本的 Kubernetes 外掛將配置單獨放置到了一個頁面中:

這個時候需要點選 a separate configuration page 這個連結,跳轉到 Configure Cloud 頁面:

在該頁面我們可以點選 Add a new cloud -> 選擇 Kubernetes,然後填寫 Kubernetes 和 Jenkins 配置資訊:

注意 namespace,我們這裡填 kube-ops,然後點選 Test Connection,如果出現 Connection test successful 的提示資訊證明 Jenkins 已經可以和 Kubernetes 系統正常通訊了,然後下方的 Jenkins URL 地址:http://jenkins.kube-ops.svc.cluster.local:8080,這裡的格式為:服務名.namespace.svc.cluster.local:8080,根據上面建立的 jenkins 的服務名填寫。

第3步. 配置 Pod Template,其實就是配置 Jenkins Slave 執行的 Pod 模板,名稱空間我們同樣是用 kube-ops,Labels 這裡也非常重要,對於後面執行 Job 的時候需要用到該值,然後我們這裡使用的是 cnych/jenkins:jnlp6 這個映象,這個映象是在官方的 jnlp 映象基礎上定製的,加入了 docker、kubectl 等一些實用的工具。

容器的名稱必須是 jnlp,這是預設拉起的容器,另外需要將 Command to run 和 Arguments to pass to the command 的值都刪除掉,否則會失敗。

然後我們這裡需要在下面掛載兩個主機目錄,一個是 /var/run/docker.sock,該檔案是用於 Pod 中的容器能夠共享宿主機的 Docker,這就是大家說的 docker in docker 的方式,Docker 二進位制檔案已經打包到上面的映象中了,另外一個目錄下 /root/.kube 目錄,我們將這個目錄掛載到容器的 /root/.kube 目錄下面這是為了讓我們能夠在 Pod 的容器中能夠使用 kubectl 工具來訪問我們的 Kubernetes 叢集,方便我們後面在 Slave Pod 部署 Kubernetes 應用。

另外如果在配置了後執行 Slave Pod 的時候出現了許可權問題,這是因為 Jenkins Slave Pod 中沒有配置許可權,所以需要配置上 ServiceAccount,在 Slave Pod 配置的地方點選下面的高階,新增上對應的 ServiceAccount 即可:

到這裡我們的 Kubernetes 外掛就算配置完成了。

測試

Kubernetes 外掛的配置工作完成了,接下來我們就來新增一個 Job 任務,看是否能夠在 Slave Pod 中執行,任務執行完成後看 Pod 是否會被銷燬。

在 Jenkins 首頁點選 create new jobs,建立一個測試的任務,輸入任務名稱,然後我們選擇 Freestyle project 型別的任務,注意在下面的 Label Expression 這裡要填入 ydzs-jnlp,就是前面我們配置的 Slave Pod 中的 Label,這兩個地方必須保持一致:

然後往下拉,在 Build 區域選擇Execute shell

然後輸入我們測試命令

echo "測試 Kubernetes 動態生成 jenkins slave"
echo "==============docker in docker==========="
docker info

echo "=============kubectl============="
kubectl get pods

最後點選儲存

現在我們直接在頁面點選左側的 Build now 觸發構建即可,然後觀察 Kubernetes 叢集中 Pod 的變化:

$ kubectl get pods -n kube-ops                        
NAME                                           READY   STATUS              RESTARTS   AGE
jenkins-68ccff445c-dk24f                       1/1     Running             0          12h
jnlp-1g893                                     0/1     ContainerCreating   0          83s

我們可以看到在我們點選立刻構建的時候可以看到一個新的 Pod:jnlp-1g893 被建立了,這就是我們的 Jenkins Slave。任務執行完成後我們可以看到任務資訊,比如我們這裡是 花費了 21s 時間在 jnlp-1g893 這個 Slave上面

到這裡證明我們的任務已經構建完成,然後這個時候我們再去叢集檢視我們的 Pod 列表,發現 kube-ops 這個 namespace 下面已經沒有之前的 Slave 這個 Pod 了。

$ kubectl get pods -n kube-ops                        
NAME                                           READY   STATUS              RESTARTS   AGE
jenkins-68ccff445c-dk24f                       1/1     Running             0          12h

到這裡我們就完成了使用 Kubernetes 動態生成 Jenkins Slave 的方法。