GitLab + Jenkins + ACK 自動化部署方案

百瓶技術發表於2022-02-07

公眾號名片
作者名片

本篇文章從實踐角度介紹如何結合我們常用的 GitLab 與 Jenkins,通過 K8s 來實現專案的自動化部署,以公司目前正在使用的生產架構圖做為此次講解的重點,如圖所示:

生產環境架構圖

本文涉及到的工具和技術包括:

  • GitLab:常用的原始碼管理系統;
  • Jenkins(Jenkins Pipeline):常用的自動化構建、部署工具,Pipeline 以流水線的方式將構建、部署的各個步驟組織起來;
  • docker(dockerfile):容器引擎,所有應用最終都要以 docker 容器執行,dockerfile 是 docker 映象定義檔案;
  • Kubernetes:Google 開源的容器編排管理系統。

環境背景:

  • 已使用 GitLab 做原始碼管理,原始碼按不同的環境建立不同的分支,如:dev (開發分支)、test(測試分支)、pre(預發分支)、master(生產分支);
  • 已搭建 Jenkins 服務;
  • 已有 docker Registry 服務,用於 docker 映象的儲存(可以基於docker Registry 或 Harbor 自建,或使用雲服務,本文使用阿里雲容器映象服務);
  • 已部署了K8s叢集。

預期效果:

  • 分環境部署應用,使開發環境、測試環境、預發環境及生產環境隔離開來,其中,開發、測試、預發環境部署在同一個 K8s 叢集中,但使用不同的 namespace ,生產環境部署在阿里雲,使用 ACK 容器服務;
  • 配置儘可能通用化,只需要通過修改少量配置檔案的少量配置屬性,就能完成新專案的自動化部署配置;
  • 開發、測試及預發環境在 push 程式碼時可以設定自動觸發構建與部署,具體根據實際情況配置,生產環境使用單獨 ACK 叢集及單獨 Jenkins 系統進行部署;
  • 整體互動流程圖如下:
    互動流程圖

專案配置檔案

首先我們要在專案的根路徑下新增一些必要的配置檔案。如圖所示

GitLab 專案圖示1

GitLab 專案圖示2

包括:

  • dockerfile 檔案,用於構建 docker 映象檔案;
  • Docker_build.sh 檔案,用於將 docker 映象打 Tag 後推送到映象倉庫中;
  • 專案 Yaml 檔案,此檔案為部署專案到 K8s 叢集的主檔案。

dockerfile

在專案根目錄中新增一個 dockerfile 檔案(檔名就是 dockerfile),定義如何構建 docker 映象,以 Java 專案為例:

# 映象來源
FROM xxxxxxxxxxxxxxxxxxxxxxxxxx.cr.aliyuncs.com/billion_basic/alpine-java:latest

# 拷貝當前目錄的應用到映象
COPY target/JAR_NAME /application/

# 宣告工作目錄,不然找不到依賴包,如果有的話
WORKDIR /application

# 宣告動態容器卷
VOLUME /application/logs

# 啟動命令
# 設定時區
ENTRYPOINT ["java","-Duser.timezone=Asia/Shanghai","-Djava.security.egd=file:/dev/./urandom"]
CMD ["-jar","-Dspring.profiles.active=SPRING_ENV","-Xms512m","-Xmx1024m","/application/JAR_NAME"]

docker_build.sh

在專案根目錄下建立一個 deploy 資料夾,此資料夾中存放各個環境專案的配置檔案,其中Docker_build.sh檔案就是專為觸發專案打包為映象檔案、重新打 Tag 後推送到映象倉庫中存在的,同樣以 Java 專案為例:

# !/bin/bash

# 模組名稱
PROJECT_NAME=$1

# 名稱空間目錄
WORKSPACE="/home/jenkins/workspace"

# 模組目錄
PROJECT_PATH=$WORKSPACE/pro_$PROJECT_NAME

# jar 包目錄
JAR_PATH=$PROJECT_PATH/target

# jar 包名稱
JAR_NAME=$PROJECT_NAME.jar

# dockerfile 目錄
dockerFILE_PATH="/$PROJECT_PATH/dockerfile"

# sed -i "s/VAR_CONTAINER_PORT1/$PROJECT_PORT/g" $PROJECT_PATH/dockerfile
sed -i "s/JAR_NAME/$JAR_NAME/g" $PROJECT_PATH/dockerfile
sed -i "s/SPRING_ENV/k8s/g" $PROJECT_PATH/dockerfile

cd $PROJECT_PATH

# 登入阿里雲倉庫
docker login  xxxxxxxxxxxxxxxxxxxxxxxxxx.cr.aliyuncs.com -u 百瓶網 -p xxxxxxxxxxxxxxxxxxxxxxxxxx

# 構建模組映象
docker build -t $PROJECT_NAME  . 
docker tag $PROJECT_NAME xxxxxxxxxxxxxxxxxxxxxxxxxx.cr.aliyuncs.com/billion_pro/pro_$PROJECT_NAME:$BUILD_NUMBER

# 推送到阿里雲倉庫
docker push xxxxxxxxxxxxxxxxxxxxxxxxxx.cr.aliyuncs.com/billion_pro/pro_$PROJECT_NAME:$BUILD_NUMBER

project.yaml檔案

project.yaml 定義了專案部署到K8s叢集中所需的專案名稱、PV、PVC、namespace、副本數、映象地址、服務埠、醒目自檢、專案資源請求配置、檔案掛載及 service 等:

# ------------------- PersistentVolume(定義PV) ------------------- #
apiVersion: v1
kind: PersistentVolume
metadata:
# 專案名稱
  name: pv-billionbottle-wx
  namespace: billion-pro
  labels:  
    alicloud-pvname: pv-billionbottle-wx
spec:
  capacity:
    storage: 5Gi
  accessModes:
    - ReadWriteMany
  csi:
    driver: nasplugin.csi.alibabacloud.com
    volumeHandle: pv-billionbottle-wx
    volumeAttributes:
      server: "xxxxxxxxxxxxx.nas.aliyuncs.com"
      path: "/k8s/java"
  mountOptions:
  - nolock,tcp,noresvport
  - vers=3

---
# ------------------- PersistentVolumeClaim(定義PVC) ------------------- #
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
  name: pvc-billionbottle-wx
  namespace: billion-pro
spec:
  accessModes:
    - ReadWriteMany
  resources:
    requests:
      storage: 5Gi
  selector:
    matchLabels:
      alicloud-pvname: pv-billionbottle-wx      

---      
# ------------------- Deployment ------------------- #
apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    k8s-app: billionbottle-wx
  name: billionbottle-wx
# 定義 namespace  
  namespace: billion-pro
spec:
# 定義副本數
  replicas: 2
  revisionHistoryLimit: 10
  selector:
    matchLabels:
      k8s-app: billionbottle-wx
  template:
    metadata:
      labels:
        k8s-app: billionbottle-wx
    spec:
      serviceAccountName: default
      imagePullSecrets:
        - name: registrykey-k8s
      containers:
      - name: billionbottle-wx
# 定義映象地址  
        image: $IMAGE_NAME 
        imagePullPolicy: IfNotPresent
# 定義自檢     
        livenessProbe:
          failureThreshold: 3
          initialDelaySeconds: 60
          periodSeconds: 60
          successThreshold: 1
          tcpSocket:
            port: 8020
          timeoutSeconds: 1
        ports:
# 定義服務埠    
          - containerPort: 8020
            protocol: TCP
        readinessProbe:
          failureThreshold: 3
          initialDelaySeconds: 60
          periodSeconds: 60
          successThreshold: 1
          tcpSocket:
            port: 8020
          timeoutSeconds: 1
# 定義專案資源配置     
        resources:
          requests:
            memory: "1024Mi"
            cpu: "300m"
          limits:
            memory: "1024Mi"
            cpu: "300m"
# 定義檔案掛載
        volumeMounts:
          - name: pv-billionbottle-key
            mountPath: "/home/billionbottle/key"         
          - name: pvc-billionbottle-wx
            mountPath: "/billionbottle/logs"
      volumes:
        - name: pv-billionbottle-key
          persistentVolumeClaim:
            claimName: pvc-billionbottle-key  
        - name: pvc-billionbottle-wx
          persistentVolumeClaim:
            claimName: pvc-billionbottle-wx

---
# ------------------- Dashboard Service(定義service) ------------------- #
apiVersion: v1
kind: Service
metadata:
  labels:
    k8s-app: billionbottle-wx
  name: billionbottle-wx
  namespace: billion-pro
spec:
  ports:
    - port: 8020
      targetPort: 8020
  type: ClusterIP
  selector:
    k8s-app: billionbottle-wx

這裡預設通過 Pipeline 定義了映象的路徑,可直接用變數去替換 $IMAGE_NAME,且可以在這裡直接指定容器的埠而不用去改 dockerfile 檔案模版(讓模版檔案在各個環境複用,通常不需要去做修改),同時新增了 ENV 的配置,可以直接讀取 configmap 的配置檔案。將 Service type 從預設的 NodePort 改為 ClusterIp 保證專案之間只在內部通訊。部署不同專案時只需要修改 docker_build.shProject.yaml 中的環境變數、專案名稱及其他少量配置項,根目錄下的 dockerfile 檔案可以複用到各個環境。

部署時,我們要在 K8s 叢集中的 Docker 映象倉庫拉取映象,因此我們要先在 K8s 中建立映象倉庫訪問憑證(imagePullSecrets)。

# 登入 docker Registry 生成 /root/.docker/config.json 檔案
docker login --username=your-username registry.cn-xxxxx.aliyuncs.com
# 建立 namespace billion-pro (我這裡時根據專案的環境分支名稱建立的namespace)
kubectl create namespace billion-pro
# 在 namespace billion-pro 中建立一個 secret 
kubectl create secret registrykey-k8s aliyun-registry-secret --from-file=.dockerconfigjson=/root/.docker/config.json --type=kubernetes.io/dockerconfigjson --name=billion-pro

Jenkinsfile (Pipeline)

Jenkinsfile 是 Jenkins Pipeline 配置檔案,遵循 Groovy 指令碼規範。對於 Java 專案的構建部署,Jenkinsfile 的 Pipeline 指令碼檔案如下:

#!/bin/sh -ilex
def env = "pro"
def registry = "xxxxxxxxxxxxxxx.cn-shenzhen.cr.aliyuncs.com"
def git_address = "http://xxxxxxxxx/billionbottle/billionbottle-wx.git"
def git_auth = "1eb0be9b-ffbd-457c-bcbf-4183d9d9fc35"
def project_name = "billionbottle-wx"
def k8sauth = "8dd4e736-c8a4-45cf-bec0-b30631d36783"
def image_name = "${registry}/billion_pro/pro_${project_name}:${BUILD_NUMBER}"

pipeline{
      environment{
        BRANCH =  sh(returnStdout: true,script: 'echo $branch').trim()
        } 
        agent{
            node{
              label 'master'
            }
        }
        stages{
            stage('Git'){
            steps{
            git branch: '${Branch}', credentialsId: "${git_auth}", url: "${git_address}"
            }
        }
        stage('maven build'){
            steps{
            sh "mvn clean package -U -DskipTests"
            }
        }
        stage('docker build'){
            steps{
            sh "chmod 755 ./deploy/${env}_docker_build.sh && ./deploy/${env}_docker_build.sh ${project_name} ${env}"
            }
        }
        stage('K8s deploy'){
            steps{
                sh "pwd && sed -i 's#\$IMAGE_NAME#${image_name}#' deploy/${env}_${project_name}.yaml"
                kubernetesDeploy configs: "deploy/${env}_${project_name}.yaml", kubeconfigId: "${k8sauth}"
            }
        }
    }
}

Jenkinsfile 的 Pipeline 指令碼定義了整個自動化構建部署流程:

  • Code Analyze:可以使用 SonarQube 之類的靜態程式碼分析工具完成程式碼檢查,這裡先忽略;
  • Maven Build:啟動一個 maven 的程式來完成專案 maven 的構建打包,也可以啟動一個 maven 容器,掛載 maven 本地倉庫目錄到宿主機,避免每次都需要重新下載依賴包;
  • docker Build:構建 docker 映象,並推送到映象倉庫,不同環境的映象通過 tag 字首區分,比如開發環境是 dev_,測試環境是 test_,預發環境是 pre_,生產環境是 pro_
  • K8s Deploy:使用 Jenkins 自帶外掛完成專案的部署,或已有專案的更新迭代,不同環境使用不同的引數配置,K8s 叢集的訪問憑證可用 kube_config 來直接配置。

Jenkins 的配置

Jenkins 任務配置

在 Jenkins 中建立一個 Pipeline 的任務,如圖:

Jenkins 流水線配置

配置構建觸發器,將目標分支設定為 master 分支,如圖:

jenkins 分支配置

配置流水線,選擇「Pipeline script」並配置 Pipeline 指令碼檔案,配置專案 Git 地址,拉取原始碼憑證等,如圖:

Pipeline 指令碼

上圖中引用的金鑰憑據需要提前在 jenkins 中配置,如下圖:

jenkins 金鑰配置

儲存即完成了專案生產環境的 Jenkins 配置,其它環境同理,需要注意的是區分各個環境所對應的分支

Kubernetes 叢集功能介紹

K8s 它是基於容器的叢集編排引擎,具備擴張叢集、滾動升級回滾、彈性伸縮、自動治癒、服務發現等多種特效能力,結合目前生產環境的實際情況,重點介紹幾個常用的功能點,如需詳細瞭解其它功能,請直接在 Kubernets 官網 查詢。

Kubernetes 架構圖

K8s 架構圖

從巨集觀上來說 Kubernetes 的整體架構,包含 Master、Node 以及 Etcd。

Master 即主節點,負責控制整個 kubernetes 叢集。它包含 Api Server、Scheduler、Controller 等部分,它們都需要和 Etcd 進行互動以儲存資料。

  • Api Server:主要提供資源操作的統一入口,這樣就遮蔽了與 Etcd 的直接互動,功能包含安全、註冊與發現等;
  • Scheduller:負責按照一定的排程規則將 pod 排程到 Node 上;
  • Controller:資源控制中心,確保資源處於預期的狀態。

Node 即工作節點,為整個叢集提供計算力,是容器真正執行的地方,包括執行容器、kubelet、kube-proxy。

  • kubelet:主要工作是管理容器的生命週期,結合 cAdvisor 進行監控、健康檢查以及定期上報節點狀態;
  • kube-proxy:主要利用 service 提供叢集內部的服務發現和負載均衡,同時監聽 service/endpoints 變化重新整理負載均衡。

容器編排

Kubernetes 中有諸多編排相關的控制資源,例如編排無狀態應用的 deployment,編排有狀態應用的 statefulset,編排守護程式 daemonset 以及編排離線任務的 job/cronjob 等等。

我們以目前生產環境應用的 deployment 為例, deployment 、replicatset 、pod 之間的關係是一種層層控制的關係,簡單來說,replicatset 控制 pod 的數量,而 deployment 控制 replicatset 的版本屬性,這種設計模式也為兩種最基本的編排動作實現了基礎,即數量控制的水平擴縮容,版本屬性控制的更新/回滾。

水平擴縮容

pod 水平擴充套件示意圖

水平擴縮容非常好理解,我們只需要修改 replicatset 控制的 pod 副本數量即可,比如從 2 改到 3 ,那麼就完成了水平擴容這個動作,反之即水平收縮。

滾動更新部署(Rolling Update)

滾動部署是 K8s 中的預設部署策略,它用新版本的 pod 一個一個地替換應用程式先前版本的 pod ,而沒有任何叢集停機的時間,滾動部署緩慢地用新版本應用程式的例項替換之前版本的應用程式例項,如圖所示:

pod 滾動更新示意圖

在滾動更新的實際應用中我們可以配置 RollingUpdateStrategy 來控制滾動更新策略,另外還有兩個選項可以讓我們微調更新過程:

  • maxSurge :更新期間可以建立 pod 數量超過所需 pod 的數量,這可以是副本計數的絕對數量或百分比,預設值為25%;
  • maxUnavailable :更新過程中不可用的 pod 數量,這可以是副本計數的絕對數量或百分比,預設值為25%。

微服務(service)

瞭解微服務前,我們要線瞭解一個很重要的資源物件 —— service

在微服務中, pod 可以對應例項,那麼 service 對應的就是微服務,而在服務呼叫的過程中,service 的出現解決了兩個問題:

  • pod 的 ip 不是固定的,利用非固定的 ip 進行網路呼叫不現實;
  • 服務呼叫需要對不同的 pod 進行負載均衡。

service 通過 label 選擇器選取合適的 pod,構建出一個 endpoints,即 pod 負載均衡列表,實際運用中,一般我們會為同一個微服務的 pod 例項都搭上類似 app=xxx 的標籤,同時為該微服務建立一個標籤選擇器為 app=xxx 的 service。

Kubernetes 中的網路

K8s 的網路通訊,首先得有“三通”基礎:

  • node 到 pod 之間可以互通;
  • node 的 pod 之間可以互通;
  • 不同 node 之間的 pod 可以互通。

pod 間互相通訊示意圖

簡單來說不同 pod 之間通過 cni0/docker0 網橋實現了通訊,node 訪問 pod 也是通過網橋通訊,
而不同的 node 之間的 pod 通訊有很多種實現方式,包括現在比較普遍的 flannel 的 vxlan/hostgw 模式等,flannel 通過 etcd 獲知其它 node 的網路資訊,並會為本 node 建立路由表,最終使得不同 node 間可以實現跨主機通訊。

小結

到現在為止,已基本介紹清楚了我們生產環境整體架構中使用到的基礎元件的相關概念,它們是如何執行的,以及微服務是怎麼執行在 Kubernetes 中的,但涉及到配置中心、監控及告警等一些其它的元件暫未詳細介紹,爭取儘快更新這部分內容。

更多精彩請關注我們的公眾號「百瓶技術」,有不定期福利呦!

相關文章