使用 Jenkins Pipeline 流水線部署 Kubernetes 應用
要實現在 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 界面中輸入腳本運行。
-
新建任務:在 Web UI 中點擊
新建任務
-> 輸入名稱:pipeline-demo -> 選擇下面的流水線
-> 點擊確定
-
配置:在最下方的 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" } }
-
構建:點擊左側區域的
立即構建
,可以看到 Job 開始構建了
隔一會兒,構建完成,可以點擊左側區域的 Console Output
,我們就可以看到如下輸出信息:
console output 我們可以看到上面我們 Pipeline 腳本中的 4 條輸出語句都打印出來了,證明是符合我們的預期的。
如果大家對 Pipeline 語法不是特別熟悉的,可以前往輸入腳本的下面的鏈接 流水線語法 中進行查看,這裏有很多關於 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-587b78f5cd-47hf8 1/1 Running 2 4d21h
......
然後重新觸發立刻構建:
$ kubectl get pods -n kube-ops
NAME READY STATUS RESTARTS AGE
jenkins-587b78f5cd-47hf8 1/1 Running 2 4d21h
jenkins-agent-6gw0w 1/1 Running 0 4s
我們發現多了一個名叫 jenkins-agent-6gw0w
的 Pod 正在運行,隔一會兒這個 Pod 就不再了。這也證明我們的 Job 構建完成了,同樣回到 Jenkins 的 Web UI 界面中查看 Console Output,可以看到如下的信息:
pipeline demo#2
是不是也證明我們當前的任務在跑在上面動態生成的這個 Pod 中,也符合我們的預期。我們回到 Job 的主界面,也可以看到大家可能比較熟悉的 階段視圖
界面:
部署 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 鏡像了。
這裏我們需要使用到 gitlab
的插件,用於 Gitab 側代碼變動後觸發 Jenkins 的構建任務:
然後新建一個名爲 devops-demo
類型爲流水線
的任務,在 構建觸發器
區域選擇 Build when a change is pushed to GitLab
,後面的 http://jenkins.k8s.local/project/devops-demo
是我們需要在 Gitlab 上配的 Webhook 地址:
其中Comment (regex) for triggering a build
是說在 git 倉庫,發送包含 jenkins build
這樣的關鍵字的時候會觸發執行此 build 構建。然後點擊下面的高級
可以生成 token。這裏的 url 和 token 是 jenkins 的 api,可以提供給 GtiLab 使用,在代碼合併 / 提交 commit/push 代碼等操作時,通知 Jenkins 執行 build 操作。
注: 複製出 URL 和 Token,我們後面配置 Gitlab 的 Webhook 會用到。
然後在下面的流水線區域我們可以選擇 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 { # 添加自定義域名解析
192.168.31.30 git.k8s.local
192.168.31.30 jenkins.k8s.local
192.168.31.30 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
......
修改完成後,隔一小會兒,CoreDNS 就會自動熱加載,我們就可以在集羣內訪問我們自定義的域名了。然後肯定沒有權限,所以需要配置帳號認證信息。
在 Credentials
區域點擊添加按鈕添加我們訪問 Gitlab 的用戶名和密碼:
然後需要我們配置用於構建的分支,如果所有的分支我們都想要進行構建的話,只需要將 Branch Specifier
區域留空即可,一般情況下不同的環境對應的分支才需要構建,比如 master、dev、test 等,平時開發的 feature 或者 bugfix 的分支沒必要頻繁構建,我們這裏就只配置 master 個分支用於構建。
最後點擊保存,至此,Jenkins 的持續集成配置好了,還需要配置 Gitlab 的 Webhook,用於代碼提交通知 Jenkins。前往 Gitlab 中配置項目 devops-demo 的 Webhook,settings -> Webhooks,填寫上面得到的 trigger 地址:
我們這裏都是自定義的域名,也沒有配置 https 服務,所以記得取消配置下面的 啓用SSL驗證
。
保存後,如果出現 Urlis blocked: Requests to the local network are not allowed
這樣的報警信息,則需要進入 GitLab Admin -> 設置 -> 網絡
-> 勾選 外發請求
,然後保存配置。
現在就可以正常保存了,可以直接點擊 測試 -> Push Event
測試是否可以正常訪問 Webhook 地址,出現了 Hook executed successfully: HTTP 200
則證明 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-587b78f5cd-47hf8 1/1 Running 2 5d
slave-e3e34e24-721a-4c32-a83e-19033e244b9d-2h6wd-tjkkf 0/4 ContainerCreating 0 38s
我們可以看到生成的 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 <icnych@gmail.com>
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 --docker-email=admin@admin.com --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"
}
}
}
}
}
}
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/xN7_hdX6X58rLjJAfTvY_w