项目简介
利用 Jenkins、Docker、SonarQube 和 Harbor 技术,搭建一个完整的 CI/CD 管道,实现持续集成、持续交付和持续部署的流程。通过自动化构建、测试、代码质量检查和容器化部署,将开发人员从繁琐的手动操作中解放出来,提高团队的开发效率、软件质量和安全性,实现持续更新迭代和持续部署交付。
CICD流程图
流程说明
- 开发人员将代码提交到Gitlab代码仓库时,gitlab请求jenkins的webhook地址,触发持续构建和持续部署流程。
- Jenkins从Gitlab中拉取项目源码,编译并打成jar包,然后调用SonarQube完成代码扫描。
- 扫描完成调用docker打包成容器镜像,并推送至Harbor镜像仓库。
- Jenkins发送SSH远程命令,让生产部署服务器到Harbor私有仓库拉取镜像到本地,然后创建容器。
- Jenkins完成CICD流程后,将结果邮件通知给开发和运维人员。
- 用户访问项目服务器。
服务器列表
服务器名称 | 主机名 | IP | 部署服务 |
---|---|---|---|
代码托管服务器 | gitlab | 192.168.10.72 | Gitlab |
持续集成服务器 | jenkins | 192.168.10.73 | Jenkins、Maven、Docker |
代码审查服务器 | sonarqube | 192.168.10.71 | SonarQube |
镜像仓库服务器 | harbor | 192.168.10.100 | Docker、harbor |
服务部署服务器 | springboot | 192.168.10.74 | Docker |
项目代码仓库地址
gitee:https://gitee.com/cuiliang0302/sprint_boot_demo
github:https://github.com/cuiliang0302/sprint-boot-demo
服务部署(rpm方式)
gitlab部署
参考文档:https://www.cuiliangblog.cn/detail/section/92727905
jenkins部署
参考文档:https://www.cuiliangblog.cn/detail/section/15130009
docker部署
参考文档:https://www.cuiliangblog.cn/detail/section/26447182
harbor部署
参考文档:https://www.cuiliangblog.cn/detail/section/15189547
SonarQube部署
参考文档:https://www.cuiliangblog.cn/detail/section/131467837
harbor项目权限配置
创建项目
Harbor的项目分为公开和私有的:
公开项目:所有用户都可以访问,通常存放公共的镜像,默认有一个library公开项目。
私有项目:只有授权用户才可以访问,通常存放项目本身的镜像。 我们可以为微服务项目创建一个新的项目
创建用户
创建一个普通用户cuiliang。
配置项目用户权限
在spring_boot_demo项目中添加普通用户cuiliang,并设置角色为开发者。
权限说明
角色 | 权限 |
---|---|
访客 | 对项目有只读权限 |
开发人员 | 对项目有读写权限 |
维护人员 | 对项目有读写权限、创建webhook权限 |
项目管理员 | 出上述外,还有用户管理等权限 |
上传下载镜像测试
可参考文章https://www.cuiliangblog.cn/detail/section/15189547,此处不再赘述。
gitlab项目权限配置
具体gitlab权限配置参考文档:https://www.cuiliangblog.cn/detail/section/131513569
创建开发组develop,用户cuiliang,项目springboot demo
创建组
管理员用户登录,创建群组,组名称为develop,组权限为私有
创建项目
创建sprint boot demo项目,并指定develop,项目类型为私有
创建用户
创建一个普通用户cuiliang
用户添加到组中
将cuiliang添加到群组develop中,cuiliang角色为Developer
开启分支推送权限
用户权限验证
使用任意一台机器模拟开发人员拉取代码,完成开发后推送至代码仓库。
拉取仓库代码
[root@tiaoban opt]# git clone https://gitee.com/cuiliang0302/sprint_boot_demo.git
正克隆到 'sprint_boot_demo'...
remote: Enumerating objects: 69, done.
remote: Counting objects: 100% (69/69), done.
remote: Compressing objects: 100% (54/54), done.
remote: Total 69 (delta 15), reused 0 (delta 0), pack-reused 0
接收对象中: 100% (69/69), 73.15 KiB | 1.49 MiB/s, 完成.
处理 delta 中: 100% (15/15), 完成.
[root@tiaoban opt]# cd sprint_boot_demo/
[root@tiaoban sprint_boot_demo]# ls
email.html Jenkinsfile LICENSE mvnw mvnw.cmd pom.xml readme.md sonar-project.properties src test
推送至gitlab仓库
# 修改远程仓库地址
[root@tiaoban sprint_boot_demo]# git remote set-url origin http://192.168.10.72/develop/sprint-boot-demo.git
[root@tiaoban sprint_boot_demo]# git remote -v
origin http://192.168.10.72/develop/sprint-boot-demo.git (fetch)
origin http://192.168.10.72/develop/sprint-boot-demo.git (push)
# 推送代码至gitlab
[root@tiaoban sprint_boot_demo]# git push --set-upstream origin --all
Username for 'http://192.168.10.72': cuiliang
Password for 'http://cuiliang@192.168.10.72':
枚举对象中: 55, 完成.
对象计数中: 100% (55/55), 完成.
使用 4 个线程进行压缩
压缩对象中: 100% (34/34), 完成.
写入对象中: 100% (55/55), 71.51 KiB | 71.51 MiB/s, 完成.
总共 55(差异 10),复用 52(差异 9),包复用 0
To http://192.168.10.72/develop/sprint-boot-demo.git
* [new branch] main -> main
分支 'main' 设置为跟踪 'origin/main'。
查看验证
jenkins流水线配置
拉取gitlab仓库代码
具体步骤可参考文档:https://www.cuiliangblog.cn/detail/section/127410630,此处以账号密码验证为例,并启用webhook配置。
jenkins流水线配置如下
拉取代码部分的jenkinsfile如下
pipeline {
agent any
stages {
stage('拉取代码') {
environment {
// gitlab仓库信息
GITLAB_CRED = "gitlab-cuiliang-password"
GITLAB_URL = "http://192.168.10.72/develop/sprint-boot-demo.git"
}
steps {
echo '开始拉取代码'
checkout scmGit(branches: [[name: '*/main']], extensions: [], userRemoteConfigs: [[credentialsId: "${GITLAB_CRED}", url: "${GITLAB_URL}"]])
echo '拉取代码完成'
}
}
}
}
当git仓库提交代码后,Gitlab会自动请求Jenkins的webhook地址,自动触发流水线,执行结果如下:
Maven打包编译
具体步骤可参考文档:https://www.cuiliangblog.cn/detail/section/131898197
打包编译部分的jenkinsfile如下
pipeline {
agent any
stages {
stage('拉取代码') {
……
}
stage('打包编译') {
steps {
echo '开始打包编译'
sh 'mvn clean package'
echo '打包编译完成'
}
}
}
}
触发流水线结果如下
SonarQube代码审查
具体步骤可参考文档:https://www.cuiliangblog.cn/detail/section/165534414
代码审查阶段的jenkinsfile如下
pipeline {
agent any
stages {
stage('拉取代码') {
……
}
stage('打包编译') {
……
}
stage('代码审查') {
environment {
// SonarQube信息
SONARQUBE_SCANNER = "SonarQubeScanner"
SONARQUBE_SERVER = "SonarQubeServer"
}
steps{
echo '开始代码审查'
script {
def scannerHome = tool "${SONARQUBE_SCANNER}"
withSonarQubeEnv("${SONARQUBE_SERVER}") {
sh "${scannerHome}/bin/sonar-scanner"
}
}
echo '代码审查完成'
}
}
}
}
触发流水线结果如下
代码审查结果如下
构建并推送镜像至仓库
具体步骤可参考文档:https://www.cuiliangblog.cn/detail/section/166573065
构建并推送镜像的jenkinsfile如下
pipeline {
agent any
stages {
stage('拉取代码') {
……
}
stage('打包编译') {
……
}
stage('代码审查') {
……
}
stage('构建镜像') {
environment {
// harbor信息
HARBOR_CRED = "harbor-cuiliang-password"
HARBOR_URL = "harbor.local.com"
HARBOR_PROJECT = "spring_boot_demo"
// 镜像信息
IMAGE_APP = "demo"
IMAGE_TAG = sh(script: "git rev-parse --short HEAD", returnStdout: true).trim()
IMAGE_NAME = "${HARBOR_URL}/${HARBOR_PROJECT}/${IMAGE_APP}:${IMAGE_TAG}"
}
steps {
echo '开始构建镜像'
script {
docker.build "${IMAGE_NAME}"
}
echo '构建镜像完成'
echo '开始推送镜像'
script {
docker.withRegistry("https://${HARBOR_URL}", "${HARBOR_CRED}") {
docker.image("${IMAGE_NAME}").push()
}
}
echo '推送镜像完成'
echo '开始删除镜像'
script {
sh "docker rmi -f ${IMAGE_NAME}"
}
echo '删除镜像完成'
}
}
}
}
触发流水线结果如下
查看harbor镜像仓库,已上传镜像
docker运行服务
远程执行命令具体内容可参考文档:https://www.cuiliangblog.cn/detail/section/166296541
部署运行阶段的jenkinsfile如下
pipeline {
agent any
environment {
// 全局变量
HARBOR_CRED = "harbor-cuiliang-password"
IMAGE_NAME = ""
IMAGE_APP = "demo"
}
stages {
stage('拉取代码') {
……
}
stage('打包编译') {
……
}
stage('代码审查') {
……
}
stage('构建镜像') {
……
}
stage('项目部署') {
environment {
// 目标主机信息
HOST_NAME = "springboot1"
}
steps {
echo '开始部署项目'
// 获取harbor账号密码
withCredentials([usernamePassword(credentialsId: "${HARBOR_CRED}", passwordVariable: 'HARBOR_PASSWORD', usernameVariable: 'HARBOR_USERNAME')]) {
// 执行远程命令
sshPublisher(publishers: [sshPublisherDesc(configName: "${HOST_NAME}", transfers: [sshTransfer(
cleanRemote: false, excludes: '', execCommand: "sh -x /opt/jenkins/springboot/deployment.sh ${HARBOR_USERNAME} ${HARBOR_PASSWORD} ${IMAGE_NAME} ${IMAGE_APP}",
execTimeout: 120000, flatten: false, makeEmptyDirs: false, noDefaultExcludes: false, patternSeparator: '[, ]+', remoteDirectory: '/opt/jenkins/springboot',
remoteDirectorySDF: false, removePrefix: '', sourceFiles: 'deployment.sh')], usePromotionTimestamp: false, useWorkspaceInPromotion: false, verbose: false
)])
}
echo '部署项目完成'
}
}
}
}
触发流水线后运行结果如下
登录springboot服务器验证
[root@springboot ~]# docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
e80896487125 harbor.local.com/spring_boot_demo/demo:8880a30 "java -jar /app.jar" About a minute ago Up About a minute (unhealthy) 0.0.0.0:8888->8888/tcp, :::8888->8888/tcp demo
[root@springboot ~]# curl 127.0.0.1:8888/
<h1>Hello SpringBoot</h1><p>Version:v2 Env:test</p>[root@springboot1 ~]#
[root@springboot ~]# ls /opt/jenkins/springboot/
deployment.sh Dockerfile email.html Jenkinsfile LICENSE mvnw mvnw.cmd pom.xml readme.md sonar-project.properties src target test
添加邮件通知推送
发送邮件配置具体内容可参考文档:https://www.cuiliangblog.cn/detail/section/133029974
在项目根路径下新增email.html文件,内容如下
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>${ENV, var="JOB_NAME"}-第${BUILD_NUMBER}次构建日志</title>
<style>
body {
font-family: Arial, sans-serif;
line-height: 1.6;
margin: 0;
padding: 0;
}
.container {
max-width: 1000px;
margin: 20px auto;
padding: 20px;
border: 1px solid #ccc;
border-radius: 5px;
}
.container h2 {
text-align: center;
}
.logo img {
max-width: 150px;
height: auto;
}
.content {
padding: 20px;
background-color: #f9f9f9;
border-radius: 5px;
}
.footer {
margin-top: 20px;
text-align: center;
}
</style>
</head>
<body>
<div class="container">
<div class="content">
<h2>Jenkins ${PROJECT_NAME}项目构建结果</h2>
<p>尊敬的用户:</p>
<p>${PROJECT_NAME}项目构建结果为<span style="color:red;font-weight: bold;">${BUILD_STATUS}</span>,以下是详细信息:</p>
<h4>构建信息</h4>
<hr/>
<ul>
<li>项目名称:${PROJECT_NAME}</li>
<li>构建编号:第${BUILD_NUMBER}次构建</li>
<li>触发原因:${CAUSE}</li>
<li>构建状态:${BUILD_STATUS}</li>
<li>构建日志:<a href="${BUILD_URL}console">${BUILD_URL}console</a></li>
<li>构建Url:<a href="${BUILD_URL}">${BUILD_URL}</a></li>
<li>工作目录:<a href="${PROJECT_URL}ws">${PROJECT_URL}ws</a></li>
<li>项目Url:<a href="${PROJECT_URL}">${PROJECT_URL}</a></li>
</ul>
<h4>失败用例</h4>
<hr/>
<p>$FAILED_TESTS</p>
<h4>最近提交</h4>
<hr/>
<ul>
${CHANGES_SINCE_LAST_SUCCESS, reverse=true, format="%c", changesFormat="<li>%d [%a] %m</li>"}
</ul>
<h4>提交详情</h4>
<hr/>
<p><a href="${PROJECT_URL}changes">${PROJECT_URL}changes</a></p>
<p style="margin-top:50px">如有任何疑问或需要帮助,请随时联系我们。</p>
</div>
<div class="footer">
<p>此为系统自动发送邮件,请勿回复。</p>
</div>
</div>
</body>
</html>
完整的jenkinsfile如下
pipeline {
agent any
environment {
// 全局变量
HARBOR_CRED = "harbor-cuiliang-password"
IMAGE_NAME = ""
IMAGE_APP = "demo"
}
stages {
stage('拉取代码') {
environment {
// gitlab仓库信息
GITLAB_CRED = "gitlab-cuiliang-password"
GITLAB_URL = "http://192.168.10.72/develop/sprint-boot-demo.git"
}
steps {
echo '开始拉取代码'
checkout scmGit(branches: [[name: '*/main']], extensions: [], userRemoteConfigs: [[credentialsId: "${GITLAB_CRED}", url: "${GITLAB_URL}"]])
echo '拉取代码完成'
}
}
stage('打包编译') {
steps {
echo '开始打包编译'
sh 'mvn clean package'
echo '打包编译完成'
}
}
stage('代码审查') {
environment {
// SonarQube信息
SONARQUBE_SCANNER = "SonarQubeScanner"
SONARQUBE_SERVER = "SonarQubeServer"
}
steps{
echo '开始代码审查'
script {
def scannerHome = tool "${SONARQUBE_SCANNER}"
withSonarQubeEnv("${SONARQUBE_SERVER}") {
sh "${scannerHome}/bin/sonar-scanner"
}
}
echo '代码审查完成'
}
}
stage('构建镜像') {
environment {
// harbor仓库信息
HARBOR_URL = "harbor.local.com"
HARBOR_PROJECT = "spring_boot_demo"
// 镜像名称
IMAGE_TAG = sh(script: "git rev-parse --short HEAD", returnStdout: true).trim()
}
steps {
echo '开始构建镜像'
script {
IMAGE_NAME = "${HARBOR_URL}/${HARBOR_PROJECT}/${IMAGE_APP}:${IMAGE_TAG}"
docker.build "${IMAGE_NAME}"
}
echo '构建镜像完成'
echo '开始推送镜像'
script {
docker.withRegistry("https://${HARBOR_URL}", "${HARBOR_CRED}") {
docker.image("${IMAGE_NAME}").push()
}
}
echo '推送镜像完成'
echo '开始删除镜像'
script {
sh "docker rmi -f ${IMAGE_NAME}"
}
echo '删除镜像完成'
}
}
stage('项目部署') {
environment {
// 目标主机信息
HOST_NAME = "springboot1"
}
steps {
echo '开始部署项目'
// 获取harbor账号密码
withCredentials([usernamePassword(credentialsId: "${HARBOR_CRED}", passwordVariable: 'HARBOR_PASSWORD', usernameVariable: 'HARBOR_USERNAME')]) {
// 执行远程命令
sshPublisher(publishers: [sshPublisherDesc(configName: "${HOST_NAME}", transfers: [sshTransfer(
cleanRemote: false, excludes: '', execCommand: "sh -x /opt/jenkins/springboot/deployment.sh ${HARBOR_USERNAME} ${HARBOR_PASSWORD} ${IMAGE_NAME} ${IMAGE_APP}",
execTimeout: 120000, flatten: false, makeEmptyDirs: false, noDefaultExcludes: false, patternSeparator: '[, ]+', remoteDirectory: '/opt/jenkins/springboot',
remoteDirectorySDF: false, removePrefix: '', sourceFiles: 'deployment.sh')], usePromotionTimestamp: false, useWorkspaceInPromotion: false, verbose: false
)])
}
echo '部署项目完成'
}
}
}
post {
always {
echo '开始发送邮件通知'
// 构建后发送邮件
emailext(
subject: '构建通知:${PROJECT_NAME} - Build # ${BUILD_NUMBER} - ${BUILD_STATUS}!',
body: '${FILE,path="email.html"}',
to: 'cuiliang0302@qq.com'
)
echo '邮件通知发送完成'
}
}
}
触发流水线后运行结果如下
邮件通知内容如下
至此,整个CICD流程完成。
查看更多
微信公众号
微信公众号同步更新,欢迎关注微信公众号《崔亮的博客》第一时间获取最近文章。
博客网站
崔亮的博客-专注devops自动化运维,传播优秀it运维技术文章。更多原创运维开发相关文章,欢迎访问https://www.cuiliangblog.cn