持续集成工具-Jenkins
1. 什么是持续集成
持续集成 Continuous integration ,简称CI
随着软件开发复杂度的不断提高,团队开发成员间如何更好地协同工作以确保软件开发的质量已经慢慢成为开发过程中不可回避的问题。尤其是近些年来,敏捷(Agile) 在软件工程领域越来越红火,如何能再不断变化的需求中快速适应和保证软件的质量也显得尤其的重要。
持续集成正是针对这一类问题的一种软件开发实践。它倡导团队开发成员必须经常集成他们的工作,甚至每天都可能发生多次集成。而每次的集成都是通过自动化的构建来验证,包括自动编译、发布和测试,从而尽快地发现集成错误,让团队能够更快的开发内聚的软件。
持续集成具有的特点:
它是一个自动化的周期性的集成测试过程,从检出代码、编译构建、运行测试、结果记录、测试统计等都是自动完成的,无需人工干预;
需要有专门的集成服务器来执行集成构建;
需要有代码托管工具支持,我们下一小节将介绍Git以及可视化界面Gogs的使用
持续集成的作用:
保证团队开发人员提交代码的质量,减轻了软件发布时的压力;
持续集成中的任何一个环节都是自动完成的,无需太多的人工干预,有利于减少重复过程以节省时间、费用和工作量;
2 Jenkins简介
Jenkins,原名Hudson,2011年改为现在的名字,它 是一个开源的实现持续集成的软件工具。官方网站:http://jenkins-ci.org/。
Jenkins 能实施监控集成中存在的错误,提供详细的日志文件和提醒功能,还能用图表的形式形象地展示项目构建的趋势和稳定性。
特点:
易安装:仅仅一个 java -jar jenkins.war,从官网下载该文件后,直接运行,无需额外的安装,更无需安装数据库;
易配置:提供友好的GUI配置界面;
变更支持:Jenkins能从代码仓库(Subversion/CVS)中获取并产生代码更新列表并输出到编译输出信息中;
支持永久链接:用户是通过web来访问Jenkins的,而这些web页面的链接地址都是永久链接地址,因此,你可以在各种文档中直接使用该链接;
集成E-Mail/RSS/IM:当完成一次集成时,可通过这些工具实时告诉你集成结果(据我所知,构建一次集成需要花费一定时间,有了这个功能,你就可以在等待结果过程中,干别的事情);
JUnit/TestNG测试报告:也就是用以图表等形式提供详细的测试报表功能;
支持分布式构建:Jenkins可以把集成构建等工作分发到多台计算机中完成;
文件指纹信息:Jenkins会保存哪次集成构建产生了哪些jars文件,哪一次集成构建使用了哪个版本的jars文件等构建记录;
支持第三方插件:使得 Jenkins 变得越来越强大
环境准备
服务器: 192.168.234.59
jdk-8u171-linux-x64.rpm
jenkins.war
plugins.zip
git.tar.gz
apache-maven-3.9.5-bin.tar.gz
node-v14.21.3-linux-x64.tar.xz
openjdk-17.0.0.1+2_linux-x64_bin.tar.gz
sonar-scanner-cli-4.6.2.2472-linux.zip
1. 安装JDK8
运行Jenkins需要JDK8环境
上传jdk-8u171-linux-x64.rpm到/opt目录下
# rpm安装jdk, 默认会安装到/usr/java/jdk1.8.0_171-amd64目录
rpm -ivh jdk-8u171-linux-x64.rpm
# 在安装目录下验证jdk版本
/usr/java/jdk1.8.0_171-amd64/bin/java -version2. 安装OpenJDK17
安装OpenJDK17或更多版本JDK, 用于针对不同项目所需的JDK版本不同
上传openjdk-17.0.0.1+2_linux-x64_bin.tar.gz到/opt目录
# 解压openjdk17安装包, 解压后的目录是/usr/java/jdk-17.0.0.1
tar -zxvf openjdk-17.0.0.1+2_linux-x64_bin.tar.gz -C /usr/java
# 在解压目录下验证openjdk版本
/usr/java/jdk-17.0.0.1/bin/java -version3. 安装Git
安装Git用于Jenkins自动拉取代码
上传git.tar.gz到/opt目录下
# 解压git安装包
tar -zxvf git.tar.gz
# 进入到解压目录安装
yum --disablerepo=* localinstall *.rpm
# 检查git版本
git --version4. 安装maven
安装maven用于Jenkins构建maven项目
上传apache-maven-3.9.5-bin.tar.gz到/opt目录下
# 解压maven安装包, 解压后的目录是/usr/maven/apache-maven-3.9.5
tar -zxvf apache-maven-3.9.5-bin.tar.gz -C /usr/maven如果是联网环境, 根据maven私服配置usr/maven/apache-maven-3.9.5/conf/settings.xml文件
如果是离线环境, 上传本地仓库到usr/maven/apache-maven-3.9.5/repository下, 并修改setting配置内容为:
<?xml version="1.0" encoding="UTF-8"?>
<settings xmlns="http://maven.apache.org/SETTINGS/1.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.0.0 http://maven.apache.org/xsd/settings-1.0.0.xsd">
<localRepository>/usr/maven/apache-maven-3.9.5/repository</localRepository>
<offline>true</offline>
<!-- 完全禁用所有仓库 -->
<profiles>
<profile>
<id>strict-offline</id>
<repositories/>
<pluginRepositories/>
</profile>
</profiles>
<activeProfiles>
<activeProfile>strict-offline</activeProfile>
</activeProfiles>
<mirrors>
<mirror>
<id>my</id>
<mirrorOf>*</mirrorOf>
<url>file:///opt/apache-maven-3.9.5/repository</url>
<layout>default</layout>
</mirror>
</mirrors>
</settings>5. 安装Node
安装Node用于Jenkins构建前端项目
上传node-v14.21.3-linux-x64.tar.xz到/opt目录下
# 解压node安装包, 解压后的目录为/usr/git/node/node-v14.21.3-linux-x64
tar -zxvf node-v14.21.3-linux-x64.tar.xz -C /usr/node
# 设置node仓库
npm config set registry https://repo.ctbiyi.com/repository/npmall6. 配置环境变量
vim /etc/profile
# 在末尾添加:
export JAVA_HOME=/usr/java/jdk1.8.0_171-amd64
export MAVEN_HOME=/opt/apache-maven-3.9.5
export NODE_HOME=/opt/node-v14.21.3-linux-x64
export PATH=$PATH:$JAVA_HOME/bin:$MAVEN_HOME/bin:$NODE_HOME/bin# 刷新环境变量
source /etc/profile# 针对项目需要, 配置maven仅使用jdk17, 在家目录下创建.mavenrc文件
vi ~/.mavenrc
# 内容如下:
export JAVA_HOME=/usr/java/jdk-17.0.0.17. 安装Jenkins
上传jenkins.war到/opt目录下
启动jenkins服务
# 启动jenkins服务, 复制控制台日志打印的初始密码 java -jar jenkins.war浏览器访问
http://192.168.234.59:8080登录jenkins面板使用控制台日志打印的初始密码登录jenkins
创建root用户, 账号密码都设置为root
跳过所有插件安装
停止jenkins服务
8. 安装Jenkins插件
更多插件: https://plugins.jenkins.io/
上传plugins.zip到opt目录下
# 解压插件包到jenkins工作目录下
unzip plugins.zip -C /root/.jenkins
# 启动jenkins服务完成插件安装
java -jar jenkins.war9. 安装Sonar-Scanner-Cli
上传sonar-scanner-cli-4.6.2.2472-linux.zip到/opt目录下
解压安装sonar-scanner-cli
[root@dc-59 opt]# unzip sonar-scanner-cli-4.6.2.2472-linux.zip
[root@dc-59 opt]# mv sonar-scanner-4.6.2.2472-linux sonar-scanner-cli编辑/etc/profile文件, 在末尾追加内容如下, 配置环境变量
export SONAR_SCANNER_HOME=/opt/sonar-scanner-cli
export PATH=$SONAR_SCANNER_HOME/bin:$PATH执行 source /etc/profile 刷新环境变量
10. 配置SonarQube
登录SonarQube Web UI, 配置质量阀 
登录SonarQube Web UI, 点击右上角头像旁边的 + 号, 创建项目(根据实际情况填写项目名), 保留生成的令牌

登陆Jenkins, 进入 系统管理 -> 凭据管理 -> 全局 -> 添加凭据 将上一步生成的令牌添加到jenkins中 
登陆Jenkins, 进入 系统管理 -> 系统配置, 在SonarQube服务器中添加sonarqube服务器 
登陆Jenkins, 进入 系统管理 -> 全局工具配置 配置sonar-scanner-cli 
11. 配置Jenkins
登录jenkins, 进入
系统管理->全局工具配置配置JDK

配置Git

配置Maven

进入
系统管理->系统配置配置环境变量

12. 添加Git仓库凭据
登录jenkins, 进入
用户->凭据->全局->添加凭据, 输入git仓库的账号和密码
13. 配置ssh server
登录jenkins, 进入
系统管理->系统配置新增ssh servers

点击test, 出现success即可
登录刚刚配置的服务器, 切换到root用户执行
visudo命令, 编辑sudoers配置, 允许ctdi用户免密提权# 注释掉此行: %wheel ALL=(ALL) ALL # 放开此行注释: %wheel ALL=(ALL) NOPASSWD: ALL
14. 创建流水线
新建任务, 选择流水线类型

勾选参数化构建过程, 添加参数

配置流水线
流水线内容如下(其中'拉取代码'步骤使用的凭据为配置的Git仓库凭据):pipeline { agent any parameters { gitParameter( name: '选择分支', type: 'PT_BRANCH', branchFilter: 'origin/(.*)', defaultValue: 'dev', description: '' ) choice( name: '选择服务器', choices: ['192.168.234.59', '192.168.234.60', '192.168.234.61'], description: '' ) extendedChoice( name: '选择应用', type: 'PT_CHECKBOX', description: '', multiSelectDelimiter: ',', quoteValue: false, defaultValue: 'Smart-Agent', value: 'Smart-Gateway,Smart-Agent,Smart-Auth,Smart-Statistics' ) } stages { stage("拉取代码") { steps { git branch: params.BRANCH_NAME ?: 'dev', credentialsId: 'dd825150-8e6e-4bd3-a8b9-320468cfdc2a', url: 'http://192.168.234.59:3000/root/project_java.git' } } stage("编译打包") { steps { echo "开始编译" sh 'mvn clean package -Dmaven.test.skip=true --offline -s /opt/maven/conf/settings.xml' echo "编译完成" } } stage("代码扫描") { steps { withSonarQubeEnv('sonarqube') { sh ''' mvn sonar:sonar \ -Dsonar.projectKey=project_java \ -Dsonar.host.url=$SONAR_HOST_URL \ --batch-mode \ -s /opt/maven/conf/settings.xml ''' } } post { success { script { def qg = waitForQualityGate() if (qg.status != 'OK') { error "代码扫描完成, 质量检查未通过,当前状态:${qg.status}, 终止流水线!" } else { echo "代码扫描完成, 质量检查通过,状态:${qg.status}" } } } failure { error "代码扫描失败, 终止流水线!" } } } stage("部署应用") { steps { script { def selectedOptions = params.'选择应用' def targetServer = params.'选择服务器' echo "目标服务器为 ${targetServer}" echo "要部署的服务为 ${selectedOptions}" if (selectedOptions.contains('Smart-Gateway')) { echo "开始部署Smart-Gateway" sshPublisher(publishers: [ sshPublisherDesc( configName: "${targetServer}", transfers: [ sshTransfer( sourceFiles: 'smart-gateway/target/Smart-Gateway.jar', remoteDirectory: '/home/ctdi', removePrefix: 'smart-gateway/target', execCommand: """ sudo mv /opt/smart/Smart-Gateway.jar /opt/smart/Smart-Gateway.jar.bak; sudo mv /home/ctdi/Smart-Gateway.jar /opt/smart/Smart-Gateway.jar; sudo docker-compose -f /opt/smart/docker-compose.yml restart smart-gateway """, execTimeout: 120000 ) ], verbose: true ) ]) echo "部署Smart-Gateway完成" } if (selectedOptions.contains('Smart-Agent')) { echo "开始部署Smart-Agent" sshPublisher(publishers: [ sshPublisherDesc( configName: "${targetServer}", transfers: [ sshTransfer( sourceFiles: 'smart-agent/target/Smart-Agent.jar', remoteDirectory: '/home/ctdi', removePrefix: 'smart-agent/target', execCommand: """ sudo mv /opt/smart/Smart-Agent.jar /opt/smart/Smart-Agent.jar.bak; sudo mv /home/ctdi/Smart-Agent.jar /opt/smart/Smart-Agent.jar; sudo docker-compose -f /opt/smart/docker-compose.yml restart smart-agent """, execTimeout: 120000 ) ], verbose: true ) ]) echo "部署Smart-Agent完成" } if (selectedOptions.contains('Smart-Auth')) { echo "开始部署Smart-Auth" sshPublisher(publishers: [ sshPublisherDesc( configName: "${targetServer}", transfers: [ sshTransfer( sourceFiles: 'smart-auth/target/Smart-Auth.jar', remoteDirectory: '/home/ctdi', removePrefix: 'smart-auth/target', execCommand: """ sudo mv /opt/smart/Smart-Auth.jar /opt/smart/Smart-Auth.jar.bak; sudo mv /home/ctdi/Smart-Auth.jar /opt/smart/Smart-Auth.jar; sudo docker-compose -f /opt/smart/docker-compose.yml restart smart-auth """, execTimeout: 120000 ) ], verbose: true ) ]) echo "部署Smart-Auth完成" } if (selectedOptions.contains('Smart-Statistics')) { echo "开始部署Smart-Statistics" sshPublisher(publishers: [ sshPublisherDesc( configName: "${targetServer}", transfers: [ sshTransfer( sourceFiles: 'smart-statistics/target/Smart-Statistics.jar', remoteDirectory: '/home/ctdi', removePrefix: 'smart-statistics/target', execCommand: """ sudo mv /opt/smart/Smart-Statistics.jar /opt/smart/Smart-Statistics.jar.bak; sudo mv /home/ctdi/Smart-Statistics.jar /opt/smart/Smart-Statistics.jar; sudo docker-compose -f /opt/smart/docker-compose.yml restart smart-statistics """, execTimeout: 120000 ) ], verbose: true ) ]) echo "部署Smart-Statistics完成" } } } } } post { always { echo '检查应用状态!' } success { script { def selectedApps = params.'选择应用'.tokenize(',') def targetServer = params.'选择服务器' def endpoints = [ "Smart-Gateway": "http://${targetServer}:9000/actuator/health", "Smart-Agent": "http://${targetServer}:8091/actuator/health", "Smart-Auth": "http://${targetServer}:8092/actuator/health", "Smart-Statistics": "http://${targetServer}:8093/actuator/health" ] def rollbackApps = [] echo '等待应用启动完成...' sleep 20 selectedApps.each { app -> if (endpoints.containsKey(app)) { def endpoint = endpoints[app] try { def response = sh(script: "curl -s ${endpoint}", returnStdout: true).trim() def jsonResponse = new groovy.json.JsonSlurper().parseText(response) if (jsonResponse.status != "UP") { echo "${targetServer} 服务器的 ${app} 状态异常: ${response}" rollbackApps.add(app) } else { echo "${targetServer} 服务器的 ${app} 状态正常" } } catch (Exception e) { echo "检查${targetServer} 服务器的 ${app} 失败: ${e.getMessage()}" rollbackApps.add(app) } } } if (rollbackApps) { echo "以下应用状态异常,将执行回退: ${rollbackApps.join(', ')}" rollbackApps.each { app -> echo "正在回退 ${targetServer} 服务器的 ${app}" def serviceName = app.toLowerCase().replace('-', '_') sshPublisher(publishers: [ sshPublisherDesc( configName: "${targetServer}", transfers: [ sshTransfer( execCommand: """ sudo mv /opt/smart/${app}.jar.bak /opt/smart/${app}.jar; sudo docker-compose -f /opt/smart/docker-compose.yml start ${serviceName}; """, execTimeout: 120000 ) ], verbose: true ) ]) echo "${targetServer} 服务器的 ${app} 回退完成!" } } else { echo "${selectedApps} 运行正常,无需回退!" } } } failure { echo '构建失败!' } } }运行效果如下
