
Photo by Tobi from Pexels
前言:這篇文章接着上一篇 CI/CD探索與實踐 (Gitlab+Kubernetes) 記錄這段時間的學習與心得。
在上文中,簡單的完成了一個社區的流水線演示,但我平時在工作中使用的持續集成工具是Jenkins,公司的Gitlab版本很低,不是很好用。所以在學習的過程中就參考了前文流水線的思想,使用Jenkins實現了一下。至於為什么要使用Docker Swarm,其實是因為之前還有Skywalking沒有演示,我的筆記本內存不夠開着Kubernetes再開Skywalking了 (T_T)
本文的關注點是CI/CD的實踐過程,示例為最簡單的構建流程。如果有任何表述不當的地方歡迎評論交流。
1. Jenkins 介紹
由於前面的文章已經介紹過了持續集成和持續部署的概念,所以現在簡單的介紹一下Jenkins

Jenkins是使用Java語言開發,最流行的開源免費 CI&CD 軟件,用於自動化各種任務,包括構建、測試和部署軟件。支持各種運行方式,可通過系統包、Docker 或者通過一個獨立的 Java 程序。其前身是 Oracle的 Hudson項目,現在我們依舊經常能在Jenkins的堆棧中看到的Hudson。
大家有興趣可以觀看YTB上一個Jenkins官方發布的Jenkins介紹視頻:Jenkins is the Way to build, test, and deploy 。
Jenkins的靈魂:Pipeline

Jenkins 流水線 (或簡單的帶有大寫"P"的"Pipeline") 是一套插件,它支持實現和集成 continuous delivery pipelines 到Jenkins。
對Jenkins 流水線的定義被寫在一個文本文件中 (成為 Jenkinsfile),該文件可以被提交到項目的源代碼的控制倉庫。 [2] 這是"流水線即代碼"的基礎; 將CD 流水線作為應用程序的一部分,像其他代碼一樣進行版本化和審查。 創建 Jenkinsfile並提交它到源代碼控制中提供了一些即時的好處:
- 自動地為所有分支創建流水線構建過程並拉取請求。
- 在流水線上代碼復查/迭代 (以及剩余的源代碼)。
- 對流水線進行審計跟蹤。
- 該流水線的真正的源代碼 [3], 可以被項目的多個成員查看和編輯。
2. 流水線介紹
本篇實踐的內容是參考前一條流水線的思想,使用Jenkins實現完成的,以下為實踐中用到的內容:
- Docker Swarm:Docker多節點容器編排。機器資源成本、學習成本基本為0。因為需要節省內存所以替換一下K8S。理解CI/CD思想就好 ~
- Spring Boot 2.3.0
- Gitlab :代碼版本庫
- Jenkins:持續集成工具
- Maven:編譯工具
- Sonar:代碼質控
- Jacoco:單測覆蓋度
- Skywalking:鏈路追蹤系統
工作流與上一篇文章相同。Docker官方的 CI/CD最佳實踐 如下圖所示:

按照圖示替換一下對應的應用實現:

3. 環境准備
3.1 硬件條件
32G+內存 (支撐 4核8G虛擬機 × 3 )
注:把Kubernetes換成了Docker Swarm節省了一部分內存,但是Gitlab、Harbor、Skywalking都比較吃內存。
3.2 流水線環境
3.2.1 代碼
由於加入了Skywalking的演示環節,所以這里需要多個應用
-
Project-Producer
啟動時從Redis中獲得一個唯一ID作為接口返回標識

Producer存在一個優雅下線的配置(配合Dockerfile捕捉SIGTERM完成優雅下線):
spring:
application:
name: ProduceApplication
redis:
host: 192.168.31.200
port: 6379
data:
redis:
repositories:
enabled: true
server:
port: 8010
shutdown: graceful
-
Project-Consumer
更加簡單的一個Demo,每次調用resource接口會先調用Producer獲得結果加上自身hostname一起返回

3.2.2 Docker
[landscape@centos ~]$ docker version
Client: Docker Engine - Community
Version: 20.10.2
API version: 1.41
Go version: go1.13.15
Git commit: 2291f61
Built: Mon Dec 28 16:17:48 2020
OS/Arch: linux/amd64
Context: default
Experimental: true
Server: Docker Engine - Community
Engine:
Version: 20.10.2
API version: 1.41 (minimum version 1.12)
Go version: go1.13.15
Git commit: 8891c58
Built: Mon Dec 28 16:16:13 2020
OS/Arch: linux/amd64
Experimental: false
containerd:
Version: 1.4.3
GitCommit: 269548fa27e0089a8b8278fc4fc781d7f65a939b
runc:
Version: 1.0.0-rc92
GitCommit: ff819c7e9184c13b7c2607fe6c30ae19403a7aff
docker-init:
Version: 0.19.0
GitCommit: de40ad0
3.3.3 Docker Swarm
[landscape@centos ~]$ docker info
Client:
Context: default
Debug Mode: false
Plugins:
app: Docker App (Docker Inc., v0.9.1-beta3)
buildx: Build with BuildKit (Docker Inc., v0.5.1-docker)
Server:
......
Swarm: active
NodeID: nfctjdvu0d1i8ptgz2ilri52d
Is Manager: true
ClusterID: 81zaq32bokwbntfparn6kxn0r
Managers: 1
Nodes: 11
Default Address Pool: 10.0.0.0/8
SubnetSize: 24
Data Path Port: 4789
Orchestration:
Task History Retention Limit: 5
Raft:
Snapshot Interval: 10000
Number of Old Snapshots to Retain: 0
Heartbeat Tick: 1
Election Tick: 10
Dispatcher:
Heartbeat Period: 5 seconds
CA Configuration:
Expiry Duration: 3 months
Force Rotate: 0
Autolock Managers: false
......
[landscape@centos ~]$ docker node ls
ID HOSTNAME STATUS AVAILABILITY MANAGER STATUS ENGINE VERSION
nfctjdvu0d1i8ptgz2ilri52d * centos.7.1 Ready Active Leader 20.10.2
6fbuvij7vor5fbvisf61hyir2 centos.7.2 Ready Active 20.10.2
o0p2y43ecbgwr1lzeep2hqghy centos.7.2 Down Active 20.10.2
lonfukl03a075bwnq4h5kbdzl centos.7.3 Ready Active 20.10.2
ttbimoijlg7rswzyrvu5p4om0 centos.7.3 Down Active 20.10.2
9c7zerxl6cgmhlq0ywfiatf2u localhost.localdomain Down Active 20.10.2
o9g79todgpbuwmcff77hwzv8c localhost.localdomain Down Active 20.10.2
x5qamqy4nvsep27x0g9mchk66 localhost.localdomain Down Active 20.10.2
3.3.4 Gitlab
分為兩個倉庫:Producer、Consumer

3.3.5 Dockerfile
以Producer為例,Consumer類似(這里是嵌入了Skywalking agent的):
FROM openjdk:8u275-jre
LABEL MAINTAINER="Landscape"
COPY target/producer.jar /opt/app.jar
COPY lib/agent/ /opt/agent/
ENV SW_AGENT_NAME ProducerApplication
ENV SW_AGENT_COLLECTOR_BACKEND_SERVICES 192.168.31.60:11800
EXPOSE 8010
ENTRYPOINT ["java", \
"-javaagent:/opt/agent/skywalking-agent.jar", \
"-jar", \
"/opt/app.jar"]
3.3.6 Docker Stack
關鍵點:
- 掛載存儲 Skywalking agent jar包 的volume
- 網絡選擇overlay表示覆蓋整個集群
- stop_grace_period 表示最長等待優雅下線的時間,進程需要自己捕獲 SIGTERM 信號來進行優雅處理,當然Dockerfile需要是exec格式的,如上Dockerfile,因為sh格式的dockerfile 應用進程將會在子shell中運行。
version: '3.2'
services:
producer:
image: 192.168.31.197:8000/devops/producer:latest
networks:
- psnet
ports:
- "8010:8010"
volumes:
- sw_agent:/opt/agent
stop_grace_period: 1m30s
deploy:
mode: replicated
replicas: 2
placement:
constraints: [node.platform.os == linux]
consumer:
image: 192.168.31.197:8000/devops/consumer:latest
depends_on:
- producer
networks:
- psnet
ports:
- "8011:8011"
volumes:
- sw_agent:/opt/agent
deploy:
mode: replicated
replicas: 1
placement:
constraints: [node.platform.os == linux]
networks:
psnet:
driver: overlay
attachable: true
volumes:
sw_agent:
3.3.7 Jenkins
勾選 Webhook, 選擇自己希望觸發的事件,然后在Gitlab的Webhook頁面配置好鈎子調用

Jenkinsfile(流水線腳本):
def build_args = [:]
build_args.harbor_address = "192.168.31.197:8000"
build_args.image_name = anyOf("DevOps-Producer",JOB_NAME)
build_args.maven_image="registry.cn-hangzhou.aliyuncs.com/acs/maven"
node(){
stage('pull'){
checkout([
$class: 'GitSCM',
branches: [[name: "feature-devops"]],
browser: [
$class: 'GitLab', repoUrl: 'http://192.168.31.196/root/DevOps-Producer',
version: '13.7.4'
],
doGenerateSubmoduleConfigurations: false,
extensions: [],
ubmoduleCfg: [],
userRemoteConfigs: [[
credentialsId: '3', url: 'http://192.168.31.196/root/DevOps-Producer.git'
]]]
)
}
stage('compile'){
docker.image("$build_args.maven_image").inside('-v maven-repo:/usr/share/maven/ref'){
sh "mvn -s /usr/share/maven/ref/settings.xml clean compile"
}
}
stage('check'){
parallel test:{
docker.image("$build_args.maven_image").inside('-v maven-repo:/usr/share/maven/ref'){
sh "mvn -s /usr/share/maven/ref/settings.xml test"
}
},sonar:{
docker.image("$build_args.maven_image").inside('-v maven-repo:/usr/share/maven/ref -v sonar-cache:/root/.sonar/cache'){
sh "mvn -s /usr/share/maven/ref/settings.xml sonar:sonar \
-Dsonar.projectKey=DevOps-Producer \
-Dsonar.host.url=http://192.168.31.200:9000 \
-Dsonar.login=0758a52664a843769e3e105ca38cda85acf60f1c"
}
}
}
stage('release'){
docker.image("$build_args.maven_image").inside('-v maven-repo:/usr/share/maven/ref'){
sh "mvn -s /usr/share/maven/ref/settings.xml clean compile package"
}
}
stage('docker build'){
sh "cp -r ~/lib ./"
sh "docker build -f devops/Dockerfile -t 192.168.31.197:8000/devops/producer:v${BUILD_ID} ."
sh 'docker login -u admin -p buzhidao http://192.168.31.197:8000/'
sh "docker push 192.168.31.197:8000/devops/producer:v${BUILD_ID}"
}
stage('deploy'){
sh "docker service update --with-registry-auth --image 192.168.31.197:8000/devops/producer:v${BUILD_ID} devops_producer"
}
}
def anyOf(String defaultStr,String[] options){
for(a in options){
if(a && a.trim()){
return a
}
}
return defaultStr
}
以上是本次實踐過程中完成的流水線腳本,還有不少可優化的地方,例如一些路徑位置可替換為變量,賬號密碼可以用credentials方法等,僅供參考 .~
3.3.8 Harbor倉庫
預先建好項目

3.3.9 SonarQube
預先建好項目、配置Token

3.3.10 Portainer
可有可無,只是一個WebUI ,在一定程度上可屏蔽集群信息,減少對宿主機的依賴,但是有時候感覺用起來還不如命令

4. 流水線運行
啟動Jenkins流水線,手動或者推送代碼通過Webhook都行

驗證一下結果,接口請求:

Sonar分析結果:

Skywalking 追蹤結果:

5. 一些流水線的細節
5.1 Jenkins流水線運行過程中的同級容器
流水線運行過程中對於需要的工具創建同級容器的方式可以基本杜絕對宿主機的依賴,只需要集群環境有相同的容器運行時即可。例如編譯、構建、測試等步驟都依賴Maven,則通過Maven鏡像完成所有工作,不需要宿主機上安裝Maven。
但這樣需要考慮的是Maven這樣的本地緩存應用如果每次都重新下載緩存的話就得不償失了,所以需要提前掛載本地倉庫卷。
另外如何掛載卷也是一個問題,因為Jenkins掛載了宿主機的 docker.sock 與宿主機進行通信,但如果在運行的過程中 Jenkins 希望啟動新容器並掛載容器內的 “/opt/one_path”,這是Jenkins可以在容器內訪問到的路徑,而宿主機的Docker Daemon只能看到宿主機的文件系統,找不到宿主機上的 “/opt/one_path” 則執行失敗。
這也是為什么在上例 Jenkinsfile 中使用了Docker插件的原因,Docker插件的文檔中說如下說明:
The above is a complete Pipeline script.
insidewill:
- Automatically grab an agent and a workspace (no extra
nodeblock is required).- Pull the requested image to the Docker server (if not already cached).
- Start a container running that image.
- Mount the Jenkins workspace as a "volume" inside the container, using the same file path. (這是重點)
- Run your build steps. External processes like
shwill be wrapped indocker execso they are run inside the container. Other steps (such as test reporting) run unmodified: they can still access workspace files created by build steps.- At the end of the block, stop the container and discard any storage it consumed.
- Record the fact that the build used the specified image. This unlocks features in other Jenkins plugins: you can track all projects using an image, or configure this project to be triggered automatically when an updated image is pushed to the Docker registry. If you use Docker Traceability plugin, you will be also able to see a history of the image deployments on Docker servers.
第四條翻譯為:使用相同的文件路徑將Jenkins工作區作為一個“卷”掛載到容器中。
如此一來,我們通過插件即可實現不進行任何手動掛載即可讓運行時的同級容器看到Jenkins 的Workspace,例如將maven的配置文件放在Workspace的某個目錄,掛載倉庫卷 + 命令指定配置文件,完成需求且不需要直接掛載宿主機目錄。_
5.2 關於SonarQube代碼分析和單元測試
這一點在上一章寫過了,SonarQube如果只作為Review優化標准的話可以無視結果,有需要可以在上面加入try catch
5.3 關於優雅下線
讀者可自行嘗試在Shell中捕獲SIGTERM和 SIGINT,提供示例腳本
-
TrapTerm.sh:
#!/bin/bash trap "echo '當前進程不可結束 by kill'" SIGTERM count=1 while [ $count -le 10 ] do echo "Loop #$count" sleep 3 count=$[ $count + 1 ] done # echo "This is the end of the test script" -
TrapInterrupt.sh
#!/bin/bash trap "echo '當前進程不可中斷 Ctrl-C'" SIGINT count=1 while [ $count -le 10 ] do echo "Loop #$count" sleep 1 count=$[ $count + 1 ] done # echo "This is the end of the test script"
配合Spring Boot文檔食用(Graceful shutdown):

6. 結語
Jenkins是很強大的持續集成服務器,聲明式流水線語法非常規整、優雅,腳本式流水線語法更加自由。順便有時候還會用來做一些閑散的操作,例如:
-
Grafana/Prometheus 的釘釘報警格式不友好?Jenkins Webhook + Groovy 處理 ~
-
Jira任務太多看不過來,發布版本不方便整理總結?Jenkins Webhook + Groovy 處理 ~
。。。。。。
文中組件沒有深入講解使用與原理,因為題目是CI/CD相關,本條流水線的部分原本是由我本人編寫並運行在我當前所在公司的生產環境,后來看到前一篇文章中Gitlab流水線獲得了一些靈感加以優化,雖然不完整但足以理解基本思想,如有建議歡迎探討 :
