基於 Jenkins2.0 的 CI/CD (二)

Jenkins Pipeline 介紹

要實現在 Jenkins 中的構建工作,可以有多種方式,我們這裏採用比較常用的 Pipeline 這種方式。Pipeline,簡單來說,就是一套運行在 Jenkins 上的工作流框架,將原來獨立運行於單個或者多個節點的任務連接起來,實現單個任務難以完成的複雜流程編排和可視化的工作。

Jenkins Pipeline 有幾個核心概念:

  • Node:節點,一個 Node 就是一個 Jenkins 節點,Master 或者 Agent,是執行 Step 的具體運行環境,比如我們之前動態運行的 Jenkins Slave 就是一個 Node 節點

  • Stage:階段,一個 Pipeline 可以劃分爲若干個 Stage,每個 Stage 代表一組操作,比如:Build、Test、Deploy,Stage 是一個邏輯分組的概念,可以跨多個 Node

  • Step:步驟,Step 是最基本的操作單元,可以是打印一句話,也可以是構建一個 Docker 鏡像,由各類 Jenkins 插件提供,比如命令:sh ‘make’,就相當於我們平時 shell 終端中執行 make 命令一樣。
    那麼我們如何創建 Jenkins Pipline 呢?

  • Pipeline 腳本是由 Groovy 語言實現的,但是我們沒必要單獨去學習 Groovy,當然你會的話最好

  • Pipeline 支持兩種語法:Declarative(聲明式)和 Scripted Pipeline(腳本式)語法

  • Pipeline 也有兩種創建方法:可以直接在 Jenkins 的 Web UI 界面中輸入腳本;也可以通過創建一個 Jenkinsfile 腳本文件放入項目源碼庫中

  • 一般我們都推薦在 Jenkins 中直接從源代碼控制(SCMD)中直接載入 Jenkinsfile Pipeline 這種方法

創建一個簡單的 Pipeline

快速創建一個簡單的 Pipeline,直接在 Jenkins 的 Web UI 界面中輸入腳本運行。

新建 Job:在 Web UI 中點擊 New Item -> 輸入名稱:pipeline-demo -> 選擇下面的 Pipeline -> 點擊 OK
配置:在最下方的 Pipeline 區域輸入如下 Script 腳本,然後點擊保存。

node {
  stage('Clone') {
    echo "1.Clone Stage"
  }
  stage('Test') {
    echo "2.Test Stage"
  }
  stage('Build') {
    echo "3.Build Stage"
  }
  stage('Deploy') {
    echo "4. Deploy Stage"
  }
}
  • 構建:點擊左側區域的 Build Now,可以看到 Job 開始構建了
    隔一會兒,構建完成,可以點擊左側區域的 Console Output,我們就可以看到如下輸出信息:
    在這裏插入圖片描述

我們可以看到上面我們 Pipeline 腳本中的4條輸出語句都打印出來了,證明是符合我們的預期的。

在 Slave 中構建任務

上面我們創建了一個簡單的 Pipeline 任務,但是我們可以看到這個任務並沒有在 Jenkins 的 Slave 中運行,那麼如何讓我們的任務跑在 Slave 中呢?還記得上節課我們在添加 Slave Pod 的時候,一定要記住添加的 label 嗎?沒錯,我們就需要用到這個 label,我們重新編輯上面創建的 Pipeline 腳本,給 node 添加一個 label 屬性,如下:

node('jenkins-slave') {
    stage('Clone') {
      echo "1.Clone Stage"
    }
    stage('Test') {
      echo "2.Test Stage"
    }
    stage('Build') {
      echo "3.Build Stage"
    }
    stage('Deploy') {
      echo "4. Deploy Stage"
    }
}

我們這裏只是給 node 添加了一個 jenkins-slave 這樣的一個label,然後我們保存,構建之前查看下 kubernetes 集羣中的 Pod:

$ kubectl get pods -n jenkins
NAME                       READY     STATUS              RESTARTS   AGE
jenkins-7c85b6f4bd-rfqgv   1/1       Running             4          6d

然後重新觸發立刻構建:

$ kubectl get pods -n jenkins
NAME                       READY     STATUS    RESTARTS   AGE
jenkins-7c85b6f4bd-rfqgv   1/1       Running   4          6d
jnlp-0hrrz                 1/1       Running   0          23s

我們發現多了一個名叫jnlp-0hrrz的 Pod 正在運行,隔一會兒這個 Pod 就不再了:

$ kubectl get pods -n jenkins
NAME                       READY     STATUS    RESTARTS   AGE
jenkins-7c85b6f4bd-rfqgv   1/1       Running   4          6d

這也證明我們的 Job 構建完成了,同樣回到 Jenkins 的 Web UI 界面中查看 Console Output,可以看到如下的信息:

在這裏插入圖片描述

是不是也證明我們當前的任務在跑在上面動態生成的這個 Pod 中,也符合我們的預期。我們回到 Job 的主界面,也可以看到大家可能比較熟悉的 Stage View 界面:

在這裏插入圖片描述

部署 Kubernetes 應用

已經知道了如何在 Jenkins Slave 中構建任務了,那麼如何來部署一個原生的 Kubernetes 應用呢? 要部署 Kubernetes 應用,我們就得對我們之前部署應用的流程要非常熟悉纔行,我們之前的流程是怎樣的:

  • 編寫代碼
  • 測試
  • 編寫 Dockerfile
  • 構建打包 Docker 鏡像
  • 推送 Docker 鏡像到倉庫
  • 編寫 Kubernetes YAML 文件
  • 更改 YAML 文件中 Docker 鏡像 TAG
  • 利用 kubectl 工具部署應用

我們之前在 Kubernetes 環境中部署一個原生應用的流程應該基本上是上面這些流程吧?現在我們就需要把上面這些流程放入 Jenkins 中來自動幫我們完成(當然編碼除外),從測試到更新 YAML 文件屬於 CI 流程,後面部署屬於 CD 的流程。如果按照我們上面的示例,我們現在要來編寫一個 Pipeline 的腳本,應該怎麼編寫呢?

node('jenkins-slave') {
    stage('Clone') {
      echo "1.Clone Stage"
    }
    stage('Test') {
      echo "2.Test Stage"
    }
    stage('Build') {
      echo "3.Build Docker Image Stage"
    }
    stage('Push') {
      echo "4.Push Docker Image Stage"
    }
    stage('YAML') {
      echo "5. Change YAML File Stage"
    }
    stage('Deploy') {
      echo "6. Deploy Stage"
    }
}

編寫 Pipeline 腳本:

  • 第一步,clone 代碼,這個沒得說吧
  • 第二步,進行測試,如果測試通過了才繼續下面的任務
  • 第三步,由於 Dockerfile 基本上都是放入源碼中進行管理的,所以我們這裏就是直接構建 Docker 鏡像了
  • 第四步,鏡像打包完成,就應該推送到鏡像倉庫中吧
  • 第五步,鏡像推送完成,是不是需要更改 YAML 文件中的鏡像 TAG 爲這次鏡像的 TAG
  • 第六步,萬事俱備,只差最後一步,使用 kubectl 命令行工具進行部署了

到這裏我們的整個 CI/CD 的流程是不是就都完成了。

接下來我們就來對每一步具體要做的事情進行詳細描述就行了:

第一步,Clone 代碼

stage('Clone') {
    echo "1.Clone Stage"
    git url: "https://github.com/cnych/jenkins-demo.git"
}

第二步,測試

由於編譯跳過Test,忽略該步驟即可

第三步,構建鏡像

我們平時構建的時候是不是都是直接使用docker build命令進行構建就行了,那麼這個地方呢?我們上節課給大家提供的 Slave Pod 的鏡像裏面是不是採用的 Docker In Docker 的方式,也就是說我們也可以直接在 Slave 中使用 docker build 命令,所以我們這裏直接使用 sh 直接執行 docker build 命令即可,但是鏡像的 tag 呢?如果我們使用鏡像 tag,則每次都是 latest 的 tag,這對於以後的排查或者回滾之類的工作會帶來很大麻煩,我們這裏採用和git commit的記錄爲鏡像的 tag,這裏有一個好處就是鏡像的 tag 可以和 git 提交記錄對應起來,也方便日後對應查看。但是由於這個 tag 不只是我們這一個 stage 需要使用,下一個推送鏡像是不是也需要,所以這裏我們把這個 tag 編寫成一個公共的參數,把它放在 Clone 這個 stage 中,這樣一來我們前兩個 stage 就變成了下面這個樣子

stage('Clone') {
    echo "1.Clone Stage"
    git url: "https://github.com/cnych/jenkins-demo.git"
    script {
        build_tag = sh(returnStdout: true, script: 'git rev-parse --short HEAD').trim()  //定義全局變量
    }
}
stage('Build') {
    echo "3.Build Docker Image Stage"
    sh "docker build -t cnych/jenkins-demo:${build_tag} ."
}

第四步,推送鏡像

stage('Push') {
    echo "4.Push Docker Image Stage"
    sh "docker login -u cnych -p xxxxx"
    sh "docker push cnych/jenkins-demo:${build_tag}" // 鏡像倉庫自行搭建,比如nexus,harbor
}

添加倉庫祕鑰

在首頁點擊 Credentials -> Stores scoped to Jenkins 下面的 Jenkins -> Global credentials (unrestricted) -> 左側的 Add Credentials:添加一個 Username with password 類型的認證信息,如下:
在這裏插入圖片描述

輸入 docker hub 的用戶名和密碼,ID 部分我們輸入dockerHub,注意,這個值非常重要,在後面 Pipeline 的腳本中我們需要使用到這個 ID 值。

有了上面的 docker hub 的用戶名和密碼的認證信息,現在我們可以在 Pipeline 中使用這裏的用戶名和密碼了:

stage('Push') {
    echo "4.Push Docker Image Stage"
    withCredentials([usernamePassword(credentialsId: 'dockerHub', passwordVariable: 'dockerHubPassword', usernameVariable: 'dockerHubUser')]) {
        sh "docker login -u ${dockerHubUser} -p ${dockerHubPassword}"
        sh "docker push cnych/jenkins-demo:${build_tag}"
    }
}

注意我們這裏在 stage 中使用了一個新的函數withCredentials,其中有一個 credentialsId 值就是我們剛剛創建的 ID 值,而對應的用戶名變量就是 ID 值加上 User,密碼變量就是 ID 值加上 Password,然後我們就可以在腳本中直接使用這裏兩個變量值來直接替換掉之前的登錄 docker hub 的用戶名和密碼,現在是不是就很安全了,我只是傳遞進去了兩個變量而已,別人並不知道我的真正用戶名和密碼,只有我們自己的 Jenkins 平臺上添加的才知道。

第五步,更改 YAML

上面我們已經完成了鏡像的打包、推送的工作,接下來我們是不是應該更新 Kubernetes 系統中應用的鏡像版本了,當然爲了方便維護,我們都是用 YAML 文件的形式來編寫應用部署規則,比如我們這裏的 YAML 文件:(k8s.yaml)

apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: jenkins-demo
spec:
  template:
    metadata:
      labels:
        app: jenkins-demo
    spec:
      containers:
      - image: cnych/jenkins-demo:<BUILD_TAG>
        imagePullPolicy: IfNotPresent
        name: jenkins-demo
        env:
        - name: branch
          value: <BRANCH_NAME>

sed 替換分支變量<BRANCH_NAME>和鏡像版本<BUILD_TAG>

stage('YAML') {
    echo "5. Change YAML File Stage"
    sh "sed -i 's/<BUILD_TAG>/${build_tag}/' k8s.yaml"
    sh "sed -i 's/<BRANCH_NAME>/${env.BRANCH_NAME}/' k8s.yaml"
}

上面的 sed 命令就是將 k8s.yaml 文件中的 標識給替換成變量 build_tag 的值

第六步,部署

Kubernetes 應用的 YAML 文件已經更改完成了,之前我們手動的環境下,是不是直接使用 kubectl apply 命令就可以直接更新應用了啊?當然我們這裏只是寫入到了 Pipeline 裏面,思路都是一樣的:

stage('Deploy') {
    echo "6. Deploy Stage"
    sh "kubectl apply -f k8s.yaml"
}

這樣到這裏我們的整個流程就算完成了。

人工確認

直接就發佈到線上環境去還是挺少見的,所以我們需要增加人工確認的環節,一般都是在 CD 的環節才需要人工干預,比如我們這裏的最後兩步,我們就可以在前面加上確認,比如:

stage('YAML') {
    echo "5. Change YAML File Stage"
    def userInput = input(
        id: 'userInput',
        message: 'Choose a deploy environment',
        parameters: [
            [
                $class: 'ChoiceParameterDefinition',
                choices: "Dev\nQA\nProd",
                name: 'Env'
            ]
        ]
    )
    echo "This is a deploy step to ${userInput.Env}"
    sh "sed -i 's/<BUILD_TAG>/${build_tag}/' k8s.yaml"
    sh "sed -i 's/<BRANCH_NAME>/${env.BRANCH_NAME}/' k8s.yaml"
}

我們這裏使用了 input 關鍵字,裏面使用一個 Choice 的列表來讓用戶進行選擇,然後在我們選擇了部署環境後,我們當然也可以針對不同的環境再做一些操作,比如可以給不同環境的 YAML 文件部署到不同的 namespace 下面去,增加不同的標籤等等操作:

stage('Deploy') {
    echo "6. Deploy Stage"
    if (userInput.Env == "Dev") {
      // deploy dev stuff
    } else if (userInput.Env == "QA"){
      // deploy qa stuff
    } else {
      // deploy prod stuff
    }
    sh "kubectl apply -f k8s.yaml"
}

由於這一步也屬於部署的範疇,所以我們可以將最後兩步都合併成一步,我們最終的 Pipeline 腳本如下:

node('jenkins-slave') {
    stage('Clone') {
        echo "1.Clone Stage"
        git url: "https://github.com/cnych/jenkins-demo.git"
        script {
            build_tag = sh(returnStdout: true, script: 'git rev-parse --short HEAD').trim()
        }
    }
    stage('Test') {
      echo "2.Test Stage"
    }
    stage('Build') {
        echo "3.Build Docker Image Stage"
        sh "docker build -t cnych/jenkins-demo:${build_tag} ."
    }
    stage('Push') {
        echo "4.Push Docker Image Stage"
        withCredentials([usernamePassword(credentialsId: 'dockerHub', passwordVariable: 'dockerHubPassword', usernameVariable: 'dockerHubUser')]) {
            sh "docker login -u ${dockerHubUser} -p ${dockerHubPassword}"
            sh "docker push cnych/jenkins-demo:${build_tag}"
        }
    }
    stage('Deploy') {
        echo "5. Deploy Stage"
        def userInput = input(
            id: 'userInput',
            message: 'Choose a deploy environment',
            parameters: [
                [
                    $class: 'ChoiceParameterDefinition',
                    choices: "Dev\nQA\nProd",
                    name: 'Env'
                ]
            ]
        )
        echo "This is a deploy step to ${userInput}"
        sh "sed -i 's/<BUILD_TAG>/${build_tag}/' k8s.yaml"
        sh "sed -i 's/<BRANCH_NAME>/${env.BRANCH_NAME}/' k8s.yaml"
        if (userInput == "Dev") {
            // deploy dev stuff
        } else if (userInput == "QA"){
            // deploy qa stuff
        } else {
            // deploy prod stuff
        }
        sh "kubectl apply -f k8s.yaml"
    }
}

現在我們在 Jenkins Web UI 中重新配置 jenkins-demo 這個任務,將上面的腳本粘貼到 Script 區域,重新保存,然後點擊左側的 Build Now,觸發構建,然後過一會兒我們就可以看到 Stage View 界面出現了暫停的情況

在這裏插入圖片描述

上面 Deploy 階段加入了人工確認的步驟,所以這個時候構建暫停了,需要我們人爲的確認下,比如我們這裏選擇 QA,然後點擊 Proceed,就可以繼續往下走了,然後構建就成功了,我們在 Stage View 的 Deploy 這個階段可以看到如下的一些日誌信息:

在這裏插入圖片描述

打印出來了 QA,和我們剛剛的選擇是一致的,現在我們去 Kubernetes 集羣中觀察下部署的應用:

$ kubectl get deployment -n jenkins
NAME           DESIRED   CURRENT   UP-TO-DATE   AVAILABLE   AGE
jenkins        1         1         1            1           7d
jenkins-demo   1         1         1            0           1m
$ kubectl get pods -n jenkins
NAME                           READY     STATUS      RESTARTS   AGE
jenkins-7c85b6f4bd-rfqgv       1/1       Running     4          7d
jenkins-demo-f6f4f646b-2zdrq   0/1       Completed   4          1m
$ kubectl logs jenkins-demo-f6f4f646b-2zdrq -n jenkins
Hello, Kubernetes!I'm from Jenkins CI!

應用已經正確的部署到了 Kubernetes 的集羣環境中了。

Jenkinsfile(擴展)

在實際的工作實踐中,我們更多的是將 Pipeline 腳本寫入到 Jenkinsfile 文件中,然後和代碼一起提交到代碼倉庫中進行版本管理。現在我們將上面的 Pipeline 腳本拷貝到一個 Jenkinsfile 中,將該文件放入上面的 git 倉庫中,但是要注意的是,現在既然我們已經在 git 倉庫中了,是不是就不需要 git clone 這一步驟了,所以我們需要將第一步 Clone 操作中的 git clone 這一步去掉,可以參考:https://github.com/cnych/jenkins-demo/

然後我們更改上面的 jenkins-demo 這個任務,點擊 Configure -> 最下方的 Pipeline 區域 -> 將之前的 Pipeline Script 更改成 Pipeline Script from SCM,然後根據我們的實際情況填寫上對應的倉庫配置,要注意 Jenkinsfile 腳本路徑:

在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章