1. 程式人生 > 其它 >Kubernetes DevOps: Jenkins Pipeline (流水線)

Kubernetes DevOps: 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,直接在 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·,我們就可以看到如下輸出資訊:

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

如果大家對 Pipeline 語法不是特別熟悉的,可以前往輸入指令碼的下面的連結 Pipeline Syntax 中進行檢視,這裡有很多關於 Pipeline 語法的介紹,也可以自動幫我們生成一些指令碼。

在 Slave 中構建任務

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

node('ydzs-jnlp') {
  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 添加了一個 ydzs-jnlp 這樣的一個label,然後我們儲存,構建之前檢視下 kubernetes 叢集中的 Pod:

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

然後重新觸發立刻構建:

$ kubectl get pods -n kube-ops
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 kube-ops
NAME                       READY     STATUS    RESTARTS   AGE
jenkins-7c85b6f4bd-rfqgv   1/1       Running   4          6d

這也證明我們的 Job 構建完成了,同樣回到 Jenkins 的 Web UI 介面中檢視 Console Output,可以看到如下的資訊:

pipeline demo2 是不是也證明我們當前的任務在跑在上面動態生成的這個 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('ydzs-jnlp') {
    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"
    }
}

現在我們建立一個流水線的作業,直接使用上面的指令碼來構建,同樣可以得到正確的結果:

這裡我們來將一個簡單 golang 程式,部署到 kubernetes 環境中,程式碼連結:https://github.com/cnych/drone-k8s-demo。我們將程式碼推送到我們自己的 GitLab 倉庫上去,地址:http://git.k8s.local/course/devops-demo,這樣讓 Jenkins 和 Gitlab 去進行連線進行 CI/CD。

如果按照之前的示例,我們是不是應該像這樣來編寫 Pipeline 指令碼:

第一步,clone 程式碼 第二步,進行測試,如果測試通過了才繼續下面的任務 第三步,由於 Dockerfile 基本上都是放入原始碼中進行管理的,所以我們這裡就是直接構建 Docker 映象了 第四步,映象打包完成,就應該推送到映象倉庫中吧 第五步,映象推送完成,是不是需要更改 YAML 檔案中的映象 TAG 為這次映象的 TAG 第六步,萬事俱備,只差最後一步,使用 kubectl 命令列工具進行部署了

到這裡我們的整個 CI/CD 的流程是不是就都完成了。我們同樣可以用上面的我們自定義的一個 jnlp 的映象來完成我們的整個構建工作,但是我們這裡的專案是 golang 程式碼的,構建需要相應的環境,如果每次需要特定的環境都需要重新去定製下映象這未免太麻煩了,我們這裡來採用一種更加靈活的方式,自定義 podTemplate。我們可以直接在 Pipeline 中去自定義 Slave Pod 中所需要用到的容器模板,這樣我們需要什麼映象只需要在 Slave Pod Template 中宣告即可,完全不需要去定義一個龐大的 Slave 映象了。

首先去掉 Jenkins 中 kubernetes 外掛中的 Pod Template 的定義,進入頁面 http://jenkins.k8s.local/configureClouds/,刪除下方的 Pod Template -> 儲存。

然後新建一個名為 devops-demo 型別為流水線(Pipeline)的任務:

然後在這裡需要勾選觸發遠端構建的觸發器,其中令牌我們可以隨便寫一個字串,然後記住下面的 URL,將 JENKINS_URL 替換成 Jenkins 的地址,我們這裡的地址就是:http://jenkins.k8s.local/job/devops-demo/build?token=server321

然後在下面的流水線區域我們可以選擇 Pipeline script 然後在下面測試流水線指令碼,我們這裡選擇 Pipeline script from SCM,意思就是從程式碼倉庫中通過 Jenkinsfile 檔案獲取 Pipeline script 指令碼定義,然後選擇 SCM 來源為 Git,在出現的列表中配置上倉庫地址 http://git.k8s.local/course/devops-demo.git,由於我們是在一個 Slave Pod 中去進行構建,所以如果使用 SSH 的方式去訪問 Gitlab 程式碼倉庫的話就需要頻繁的去更新 SSH-KEY,所以我們這裡採用直接使用使用者名稱和密碼的形式來方式:

我們可以看到有一個明顯的錯誤 Could not resolve host: git.k8s.local 提示不能解析我們的 GitLab 域名,這是因為我們的域名都是自定義的,我們可以通過在 CoreDNS 中新增自定義域名解析來解決這個問題:

$ kubectl edit cm coredns -n kube-system
apiVersion: v1
data:
  Corefile: |
    .:53 {
        log
        errors
        health {
          lameduck 5s
        }
        ready
        hosts {  # 新增自定義域名解析
          10.151.30.11 git.k8s.local
          10.151.30.11 jenkins.k8s.local
          10.151.30.11 harbor.k8s.local
          fallthrough
        }
        kubernetes cluster.local in-addr.arpa ip6.arpa {
           pods insecure
           upstream
           fallthrough in-addr.arpa ip6.arpa
        }
        prometheus :9153
        forward . /etc/resolv.conf
        cache 30
        loop
        reload
        loadbalance
    }
kind: ConfigMap
metadata:
  creationTimestamp: "2019-11-08T11:59:49Z"
  name: coredns
  namespace: kube-system
  resourceVersion: "67954425"
  selfLink: /api/v1/namespaces/kube-system/configmaps/coredns
  uid: 21966186-c2d9-467a-b87f-d061c5c9e4d7

修改完成後,隔一小會兒,CoreDNS 就是自動熱載入,我們就可以在叢集內訪問我們自定義的域名了。然後肯定沒有許可權,所以需要配置帳號認證資訊。在 Credentials 區域點選新增按鈕新增我們訪問 Gitlab 的使用者名稱和密碼:

然後需要我們配置用於構建的分支,如果所有的分支我們都想要進行構建的話,只需要將 Branch Specifier 區域留空即可,一般情況下不同的環境對應的分支才需要構建,比如 master、develop、test 等,平時開發的 feature 或者 bugfix 的分支沒必要頻繁構建,我們這裡就只配置 master 和 develop 兩個分支用於構建。

編輯完成後,這個時候 Jenkins 就可以正常訪問到 GitLab 了。

然後前往 Gitlab 中配置專案 devops-demo 的 Webhook,settings -> Webhooks,填寫上面得到的 trigger 地址:

儲存後,如果出現 Url is blocked: Requests to the local network are not allowed 這樣的報警資訊,則需要進入 GitLab Admin -> Settings -> NetWork -> 勾選 Outbound requests,然後重新配置即可。

儲存後,可以直接點選 Test -> Push Event 測試是否可以正常訪問 Webhook 地址,這裡需要注意的是我們需要配置下 Jenkins 的安全配置,否則這裡的觸發器沒許可權訪問 Jenkins,系統管理 -> 全域性安全配置:取消防止跨站點請求偽造,勾選上匿名使用者具有可讀許可權:

儲存後,可以直接點選 Test -> Push Event 測試訪問,正常如果沒有配置過 Jenkins 的安全選項,會出現403錯誤,系統管理 -> 全域性安全配置:勾選匿名使用者具有讀寫許可權:

由於我們這裡的 Jenkins 是比較新的 Jenkins ver. 2.222.3 版本,該版本已經預設取消了 CSRF 的安全配置入口,所以我們需要手動執行一段指令碼來禁用 CSRF 的跨站請求,系統管理 -> 指令碼命令列:執行下圖所示的命令即可。

import jenkins.model.Jenkins

def jenkins = Jenkins.instance

jenkins.setCrumbIssuer(null)

配置完成後,這個時候我們重新去 GitLab 進行測試,出現了 Hook executed successfully: HTTP 201 則證明 Webhook 配置成功了,否則就需要檢查下 Jenkins 的安全配置是否正確了。

由於當前專案中還沒有 Jenkinsfile 檔案,所以觸發過後會構建失敗,接下來我們直接在程式碼倉庫根目錄下面新增 Jenkinsfile 檔案,用於描述流水線構建流程。

首先定義最簡單的流程,要注意這裡和前面的不同之處,這裡我們使用 podTemplate 來定義不同階段使用的的容器,有哪些階段呢?

Clone 程式碼 -> 單元測試 -> Golang 編譯打包 -> Docker 映象構建/推送 -> Kubectl 部署服務。

Clone 程式碼在預設的 Slave 容器中即可;單元測試我們這裡直接忽略,有需要這個階段的同學自己新增上即可;Golang 編譯打包肯定就需要 Golang 的容器了;Docker 映象構建/推送是不是就需要 Docker 環境了;最後的 Kubectl 更新服務是不是就需要一個有 Kubectl 的容器環境了,所以我們這裡就可以很簡單的定義 podTemplate 了,如下定義:

def label = "slave-${UUID.randomUUID().toString()}"

podTemplate(label: label, containers: [
  containerTemplate(name: 'golang', image: 'golang:1.14.2-alpine3.11', command: 'cat', ttyEnabled: true),
  containerTemplate(name: 'docker', image: 'docker:latest', command: 'cat', ttyEnabled: true),
  containerTemplate(name: 'kubectl', image: 'cnych/kubectl', command: 'cat', ttyEnabled: true)
], serviceAccount: 'jenkins', volumes: [
  hostPathVolume(mountPath: '/home/jenkins/.kube', hostPath: '/root/.kube'),
  hostPathVolume(mountPath: '/var/run/docker.sock', hostPath: '/var/run/docker.sock')
]) {
  node(label) {
    def myRepo = checkout scm
    def gitCommit = myRepo.GIT_COMMIT
    def gitBranch = myRepo.GIT_BRANCH

    stage('單元測試') {
      echo "測試階段"
    }
    stage('程式碼編譯打包') {
      container('golang') {
        echo "程式碼編譯打包階段"
      }
    }
    stage('構建 Docker 映象') {
      container('docker') {
        echo "構建 Docker 映象階段"
      }
    }
    stage('執行 Kubectl') {
      container('kubectl') {
        echo "檢視 K8S 叢集 Pod 列表"
        sh "kubectl get pods"
      }
    }
  }
}

直接在 podTemplate 裡面定義每個階段需要用到的容器,volumes 裡面將我們需要用到的 docker.sock 檔案,需要注意的我們使用的 label 標籤是是一個隨機生成的,這樣有一個好處就是有多個任務來的時候就可以同時構建了。正常來說我們還需要將訪問叢集的 kubeconfig 檔案拷貝到 kubectl 容器的 ~/.kube/config 檔案下面去,這樣我們就可以在容器中訪問 Kubernetes 叢集了,但是由於我們構建是在 Slave Pod 中去構建的,Pod 就很有可能每次排程到不同的節點去,這就需要保證每個節點上有 kubeconfig 檔案才能掛載成功,所以這裡我們使用另外一種方式。

通過將 kubeconfig 檔案通過憑證上傳到 Jenkins 中,然後在 Jenkinsfile 中讀取到這個檔案後,拷貝到 kubectl 容器中的 ~/.kube/config 檔案中,這樣同樣就可以正常使用 kubectl 訪問叢集了。在 Jenkins 頁面中新增憑據,選擇 Secret file 型別,然後上傳 kubeconfig 檔案,指定 ID 即可:

然後在 Jenkinsfile 的 kubectl 容器中讀取上面新增的 Secret file 檔案,拷貝到 ~/.kube/config 即可:

stage('執行 Kubectl') {
  container('kubectl') {
    withCredentials([file(credentialsId: 'kubeconfig', variable: 'KUBECONFIG')]) {
      echo "檢視 K8S 叢集 Pod 列表"
      sh "mkdir -p ~/.kube && cp ${KUBECONFIG} ~/.kube/config"
      sh "kubectl get pods"
    }
  }
}

現在我們直接將 Jenkinsfile 檔案提交到 GitLab 程式碼倉庫中,正常來說就可以觸發 Jenkins 的構建了:

$ kubectl get pods -n kube-ops
NAME                                                     READY   STATUS              RESTARTS   AGE
jenkins-68ccff445c-dk24f                                 1/1     Running             0          14h
slave-5dc89808-76f8-418f-ad31-ec34175aa192-5tmwz-2sdm8   0/4     ContainerCreating   0          3s

我們可以看到生成的 slave Pod 包含了4個容器,就是我們在 podTemplate 指定的加上 slave 的映象,執行完成後該 Pod 也會自動銷燬。

Pipeline

接下來我們就來實現具體的流水線。

第一個階段:單元測試,我們可以在這個階段是執行一些單元測試或者靜態程式碼分析的指令碼,我們這裡直接忽略。

第二個階段:程式碼編譯打包,我們可以看到我們是在一個 golang 的容器中來執行的,我們只需要在該容器中獲取到程式碼,然後在程式碼目錄下面執行打包命令即可,如下所示:

stage('程式碼編譯打包') {
  try {
    container('golang') {
      echo "2.程式碼編譯打包階段"
      sh """
        export GOPROXY=https://goproxy.cn
        GOOS=linux GOARCH=amd64 go build -v -o demo-app
        """
    }
  } catch (exc) {
    println "構建失敗 - ${currentBuild.fullDisplayName}"
    throw(exc)
  }
}

第三個階段:構建 Docker 映象,要構建 Docker 映象,就需要提供映象的名稱和 tag,要推送到 Harbor 倉庫,就需要提供登入的使用者名稱和密碼,所以我們這裡使用到了 withCredentials 方法,在裡面可以提供一個credentialsId 為 dockerhub 的認證資訊,如下:

stage('構建 Docker 映象') {
  withCredentials([[$class: 'UsernamePasswordMultiBinding',
    credentialsId: 'docker-auth',
    usernameVariable: 'DOCKER_USER',
    passwordVariable: 'DOCKER_PASSWORD']]) {
      container('docker') {
        echo "3. 構建 Docker 映象階段"
        sh """
          docker login ${registryUrl} -u ${DOCKER_USER} -p ${DOCKER_PASSWORD}
          docker build -t ${image} .
          docker push ${image}
          """
      }
  }
}

其中 ${image}${imageTag} 我們可以在上面定義成全域性變數:

// 獲取 git commit id 作為映象標籤
def imageTag = sh(script: "git rev-parse --short HEAD", returnStdout: true).trim()
// 倉庫地址
def registryUrl = "harbor.k8s.local"
def imageEndpoint = "course/devops-demo"
// 映象
def image = "${registryUrl}/${imageEndpoint}:${imageTag}"

這裡定義的映象名稱為 course/devops-demo,所以需要提前在 Harbor 中新建一個名為 course 的私有專案:

Docker 的使用者名稱和密碼資訊則需要通過憑據來進行新增,進入 jenkins 首頁 -> 左側選單憑據 -> 新增憑據,選擇使用者名稱和密碼型別的,其中 ID 一定要和上面的 credentialsId 的值保持一致:

不過需要注意的是我們這裡使用的是 Docker IN Docker 模式來構建 Docker 映象,通過將宿主機的 docker.sock 檔案掛載到容器中來共享 Docker Daemon,所以我們也需要提前在節點上配置對 Harbor 映象倉庫的信任:

$ vi /etc/docker/daemon.json
{
  "insecure-registries" : [  # 配置忽略 Harobr 映象倉庫的證書校驗
    "harbor.k8s.local"
  ],
  "storage-driver": "overlay2",
  "exec-opts": ["native.cgroupdriver=systemd"],
  "registry-mirrors" : [
    "https://ot2k4d59.mirror.aliyuncs.com/"
  ],
}
$ systemctl daemon-reload
$ systemctl restart docker

配置生效過後我們就可以正常在流水線中去操作 Docker 命令,否則會出現如下所示的錯誤:

現在映象我們都已經推送到了 Harbor 倉庫中去了,接下來就可以部署應用到 Kubernetes 叢集中了,當然可以直接通過 kubectl 工具去操作 YAML 檔案來部署,我們這裡的示例,編寫了一個 Helm Chart 模板,所以我們也可以直接通過 Helm 來進行部署,所以當然就需要一個具有 helm 命令的容器,這裡我們使用 cnych/helm 這個映象,這個映象也非常簡單,就是簡單的將 helm 二進位制檔案下載下來放到 PATH 路徑下面去即可,對應的 Dockerfile 檔案如下所示,大家也可以根據自己的需要來進行定製:

FROM alpine
MAINTAINER cnych <[email protected]>
ARG HELM_VERSION="v3.2.1"
RUN apk add --update ca-certificates \
 && apk add --update -t deps wget git openssl bash \
 && wget https://get.helm.sh/helm-${HELM_VERSION}-linux-amd64.tar.gz \
 && tar -xvf helm-${HELM_VERSION}-linux-amd64.tar.gz \
 && mv linux-amd64/helm /usr/local/bin \
 && apk del --purge deps \
 && rm /var/cache/apk/* \
 && rm -f /helm-${HELM_VERSION}-linux-amd64.tar.gz
ENTRYPOINT ["helm"]
CMD ["help"]

我們這裡使用的是 Helm3 版本,所以要想用 Helm 來部署應用,同樣的需要配置一個 kubeconfig 檔案在容器中,這樣才能訪問到 Kubernetes 叢集。所以我們可以將 執行 Kubectl 的階段做如下更改:

stage('執行 Helm') {
  withCredentials([file(credentialsId: 'kubeconfig', variable: 'KUBECONFIG')]) {
    container('helm') {
      sh "mkdir -p ~/.kube && cp ${KUBECONFIG} ~/.kube/config"
      echo "4.開始 Helm 部署"
      helmDeploy(
          debug       : false,
          name        : "devops-demo",
          chartDir    : "./helm",
          namespace   : "kube-ops",
          valuePath   : "./helm/my-value.yaml",
          imageTag    : "${imageTag}"
      )
      echo "[INFO] Helm 部署應用成功..."
    }
  }
}

其中 helmDeploy 方法可以在全域性中進行定義封裝:

def helmLint(String chartDir) {
    println "校驗 chart 模板"
    sh "helm lint ${chartDir}"
}

def helmDeploy(Map args) {
    if (args.debug) {
        println "Debug 應用"
        sh "helm upgrade --dry-run --debug --install ${args.name} ${args.chartDir} -f ${args.valuePath} --set image.tag=${args.imageTag} --namespace ${args.namespace}"
    } else {
        println "部署應用"
        sh "helm upgrade --install ${args.name} ${args.chartDir} -f ${args.valuePath} --set image.tag=${args.imageTag} --namespace ${args.namespace}"
        echo "應用 ${args.name} 部署成功. 可以使用 helm status ${args.name} 檢視應用狀態"
    }
}

我們在 Chart 模板中定義了一個名為 my-values.yaml 的 Values 檔案,用來覆蓋預設的值,比如這裡我們需要使用 Harbor 私有倉庫的映象,則必然需要定義 imagePullSecrets,所以需要在目標 namespace 下面建立一個 Harbor 登入認證的 Secret 物件:

$ kubectl create secret docker-registry harbor-auth --docker-server=harbor.k8s.local --docker-username=admin --docker-password=Harbor12345 [email protected] --namespace kube-ops
secret/harbor-auth created

然後由於每次我們構建的映象 tag 都會變化,所以我們可以通過 --set 來動態設定。

不過需要記得在上面容器模板中新增 helm 容器:

containerTemplate(name: 'helm', image: 'cnych/helm', command: 'cat', ttyEnabled: true)

對於不同的環境我們可以使用不同的 values 檔案來進行區分,這樣當我們部署的時候可以手動選擇部署到某個環境下面去。

def userInput = input(
  id: 'userInput',
  message: '選擇一個部署環境',
  parameters: [
      [
          $class: 'ChoiceParameterDefinition',
          choices: "Dev\nQA\nProd",
          name: 'Env'
      ]
  ]
)
echo "部署應用到 ${userInput} 環境"
// 選擇不同環境下面的 values 檔案
if (userInput == "Dev") {
    // deploy dev stuff
} else if (userInput == "QA"){
    // deploy qa stuff
} else {
    // deploy prod stuff
}
// 根據 values 檔案再去使用 Helm 進行部署

然後去構建應用的時候,在 Helm 部署階段就會看到 Stage View 介面出現了暫停的情況,需要我們選擇一個環境來進行部署:

選擇完成後再去部署應用。最後我們還可以新增一個 kubectl 容器來檢視應用的相關資源物件:

stage('執行 Kubectl') {
  withCredentials([file(credentialsId: 'kubeconfig', variable: 'KUBECONFIG')]) {
    container('kubectl') {
      sh "mkdir -p ~/.kube && cp ${KUBECONFIG} ~/.kube/config"
      echo "5.檢視應用"
      sh "kubectl get all -n kube-ops -l app=devops-demo"
    }
  }
}

有時候我們部署的應用即使有很多測試,但是也難免會出現一些錯誤,這個時候如果我們是部署到線上的話,就需要要求能夠立即進行回滾,這裡我們同樣可以使用 Helm 來非常方便的操作,新增如下一個回滾的階段:

stage('快速回滾?') {
  withCredentials([file(credentialsId: 'kubeconfig', variable: 'KUBECONFIG')]) {
    container('helm') {
      sh "mkdir -p ~/.kube && cp ${KUBECONFIG} ~/.kube/config"
      def userInput = input(
        id: 'userInput',
        message: '是否需要快速回滾?',
        parameters: [
            [
                $class: 'ChoiceParameterDefinition',
                choices: "Y\nN",
                name: '回滾?'
            ]
        ]
      )
      if (userInput == "Y") {
        sh "helm rollback devops-demo --namespace kube-ops"
      }
    }
  }
}

最後一條完整的流水線就完成了.

我們可以在本地加上應用域名 devops-demo.k8s.local 的對映就可以訪問應用了:

$ curl http://devops-demo.k8s.local
{"msg":"Hello DevOps On Kubernetes"}

完整的 Jenkinsfile 檔案如下所示:

def label = "slave-${UUID.randomUUID().toString()}"

def helmLint(String chartDir) {
    println "校驗 chart 模板"
    sh "helm lint ${chartDir}"
}

def helmDeploy(Map args) {
    if (args.debug) {
        println "Debug 應用"
        sh "helm upgrade --dry-run --debug --install ${args.name} ${args.chartDir} -f ${args.valuePath} --set image.tag=${args.imageTag} --namespace ${args.namespace}"
    } else {
        println "部署應用"
        sh "helm upgrade --install ${args.name} ${args.chartDir} -f ${args.valuePath} --set image.tag=${args.imageTag} --namespace ${args.namespace}"
        echo "應用 ${args.name} 部署成功. 可以使用 helm status ${args.name} 檢視應用狀態"
    }
}

podTemplate(label: label, containers: [
  containerTemplate(name: 'golang', image: 'golang:1.14.2-alpine3.11', command: 'cat', ttyEnabled: true),
  containerTemplate(name: 'docker', image: 'docker:latest', command: 'cat', ttyEnabled: true),
  containerTemplate(name: 'helm', image: 'cnych/helm', command: 'cat', ttyEnabled: true),
  containerTemplate(name: 'kubectl', image: 'cnych/kubectl', command: 'cat', ttyEnabled: true)
], serviceAccount: 'jenkins', volumes: [
  hostPathVolume(mountPath: '/var/run/docker.sock', hostPath: '/var/run/docker.sock')
]) {
  node(label) {
    def myRepo = checkout scm
    // 獲取 git commit id 作為映象標籤
    def imageTag = sh(script: "git rev-parse --short HEAD", returnStdout: true).trim()
    // 倉庫地址
    def registryUrl = "harbor.k8s.local"
    def imageEndpoint = "course/devops-demo"
    // 映象
    def image = "${registryUrl}/${imageEndpoint}:${imageTag}"

    stage('單元測試') {
      echo "測試階段"
    }
    stage('程式碼編譯打包') {
      try {
        container('golang') {
          echo "2.程式碼編譯打包階段"
          sh """
            export GOPROXY=https://goproxy.cn
            GOOS=linux GOARCH=amd64 go build -v -o demo-app
            """
        }
      } catch (exc) {
        println "構建失敗 - ${currentBuild.fullDisplayName}"
        throw(exc)
      }
    }
    stage('構建 Docker 映象') {
      withCredentials([[$class: 'UsernamePasswordMultiBinding',
        credentialsId: 'docker-auth',
        usernameVariable: 'DOCKER_USER',
        passwordVariable: 'DOCKER_PASSWORD']]) {
          container('docker') {
            echo "3. 構建 Docker 映象階段"
            sh """
              cat /etc/resolv.conf
              docker login ${registryUrl} -u ${DOCKER_USER} -p ${DOCKER_PASSWORD}
              docker build -t ${image} .
              docker push ${image}
              """
          }
      }
    }
    stage('執行 Helm') {
      withCredentials([file(credentialsId: 'kubeconfig', variable: 'KUBECONFIG')]) {
        container('helm') {
          sh "mkdir -p ~/.kube && cp ${KUBECONFIG} ~/.kube/config"
          echo "4.開始 Helm 部署"
          def userInput = input(
            id: 'userInput',
            message: '選擇一個部署環境',
            parameters: [
                [
                    $class: 'ChoiceParameterDefinition',
                    choices: "Dev\nQA\nProd",
                    name: 'Env'
                ]
            ]
          )
          echo "部署應用到 ${userInput} 環境"
          // 選擇不同環境下面的 values 檔案
          if (userInput == "Dev") {
              // deploy dev stuff
          } else if (userInput == "QA"){
              // deploy qa stuff
          } else {
              // deploy prod stuff
          }
          helmDeploy(
              debug       : false,
              name        : "devops-demo",
              chartDir    : "./helm",
              namespace   : "kube-ops",
              valuePath   : "./helm/my-values.yaml",
              imageTag    : "${imageTag}"
          )
        }
      }
    }
    stage('執行 Kubectl') {
      withCredentials([file(credentialsId: 'kubeconfig', variable: 'KUBECONFIG')]) {
        container('kubectl') {
          sh "mkdir -p ~/.kube && cp ${KUBECONFIG} ~/.kube/config"
          echo "5.檢視應用"
          sh "kubectl get all -n kube-ops -l app=devops-demo"
        }
      }
    }
    stage('快速回滾?') {
      withCredentials([file(credentialsId: 'kubeconfig', variable: 'KUBECONFIG')]) {
        container('helm') {
          sh "mkdir -p ~/.kube && cp ${KUBECONFIG} ~/.kube/config"
          def userInput = input(
            id: 'userInput',
            message: '是否需要快速回滾?',
            parameters: [
                [
                    $class: 'ChoiceParameterDefinition',
                    choices: "Y\nN",
                    name: '回滾?'
                ]
            ]
          )
          if (userInput == "Y") {
            sh "helm rollback devops-demo --namespace kube-ops"
          }
        }
      }
    }
  }
}