使用 Jenkins 與 KubeVela 實(shí)現(xiàn)應(yīng)用的持續(xù)交付
KubeVela 打通了應(yīng)用與基礎(chǔ)設(shè)施之間的交付管控的壁壘,相較于原生的 Kubernetes 對(duì)象,KubeVela 的 Application 更好地簡化抽象了開發(fā)者需要關(guān)心的配置,將復(fù)雜的基礎(chǔ)設(shè)施能力及編排細(xì)節(jié)留給了平臺(tái)工程師。而 KubeVela 的 apiserver 則是進(jìn)一步為開發(fā)者提供了使用 HTTP Request 直接操縱 Application 的途徑,使得開發(fā)者即使沒有 Kubernetes 的使用經(jīng)驗(yàn)與集群訪問權(quán)限也可以輕松部署自己的應(yīng)用。
接下來我們就以 Jenkins 為基礎(chǔ),結(jié)合 KubeVela 來實(shí)現(xiàn)一個(gè)簡單的應(yīng)用持續(xù)交付的流程。
要實(shí)現(xiàn)一個(gè)簡單的應(yīng)用持續(xù)交付,我們需要做如下幾件事情:
- 需要一個(gè) git 倉庫來存放應(yīng)用程序代碼、測(cè)試代碼,以及描述 KubeVela Application 的 YAML 文件。
- 需要一個(gè)持續(xù)集成的工具幫你自動(dòng)化完成程序代碼的測(cè)試,并打包成鏡像上傳到倉庫中。
- 需要在 Kubernetes 集群上安裝 KubeVela 并啟用 apiserver 功能。
我們這里的演示 Demo 采用 Github 作為 git 倉庫,Jenkins 作為 CI 工具,DockerHub 作為鏡像倉庫。應(yīng)用程序以一個(gè)簡單的 Golang HTTP Server 為例,整個(gè)持續(xù)交付的流程如下。
交付流程
從整個(gè)流程可以看出開發(fā)者只需要關(guān)心應(yīng)用的開發(fā)并使用 Git 進(jìn)行代碼版本的維護(hù),即可自動(dòng)走完測(cè)試流程并部署應(yīng)用到 Kubernetes 集群中。
關(guān)于 Jenkins 在 Kubernetes 集群中的安裝配置前面我們已經(jīng)介紹過了,這里我們就不再贅述。
應(yīng)用配置
這里我們采用了 Github 作為代碼倉庫,倉庫地址為 https://github.com/cnych/KubeVela-demo-CICD-app,當(dāng)然也可以根據(jù)各自的需求與喜好,使用其他代碼倉庫,如 Gitlab。為了 Jenkins 能夠獲取到 GitHub 中的更新,并將流水線的運(yùn)行狀態(tài)反饋回 GitHub,需要在 GitHub 中完成以下兩步操作。
配置 Personal Access Token。注意將 repo:status 勾選,以獲得向 GitHub 推送 Commit 狀態(tài)的權(quán)限,將生成的 Token 復(fù)制下來,下面會(huì)用到。
Personal Access Token
然后在 Jenkins 的 Credential 中加入 Secret Text 類型的 Credential 并將上述的 GitHub 的 Personal Access Token 填入。
jenkins-secret-text
接下來到 Jenkins 的 Dashboard > Manage Jenkins > Configure System > GitHub 中點(diǎn)擊 Add GitHub Server 并將剛才創(chuàng)建的 Credential 填入。完成后可以點(diǎn)擊 Test connection 來驗(yàn)證配置是否正確。
Add GitHub Server
由于我們這里的 Jenkins 位于本地環(huán)境,要讓 GitHub 通過 Webhook 來觸發(fā) Jenkins,我們需要提供一個(gè)可訪問的地址,這里我們可以使用 ngrok 來實(shí)現(xiàn),首先前往 https://dashboard.ngrok.com 注冊(cè)一個(gè)賬號(hào),將 Authtoken 和 APIKEY 記錄下來。
export NGROK_AUTHTOKEN=<your-ngrok-authtoken>
export NGROK_API_KEY=<your-ngrok-apikey>
然后我們可以在本地 Kubernetes 集群中安裝 ngrok ingress controller:
helm repo add ngrok https://ngrok.github.io/kubernetes-ingress-controller
# 使用下面命令安裝 ngrok ingress controller
helm install ngrok-ingress-controller ngrok/kubernetes-ingress-controller \
--namespace ngrok-ingress-controller \
--create-namespace \
--set credentials.apiKey=$NGROK_API_KEY \
--set credentials.authtoken=$NGROK_AUTHTOKEN
安裝完成后為 Jenkins 創(chuàng)建一個(gè) ngrok 的 ingress 路由:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: jenkins-ngrok
namespace: kube-ops
spec:
ingressClassName: ngrok
rules:
- host: prompt-adjusted-sculpin.ngrok-free.app
http:
paths:
- backend:
service:
name: jenkins
port:
name: web
path: /
pathType: Prefix
上面的 host 域名是 ngrok 為我們分配的,你可以在 ngrok 的控制臺(tái)中手動(dòng)創(chuàng)建,應(yīng)用上面的 ingress 對(duì)象后我們就可以通過 ngrok 為我們分配的域名來訪問 Jenkins 了。
ngrok jenkins
接下來我們就可以在 GitHub 的代碼倉庫的設(shè)定里添加 Webhook,將 Jenkins 的地址對(duì)應(yīng)的 Webhook 地址填入 <ngrok domain>/github-webhook/,這樣該代碼倉庫的所有 Push 事件推送到 Jenkins 中。
github webhook
編寫應(yīng)用
我們這里采用的應(yīng)用是一個(gè)基于 Golang 語言編寫的簡單的 HTTP Server。在代碼中,聲明了一個(gè)名叫 VERSION 的常量,并在訪問該服務(wù)時(shí)打印出來。同時(shí)還附帶一個(gè)簡單的測(cè)試,用來校驗(yàn) VERSION 的格式是否符合標(biāo)準(zhǔn)。
// main.go
package main
import (
"fmt"
"net/http"
)
const VERSION = "0.1.0-v1alpha1"
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
_, _ = fmt.Fprintf(w, "Version: %s\n", VERSION)
})
if err := http.ListenAndServe(":8088", nil); err != nil {
println(err.Error())
}
}
測(cè)試代碼如下所示:
// main_test.go
package main
import (
"regexp"
"testing"
)
const verRegex string = `^v?([0-9]+)(\.[0-9]+)?(\.[0-9]+)?` +
`(-([0-9A-Za-z\-]+(\.[0-9A-Za-z\-]+)*))?` +
`(\+([0-9A-Za-z\-]+(\.[0-9A-Za-z\-]+)*))?$`
func TestVersion(t *testing.T) {
if ok, _ := regexp.MatchString(verRegex, VERSION); !ok {
t.Fatalf("invalid version: %s", VERSION)
}
}
在應(yīng)用交付時(shí)需要將 Golang 服務(wù)打包成鏡像并以 KubeVela Application 的形式發(fā)布到 Kubernetes 集群中,因此在代碼倉庫中還包含 Dockerfile 文件,用來描述鏡像的打包方式。
# Dockerfile
FROM golang:1.13-rc-alpine3.10 as builder
WORKDIR /app
COPY main.go .
RUN go build -o kubevela-demo-cicd-app main.go
FROM alpine:3.10
WORKDIR /app
COPY --from=builder /app/kubevela-demo-cicd-app /app/kubevela-demo-cicd-app
ENTRYPOINT ./kubevela-demo-cicd-app
EXPOSE 8088
配置 CI 流水線
在這里我們將包含兩條流水線,一條是用來進(jìn)行測(cè)試的流水線 (對(duì)應(yīng)用代碼運(yùn)行測(cè)試) ,一條是交付流水線 (將應(yīng)用代碼打包上傳鏡像倉庫,同時(shí)更新目標(biāo)環(huán)境中的應(yīng)用,實(shí)現(xiàn)自動(dòng)更新) 。
測(cè)試流水線
在 Jenkins 中創(chuàng)建一條新的名為 KubeVela-demo-CICD-app-test 的流水線:
測(cè)試流水線
然后配置構(gòu)建觸發(fā)器為 GitHub hook trigger for GITScm polling:
構(gòu)建觸發(fā)器
在這條流水線中,首先是采用了 golang 的鏡像作為執(zhí)行環(huán)境,方便后續(xù)運(yùn)行測(cè)試。然后將分支配置為 GitHub 倉庫中的 dev 分支,代表該條流水線被 Push 事件觸發(fā)后會(huì)拉取 dev 分支上的內(nèi)容并執(zhí)行測(cè)試,測(cè)試結(jié)束后將流水線的狀態(tài)回寫至 GitHub 中。這里我們使用的是基于 Kubernetes 的動(dòng)態(tài) Slave Agent,因此在流水線中需要配置 Kubernetes 的相關(guān)信息,包括 Kubernetes 的地址、Service Account 等。
void setBuildStatus(String message, String state) {
step([
$class: "GitHubCommitStatusSetter",
reposSource: [$class: "ManuallyEnteredRepositorySource", url: "https://github.com/cnych/KubeVela-demo-CICD-app"],
contextSource: [$class: "ManuallyEnteredCommitContextSource", context: "ci/jenkins/test-status"],
errorHandlers: [[$class: "ChangingBuildStatusErrorHandler", result: "UNSTABLE"]],
statusResultSource: [ $class: "ConditionalStatusResultSource", results: [[$class: "AnyBuildResult", message: message, state: state]] ]
]);
}
pipeline {
agent {
kubernetes {
cloud 'Kubernetes'
containerTemplate {
name 'golang'
image 'golang:1.13-rc-alpine3.10'
command 'cat'
ttyEnabled true
}
serviceAccount 'jenkins'
}
}
stages {
stage('Prepare') {
steps {
script {
def checkout = git branch: 'dev', url: 'https://github.com/cnych/KubeVela-demo-CICD-app.git'
env.GIT_COMMIT = checkout.GIT_COMMIT
env.GIT_BRANCH = checkout.GIT_BRANCH
echo "env.GIT_BRANCH=${env.GIT_BRANCH},env.GIT_COMMIT=${env.GIT_COMMIT}"
}
setBuildStatus("Test running", "PENDING");
}
}
stage('Test') {
steps {
container('golang') {
sh 'CGO_ENABLED=0 GOCACHE=$(pwd)/.cache go test *.go'
}
}
}
}
post {
success {
setBuildStatus("Test success", "SUCCESS");
}
failure {
setBuildStatus("Test failed", "FAILURE");
}
}
}
我們可以使用上面的代碼來執(zhí)行流水線:
測(cè)試流水線
部署流水線
類似測(cè)試流水線創(chuàng)建一個(gè)名為 KubeVela-demo-CICD-app-deploy 的部署流水線,首先將代碼倉庫中的分支拉取下來,區(qū)別是這里采用 prod 分支。然后使用 Docker 進(jìn)行鏡像構(gòu)建并推送至遠(yuǎn)端鏡像倉庫。構(gòu)建成功后,再將 Application 對(duì)應(yīng)的 YAML 文件轉(zhuǎn)換為 JSON 文件并注入 GIT_COMMIT,最后向 KubeVela apiserver 發(fā)送請(qǐng)求進(jìn)行創(chuàng)建或更新。
首先我們需要通過 VelaUX 來創(chuàng)建一個(gè)應(yīng)用,這里我們創(chuàng)建一個(gè)名為 kubevela-demo-app 的應(yīng)用,包含一個(gè)名為 kubevela-demo-app-web 的組件,組件類型為 webservice,并將組件的鏡像設(shè)置為 cnych/kubevela-demo-cicd-app,如下圖所示:
kubevela app
在應(yīng)用面板上,我們可以找到一個(gè)默認(rèn)的觸發(fā)器,點(diǎn)擊 手動(dòng)觸發(fā),我們可以看到 Webhook URL 和 Curl Command,我們可以在 Jenkins 的流水線中使用任意一個(gè)。
觸發(fā)器
Webhook URL 是這個(gè)觸發(fā)器的觸發(fā)地址,在 Curl Command 里,還提供了手動(dòng) Curl 該觸發(fā)器的請(qǐng)求示例。我們來詳細(xì)解析一下請(qǐng)求體:
{
// 必填,此次觸發(fā)的更新信息
"upgrade": {
// Key 為應(yīng)用的名稱
"<application-name>": {
// 需要更新的值,這里的內(nèi)容會(huì)被 Patch 更新到應(yīng)用上
"image": "<image-name>"
}
},
// 可選,此次觸發(fā)攜帶的代碼信息
"codeInfo": {
"commit": "<commit-id>",
"branch": "<branch>",
"user": "<user>"
}
}
upgrade 下是本次觸發(fā)要攜帶的更新信息,在應(yīng)用名下,是需要被 Patch 更新的值。默認(rèn)推薦的是更新鏡像 image,也可以擴(kuò)展這里的字段來更新應(yīng)用的其他屬性。codeInfo 中是代碼信息,可以選擇性地?cái)y帶,比如 commit ID、分支、提交者等,一般這些值可以通過在 CI 系統(tǒng)中使用變量替換來指定。
然后我們可以是部署流水線中使用上面的觸發(fā)器來部署應(yīng)用,的代碼如下所示:
void setBuildStatus(String message, String state) {
step([
$class: "GitHubCommitStatusSetter",
reposSource: [$class: "ManuallyEnteredRepositorySource", url: "https://github.com/cnych/KubeVela-demo-CICD-app"],
contextSource: [$class: "ManuallyEnteredCommitContextSource", context: "ci/jenkins/deploy-status"],
errorHandlers: [[$class: "ChangingBuildStatusErrorHandler", result: "UNSTABLE"]],
statusResultSource: [ $class: "ConditionalStatusResultSource", results: [[$class: "AnyBuildResult", message: message, state: state]] ]
]);
}
pipeline {
agent {
kubernetes {
cloud 'Kubernetes'
defaultContainer 'jnlp'
yaml '''
spec:
serviceAccountName: jenkins
containers:
- name: golang
image: golang:1.13-rc-alpine3.10
command:
- cat
tty: true
- name: docker
image: docker:latest
command:
- cat
tty: true
env:
- name: DOCKER_HOST
value: tcp://docker-dind:2375
'''
}
}
stages {
stage('Prepare') {
steps {
script {
def checkout = git branch: 'prod', url: 'https://github.com/cnych/KubeVela-demo-CICD-app.git'
env.GIT_COMMIT = checkout.GIT_COMMIT
env.GIT_BRANCH = checkout.GIT_BRANCH
echo "env.GIT_BRANCH=${env.GIT_BRANCH},env.GIT_COMMIT=${env.GIT_COMMIT}"
setBuildStatus("Deploy running", "PENDING");
}
}
}
stage('Build') {
steps {
withCredentials([[$class: 'UsernamePasswordMultiBinding',
credentialsId: 'docker-auth',
usernameVariable: 'DOCKER_USER',
passwordVariable: 'DOCKER_PASSWORD']]) {
container('docker') {
sh """
docker login -u ${DOCKER_USER} -p ${DOCKER_PASSWORD}
docker build -t cnych/kubevela-demo-cicd-app .
docker push cnych/kubevela-demo-cicd-app
"""
}
}
}
}
stage('Deploy') {
steps {
sh '''#!/bin/bash
set -ex
curl -X POST -H 'content-type: application/json' --url http://vela.k8s.local/api/v1/webhook/x0i7t8jdsz2uvime -d '{"action":"execute","upgrade":{"kubevela-demo-app":{"image":"cnych/kubevela-demo-cicd-app"}},"codeInfo":{"commit":"","branch":"","user":""}}'
'''
}
}
}
post {
success {
setBuildStatus("Deploy success", "SUCCESS");
}
failure {
setBuildStatus("Deploy failed", "FAILURE");
}
}
}
測(cè)試效果
在完成上述的配置流程后,持續(xù)交付的流程便已經(jīng)搭建完成。我們可以來檢驗(yàn)一下它的效果。
狀態(tài)
我們首先將 main.go 中的 VERSION 字段修改為 Bad Version Number,即:
const VERSION = "Bad Version Number"
然后提交該修改至 dev 分支,我們可以看到 Jenkins 上的測(cè)試流水線被觸發(fā)運(yùn)行,失敗后將該狀態(tài)回寫給 GitHub。
ci test status
ci test github status
我們重新將 VERSION 修改為 0.1.1,然后再次提交??梢钥吹竭@一次測(cè)試流水線成功完成執(zhí)行,并在 GitHub 對(duì)應(yīng)的 Commit 上看到了成功的標(biāo)志。
ci test status
ci test github status
接下來我們?cè)?GitHub 上提交 Pull Request 嘗試將 dev 分支上的更新合并至 prod 分支上。
PR
可以看到在 Jenkins 的部署流水線成功運(yùn)行結(jié)束后,GitHub 上 prod 分支最新的 Commit 也顯示了成功的標(biāo)志。
ci test status
ci test github status
我們的應(yīng)用已經(jīng)成功部署了,當(dāng)前 Deployment 的副本數(shù)是 3,并且還有一個(gè) Ingress 對(duì)象,這時(shí)我們可以訪問 Ingress 所配置的域名,成功顯示了當(dāng)前的版本號(hào)。
$ vela ls
APP COMPONENT TYPE TRAITS PHASE HEALTHY STATUS CREATED-TIME
kubevela-demo-app kubevela-demo-app webservice scaler,gateway running healthy Ready:3/3 2023-10-14 19:11:59 +0800 CST
$ kubectl get pods
NAME READY STATUS RESTARTS AGE
kubevela-demo-app-675896596f-87kxl 1/1 Running 0 9m39s
kubevela-demo-app-675896596f-q5pvz 1/1 Running 0 9m39s
kubevela-demo-app-675896596f-v895m 1/1 Running 0 44m
$ kubectl get ingress
NAME CLASS HOSTS ADDRESS PORTS AGE
kubevela-demo-app nginx kubevela-demo-cicd-app.k8s.local 80 10m
$ curl -H "Host: kubevela-demo-cicd-app.k8s.local" http://<ingress controller address>
Version: 0.1.1
如果想實(shí)現(xiàn)金絲雀發(fā)布,則可以使用上節(jié)的 kruise rollout 來實(shí)現(xiàn),至此,我們便已經(jīng)成功實(shí)現(xiàn)了一整套持續(xù)交付流程。在這個(gè)流程中,應(yīng)用的開發(fā)者借助 KubeVela + Jenkins 的能力,可以輕松完成應(yīng)用的迭代更新、集成測(cè)試、自動(dòng)發(fā)布與滾動(dòng)升級(jí),而整個(gè)流程在各個(gè)環(huán)節(jié)也可以按照開發(fā)者的喜好和條件選擇不同的工具,比如使用 Gitlab 替代 GitHub,或是使用 TravisCI 替代 Jenkins。
參考文檔:https://kubevela.io/docs/tutorials/jenkins/。