現在這家單位的 CICD 比較的混亂,然後突發奇想,想改造下,於是就用pipeline做了一個簡單的流水線,下面是關於它的一些介紹
寫一個簡單的流水線大概就是這麼個流程簡單來說就是:拉程式碼---》編譯---》打映象---》推映象---》部署到 k8s 中,下面的 pipeline 就是在這條主線上進行,根據情況進行增加
pipeline { agent { label 'pdc&&jdk8' } environment { git_addr = "程式碼倉庫地址" git_auth = "拉程式碼時的認證ID" pom_dir = "pom檔案的目錄位置(相對路徑)" server_name = "服務名" namespace_name = "服務所在的名稱空間" img_domain = "映象地址" img_addr = "${img_domain}/cloudt-safe/${server_name}"// cluster_name = "叢集名" } stages { stage('Clear dir') { steps { deleteDir() } } stage('Pull server code and ops code') { parallel { stage('Pull server code') { steps { script { checkout( [ $class: 'GitSCM', branches: [[name: '${Branch}']], userRemoteConfigs: [[credentialsId: "${git_auth}", url: "${git_addr}"]] ] ) } } } stage('Pull ops code') { steps { script { checkout( [ $class: 'GitSCM', branches: [[name: 'pipeline-0.0.1']], //拉取的構建指令碼的分支 doGenerateSubmoduleConfigurations: false, extensions: [[$class: 'RelativeTargetDirectory', relativeTargetDir: 'DEPLOYJAVA']], //DEPLOYJAVA: 把程式碼存放到此目錄中 userRemoteConfigs: [[credentialsId: 'chenf-o', url: '構建指令碼的倉庫地址']] ] ) } } } } } stage('Set Env') { steps { script { date_time = sh(script: "date +%Y%m%d%H%M", returnStdout: true).trim() git_cm_id = sh(script: "git rev-parse --short HEAD", returnStdout: true).trim() whole_img_addr = "${img_addr}:${date_time}_${git_cm_id}" } } } stage('Complie Code') { steps { script { withMaven(maven: 'maven_latest_linux') { sh "mvn -U package -am -amd -P${env_name} -pl ${pom_dir}" } } } } stage('Build image') { steps { script { dir("${env.WORKSPACE}/${pom_dir}") { sh """ echo 'FROM 基礎映象地址' > Dockerfile //由於我這裡進行了映象的最佳化,只指定一個基礎映象地址即可,後面會詳細的說明 """ withCredentials([usernamePassword(credentialsId: 'faabc5e8-9587-4679-8c7e-54713ab5cd51', passwordVariable: 'img_pwd', usernameVariable: 'img_user')]) { sh """ docker login -u ${img_user} -p ${img_pwd} ${img_domain} docker build -t ${img_addr}:${date_time}_${git_cm_id} . docker push ${whole_img_addr} """ } } } } } stage('Deploy img to K8S') { steps { script { dir('DEPLOYJAVA/deploy') { //執行構建指令碼 sh """ /usr/local/python3/bin/python3 deploy.py -n ${server_name} -s ${namespace_name} -i ${whole_img_addr} -c ${cluster_name} """ } } } // 做了下判斷如果上面指令碼執行失敗,會把上面階段打的映象刪除掉 post { failure { sh "docker rmi -f ${whole_img_addr}" } } } stage('Clear somethings') { steps { script { // 刪除打的映象 sh "docker rmi -f ${whole_img_addr}" } } post { success { // 如果上面階段執行成功,將把當前目錄刪掉 deleteDir() } } } }}複製程式碼
最佳化構建映象上面的 pipeline 中有一條命令是生成Dockerfile的,在這裡做了很多最佳化,雖然我的Dockerfile就寫了一個FROM,但是在這之後又會執行一系列的操作,下面我們對比下沒有做最佳化的Dockerfile 未最佳化
FROM 基礎映象地址RUN mkdir xxxxxCOPY *.jar /usr/app/app.jarENTRYPOINT java -jar app.jar複製程式碼
最佳化後的
FROM 基礎映象地址複製程式碼
最佳化後的Dockerfile就這一行就完了。。。。。 下面簡單介紹下這個ONBUILDONBUILD 可以這樣理解,就比如我們這裡使用的映象,是基於 java 語言做的一個映象,這個映象有兩部分,一個是包含 JDK 的基礎映象 A,另一個是包含 jar 包的映象 B,關係是先有 A 再有 B,也就是說 B 依賴於 A。假設一個完整的基於 Java 的 CICD 場景,我們需要拉程式碼,編譯,打映象,推映象,更新 pod 這一系列的步驟,而在打映象這個過程中,我們需要把編譯後的產物 jar 包 COPY 到基礎映象中,這就造成了,我們還得寫一個 Dockerfile,用來 COPY jar 包,就像下面這個樣子:
FROM jdk基礎映象COPY xxx.jar /usr/bin/app.jarENTRYPOINT java -jar app.jar複製程式碼
這樣看起來也還好,基本上三行就解決了,但是能用一行就解決為什麼要用三行呢?
FROM jdk基礎映象ONBUILD COPY target/*.jar /usr/bin/app.jarCMD ["/start.sh"]複製程式碼
打成一個映象,比如映象名是:java-service:jdk1.8,在打映象的時候,ONBUILD後面的在本地打映象的過程中不會執行,而是在下次引用時執行的
FROM java-service:jdk1.8複製程式碼
只需要這一行就可以了,並且這樣看起來更加簡潔,pipeline看起來也很規範,這樣的話,我們每一個 java 的服務都可以使用這一行 Dockerfile 了。
使用憑據有時候使用 docker 進行 push 映象時需要進行認證,如果我們直接在 pipeline 裡寫的話不太安全,所以得進行脫敏,這樣的話我們就需要用到憑據了,新增憑據也是非常簡單,由於我們只是儲存我們的使用者名稱和密碼,所以用Username with password型別的憑據就可以了,如下所示
比如說:拉取 git 倉庫的程式碼需要用到,然後這裡就新增一個憑據,對應與 pipeline 裡的下面這段內容:
stage('Pull server code') { steps { script { checkout( [ $class: 'GitSCM', branches: [[name: '${Branch}']], userRemoteConfigs: [[credentialsId: "${git_auth}", url: "${git_addr}"]] ] ) } }}複製程式碼
這裡的變數${git_auth}就是新增憑據時設定的ID,如果不設定ID會隨機生成一個ID
選擇withCredentials: Bind credentials to variables
然後和之前新增的憑據進行繫結,這裡選擇型別為:Username and password (separated)
設定使用者名稱和密碼的變數名,然後選擇剛才新增好的憑據
withCredentials([usernamePassword(credentialsId: 'faabc5e8-9587-4679-8c7e-54713ab5cd51', passwordVariable: 'img_pwd', usernameVariable: 'img_user')]) { sh """ docker login -u ${img_user} -p ${img_pwd} ${img_domain} docker build -t ${img_addr}:${date_time}_${git_cm_id} . docker push ${whole_img_addr} """}複製程式碼
credentialsId: 這個 ID 就是隨機生成的 ID
執行指令碼進行更新映象這裡是使用 python 寫了一個小指令碼,來呼叫 kubernetes 的介面做了一個patch的操作完成的。先來看下此指令碼的目錄結構
核心程式碼:deploy.py核心檔案:config.yaml 存放的是 kubeconfig 檔案,用於和 kubernetes 的認證
下面貼一下deploy.py的指令碼內容,可以參考下:
import osimport argparsefrom kubernetes import client, configclass deployServer: def __init__(self, kubeconfig): self.kubeconfig = kubeconfig config.kube_config.load_kube_config(config_file=self.kubeconfig) self._AppsV1Api = client.AppsV1Api() self._CoreV1Api = client.CoreV1Api() self._ExtensionsV1beta1Api = client.ExtensionsV1beta1Api() def deploy_deploy(self, deploy_namespace, deploy_name, deploy_img=None, deploy_which=1): try: old_deploy = self._AppsV1Api.read_namespaced_deployment( name=deploy_name, namespace=deploy_namespace, ) old_deploy_container = old_deploy.spec.template.spec.containers pod_num = len(old_deploy_container) if deploy_which == 1: pod_name = old_deploy_container[0].name old_img = old_deploy_container[0].image print("獲取上一個版本的資訊\n") print("當前Deployment有 {} 個pod, 為: {}\n".format(pod_num, pod_name)) print("上一個版本的映象地址為: {}\n".format(old_img)) print("此次構建的映象地址為: {}\n".format(deploy_img)) print("正在替換當前服務的映象地址....\n") old_deploy_container[deploy_which - 1].image = deploy_img else: print("只支援替換一個映象地址") exit(-1) new_deploy = self._AppsV1Api.patch_namespaced_deployment( name=deploy_name, namespace=deploy_namespace, body=old_deploy ) print("映象地址已經替換完成\n") return new_deploy except Exception as e: print(e)def run(): parser = argparse.ArgumentParser() parser.add_argument('-n', '--name', help="構建的服務名") parser.add_argument('-s', '--namespace', help="要構建的服務所處在的名稱空間") parser.add_argument('-i', '--img', help="此次構建的映象地址") parser.add_argument('-c', '--cluster', help="rancher中當前服務所處的叢集名稱") args = parser.parse_args() if not os.path.exists('../config/' + args.cluster): print("當前叢集名未設定或名稱不正確: {}".format(args.cluster), 'red') exit(-1) else: kubeconfig_file = '../config/' + args.cluster + '/' + 'config.yaml' if os.path.exists(kubeconfig_file): cli = deployServer(kubeconfig_file) cli.deploy_deploy( deploy_namespace=args.namespace, deploy_name=args.name, deploy_img=args.img ) else: print("當前叢集的kubeconfig不存在,請進行配置,位置為{}下的config.yaml.(注意: config.yaml名稱寫死,不需要改到)".format(args.cluster), 'red') exit(-1)if __name__ == '__main__': run()複製程式碼
寫得比較簡單,沒有難懂的地方,關鍵的地方是:
new_deploy = self._AppsV1Api.patch_namespaced_deployment( name=deploy_name, namespace=deploy_namespace, body=old_deploy )複製程式碼
這一句是執行的 patch 操作,把替換好新的映象地址的內容進行 patch。然後就是執行就可以了。
其他這裡有一個需要注意的地方是pipeline里加了一個異常捕獲,如下所示:
post { success { // 如果上面階段執行成功,將把當前目錄刪掉 deleteDir() }}複製程式碼
生命式的 pipeline 和指令碼式的 pipeline 的異常捕獲的寫法是有區別的,宣告式寫法是用的post來進行判斷,比較簡單,可以參考下官方文件
另外還有一個地方使用了並行執行,同時拉了服務的程式碼,和構建指令碼的程式碼,這樣可以提高執行整個流水線的速度,如下所示:
parallel { stage('Pull server code') { steps { script { checkout( [ $class: 'GitSCM', branches: [[name: '${Branch}']], userRemoteConfigs: [[credentialsId: "${git_auth}", url: "${git_addr}"]] ] ) } } } stage('Pull ops code') { steps { script { checkout( [ $class: 'GitSCM', branches: [[name: 'pipeline-0.0.1']], //拉取的構建指令碼的分支 doGenerateSubmoduleConfigurations: false, extensions: [[$class: 'RelativeTargetDirectory', relativeTargetDir: 'DEPLOYJAVA']], //DEPLOYJAVA: 把程式碼存放到此目錄中 userRemoteConfigs: [[credentialsId: 'chenf-o', url: '構建指令碼的倉庫地址']] ] ) } } }}複製程式碼
嗯,情況就是這麼個情況,一個簡簡單單的流水線就完成了,如果想快速使用流水線完成 CICD,可以參考下這篇文章。