前言
本篇文章主要介绍通过docker搭建Jenkins,然后使用Jenkins目前最主流的流水线(Pipeline)搭建搭建一个自动化的CI&CD全链路自动化流程。以最终达到本地提交代码Jenkins自动发布到测试环境的效果,并且在微服务环境下实现单个服务更新只会去发布代码更新了的服务的最终效果。
Jenkins是什么:
- Jenkins是一个开源软件项目,是基于Java开发的一种持续集成工具,用于监控持续重复的工作,旨在提供一个开放易用的软件平台,使软件项目可以进行持续集成。
核心功能:
- Jenkins通过可扩展插件体系实现持续集成全流程管理,主要功能包括:
自动化执行构建脚本,支持Maven、Gradle等构建工具对接(最重要) - 实时监控代码仓库变更,触发预设的测试与部署任务
- 生成可视化构建报告,集成Allure等测试框架结果分析
- 提供Blue Ocean界面优化多分支流水线编排体验
在搭建之前需要先安装Docker环境、一个Docker镜像仓库、一个Git仓库。
本篇文章介绍使用的Docker仓库为Harbor,Git仓库为Gitea
我前面的两篇文章介绍了如何快速搭建Docker环境以及Harbor镜像仓库
最终实现效果:发布四个服务,测试服前端后端两个任务,一个正式服的前后端两个任务

一、使用Docker部署Jenkins
1、拉取镜像
docker pull jenkins/jenkins:lts-jdk17
docker pull nginx:1.20.2
2、编写配置文件
2.1、Docker-compose.yml配置文件
部署采用docker-compose进行部署,这样方便做变更编写方便。
前端因为要使用Nginx用作反向代理,这里就直接写在一起了。这里需要根据自己服务器的文件情况去修改为自己的映射目录
version: '3.8'
services:
jenkins:
# image: jenkins/jenkins:lts-jdk17 # build和image是二选一 因为这里要去读取 Dockerfile.jenkins 配置文件,所以读取镜像也写在 Dockerfile.jenkins 里
build:
context: .
# dockerfile: 'Dockerfile.jenkins' 明确指定使用哪个文件作为配置
dockerfile: Dockerfile.jenkins
container_name: jenkins
ports:
- "8088:8080"
- "50000:50000"
volumes:
- ./config/jenkins:/var/jenkins_home
- /home/docker/volumes/jenkins-data:/home/jenkins-data
- /var/run/docker.sock:/var/run/docker.sock
user: root
environment:
# 解决 Jenkins 插件下载慢的问题
- JENKINS_OPTS="--prefix=/jenkins"
- JAVA_OPTS="-Duser.timezone=Asia/Shanghai -Djenkins.install.runSetupWizard=false -Djava.util.logging.config.file=/var/jenkins_home/log.properties"
- JENKINS_UC="https://mirrors.tuna.tsinghua.edu.cn/jenkins/"
- JENKINS_UC_EXPERIMENTAL="https://mirrors.tuna.tsinghua.edu.cn/jenkins/experimental/"
- JENKINS_PLUGIN_MIRROR="https://mirrors.tuna.tsinghua.edu.cn/jenkins/plugins/"
nginx:
image: nginx:1.20.2
restart: always
container_name: nginx-webserver2
ports:
- "91:91"
- "92:92"
- "443:443"
volumes:
- ./config/nginx/conf/nginx.conf:/etc/nginx/nginx.conf # 将本地 nginx 配置挂载到容器
- /home/docker/volumes/jenkins-data/nginx/:/usr/share/nginx/ # 将本地 html 目录挂载到容器
- ./config/nginx/log/:/var/log/nginx/ # (可选) 将 nginx 日志挂载到本地
privileged: true
deploy:
resources:
limits:
memory: 256m
下图是为了让大家好理解一点我画的一个两个容器和宿主机映射的关系图

所以我们需要先把用于存放映射文件目录和存放容器目录创建好
下面是我的
- 映射数据卷目录
/home/docker/volumes/jenkins-data/nginx:映射部分
/d2o-web/v3/test:项目部分,我这里分为了测试服和正式服的文件目录

- 容器存储配置目录

docker-compose.yml是放在/home/docker/Jenkins位置

2.2、nginx.conf示例文件
nginx.conf配置文件存放在Docker-compose.yml配置文件下的./config/nginx/conf目录下。
这个是我的示例文件,自己可以根据自己服务器去修改为自己的,分别存放了我前端下面的一个测试环境的一个正式环境的文件位置
worker_processes 10;
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;
events {
worker_connections 1024;
}
http {
include mime.types;
default_type application/octet-stream;
sendfile on;
keepalive_timeout 65;
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
#正式环境
server {
listen 91;
server_name localhost;
client_max_body_size 20m;
location / {
root /usr/share/nginx/d2o-web/v3/prod/;
try_files $uri $uri/ /index.html;
index index.html index.htm;
}
location /api/ {
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header REMOTE-HOST $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# 根据前端请求代理指向后端的接口地址
proxy_pass http://192.168.6.191:9100/;
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root html;
}
}
#测试环境
server {
listen 92;
server_name localhost;
client_max_body_size 20m;
location / {
root /usr/share/nginx/d2o-web/v3/test/;
try_files $uri $uri/ /index.html;
index index.html index.htm;
}
location /api/ {
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header REMOTE-HOST $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# 根据前端请求代理指向后端的接口地址
proxy_pass http://192.168.6.191:9200/;
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root html;
}
}
}
2.3、Dockerfile.jenkins配置文件
使用这个配置文件的目的主要是因为在创建容器的时候需要把 Git、Maben、Docker 客户端命令行工具 (CLI)这些需要使用到的环境一起安装进去。如果不更换阿里云镜像可以安装成功最好不更换,更换后,部分插件下载可能会网络很差造成不成功。
# 1. 使用官方的 Jenkins LTS 镜像
FROM jenkins/jenkins:lts-jdk17
# 2. 切换到 root 用户以进行系统级操作
USER root
# 不更换阿里云镜像
# RUN apt-get update && apt-get install -y git maven docker.io
# 3. 更换阿里镜像
RUN \
echo "deb https://mirrors.aliyun.com/debian/ bookworm main contrib non-free non-free-firmware" > /etc/apt/sources.list && \
echo "deb https://mirrors.aliyun.com/debian-security/ bookworm-security main contrib non-free non-free-firmware" >> /etc/apt/sources.list && \
echo "deb https://mirrors.aliyun.com/debian/ bookworm-updates main contrib non-free non-free-firmware" >> /etc/apt/sources.list && \
rm -f /etc/apt/sources.list.d/* && \
apt-get update && \
apt-get install -y --no-install-recommends \
git \
maven \
docker.io \
apt-transport-https \
ca-certificates && \
rm -rf /var/lib/apt/lists/*
# 4. 切换回 jenkins 用户,遵循安全最佳实践
USER jenkins
3、执行部署
3.1、安装容器
进入docker-compose.yml文件目录
cd /home/docker/Jenkins
执行docker命令,不用加-d到后台,因为第一次登录要使用密钥登录,日志可以看到
docker compose up

安装好了之后,访问初始登录页面:http://192.168.6.191:8088/jenkins/login

在日志复制密钥,如果日志没有了,则到对应的映射出来的/var/jenkins_home/secrets/initialAdminPassword目录下去查看

3.2、安装依赖
3.2.1、安装默认推荐插件
-
先安装推荐的依赖,安装完成后

-
安装完成后注册初始管理员账号

-
编写完成后点击保存。
注册访问的路由配置,保持默认就行,在点击保存。

-
到这一步,表示已经完成了安装了。

3.2.2、安装自定义插件
安装一些我们需要使用到的插件
-
点击右上角设置

-
下滑到
System Configuration,点击Plugins,选择Available plugins,搜索我们要安装的插件

下面的就是我们需要安装的一些插件
Docker Gitea # git仓库 SSH server NodeJS Maven Integration Plugin Locale # 语言 Generic Webhook Trigger # HTTP请求触发器 Harbor # docker仓库 Copy Artifact -
通过输入框搜索后,点击installa安装

-
然后就等待慢慢安装就好可以勾选下面的重启任务,但可能是这个版本或者哪有什么bug,关闭后,不能自动重启,需要去服务器手动执行重启。
如果安装失败,服务关闭。可以到服务器执行docker重启服务,重新安装。
docker compose restart jenkins

通过刚刚创建的账号进行重新登录

3.3、配置插件
3.3.1、配置中文插件
- 照样点击配置,下滑到System Configuration,在点击Appearance选择zh_CH点击保存。配置这个可能会成功可能会不起效果。

3.3.2、配置个人时区
- 点击个人头像
- 选择Account

- 下滑到最下面选择Time zone,把时间滑倒快底部选择Etc/GMT-8,可以看到下面的时间就为格林尼治标准时间:往后8个小时,也就是北京时间,然后点击应用或者Save保存退出。

3.3.2、配置JDK
- 点击设置选择Tools

- 下滑到JDK安装点击新增JDK,然后填写你的JDK,选择一个别名,路径填写
/opt/java/openjdk;

3.3.4、安装配置Maven
- 继续在Tools页面配置maven
- 点击新增,编写一个name,然后设置版本,填写好下载的URL。把官网要下载的对应的url复制过来就好。
我的:
https://dlcdn.apache.org/maven/maven-3/3.9.11/binaries/apache-maven-3.9.11-bin.tar.gz

3.3.4、安装配置NodeJS
- 继续在
- 下滑到NodeJS安装位置,点击新增Node JS ,填写别名,然后选择安装的版本,选择和自己项目的node版本一致就行,不用选择我的

3.3.5、配置docker
下滑到docker,点击新增填写name,勾选自动安装

3.3.6、配置Docker仓库(Harbor)和Git仓库(Gitea)凭证
-
返回设置,选择System

-
下滑到Haobor的位置(如果没有,则说明Harbor插件没下载成功,需要重新去下载一遍);点击
AddHarborButton选择Harbor Server

-
填写凭证名称,以及自己Harbor仓库的URL,然后点击Webhook Secret后面的添加

-
选择账号密码作为验证条件,把拥有读取和写入权限的账号填写进入;填写好后下滑到最下面点击添加(ID用于后面使用)

-
然后在下滑到Gitea Servers,点击新增选择Gitea server

-
同样的把名称和URL填写好,然后勾选Manage hooks,点击添加

-
选择Gitea Personal Access Token,

-
进入到GIitea,点击设置,选择应用,在点击生成新的令牌,令牌生成后,把令牌复制过去,填写到Token的位置


-
token填入,写个ID名称,点击添加

-
点击应用,Save保存
如果是其他的同类型的仓库也是一样的配置
二、部署项目
我部署成功后结构如下图,一个测试服的前后端,一个正式服的前后端

1、部署前端项目
准备一个前端仓库的代码地址。需要确认在本地可以启动并且打包成功
1.1、部署前端测试服
-
返回头像到首页,点击新建item

-
选择创建一个什么任务,选择Pipeline(流水线)任务,这个是当下最主流也是官方最推荐的创建方式,全部使用Pipeline脚本编写配置执行,不用做其他的配置,点击确认(如果没有Pipeline构建选项,需要确认自己的Pipeline插件下载成功没,没有的话得先下载安装)

-
点击确认后,会自动进入这个任务的配置页面,然后下滑到流水线 编写Pipeline 脚本的位置

-
我的前端测试服的
Pipeline脚本,根据下面我的模板改为自己的,只需要修改stage('Checkout') {}部分的Git仓库连接和stage('Deploy to Nginx'){}部分的路径 即可pipeline { agent any tools { // 步骤 1: 准备好 Node.js 环境 (提供 npm 命令) nodejs 'node-22.17' } stages { stage('Checkout') { steps { // 步骤 2: 拉取代码 git branch: 'v3-platform-web', url: 'http://192.168.6.191:3000/Onmicro/d2o-web.git', credentialsId: '191-Gitea-Jenkins' } } stage('Install Dependencies & Build') { steps { // (诊断步骤) echo "--- Environment Info ---" sh 'node -v' sh 'free -h' echo "------------------------" // 安装 pnpm sh 'npm install -g pnpm' // 验证 pnpm 并设置镜像源 --- echo "--- pnpm version ---" sh 'pnpm -v' sh 'pnpm config set registry https://registry.npmmirror.com' // 安装项目依赖 sh 'pnpm install' // 构建命令 sh 'NODE_OPTIONS=--max-old-space-size=8192 pnpm run build:demo' } } stage('Deploy to Nginx') { steps { sh ''' # 步骤 6: 部署构建产物 # 构建产物在根目录下的 dist 文件夹中 echo "正在部署 Test 环境..." rm -rf /home/jenkins-data/nginx/d2o-web/v3/test/* cp -r apps/web-antd/dist/* /home/jenkins-data/nginx/d2o-web/v3/test/ ''' } } } post { success { echo '✅ 构建并部署成功!' } failure { echo '❌ 构建或部署失败,请检查控制台输出!' } } }我的前端项目dist位置

-
修改完成后点击保存退出,点击Build Now构建。
如图下面的是每一次的构建版本以及时间,点击构建的版本。如果是绿色则代表成功,如果是红色,则代表失败

- 查看构建日志,点击构建的版本,点击Console Output则可以查看构建日志

划到最下面看到日志里提示成功,则代表成功了

然后到服务器对应的目录下去查看,可以看到对应的位置已经有打包好的前端静态文件。

1.1.1、配置Generic Webhook Trigger
Generic Webhook Trigger为一个Web 钩子,主要作用是当我们配置的git仓库有新的代码推送上去的时候,则会发生一个http请求去自动触发更新Jenkins配置的任务的构建,这样就不用我们自己去手动点击更新构建了。
-
回到任务的配置位置,找到Triggers,勾选选择Generic Webhook Trigger

2.填写配置配置Token,可以随便配置一个,但需要保持唯一性,点击保存!

-
然后再回到我们的gitea的项目仓库位置,点击设置;然后点击web钩子,在点击添加钩子,选择Gitea。

-
填写url,组成是由jenkins的首页路径拼接
generic-webhook-trigger/invoke再拼接?token=自己配置的token,如下图

-
测试,下滑到最下面点击保存后,有一个测试推送,点击测试,推送成功后会下面有一个推送记录,绿色是推送成功了。

可以看到jenkins也多了一次版本更新

1.2、部署前端正式服
-
重新返回首页构建一个项目

-
添加Pipeline 脚本,和测试服的基本上差不多没啥区别,只是把
stage('Deploy to Nginx')中对应的打包后存放到服务器的代码位置改为存放正式服的位置而已pipeline { agent any tools { // 步骤 1: 准备好 Node.js 环境 (提供 npm 命令) nodejs 'node-22.17' } stages { stage('Checkout') { steps { // 步骤 2: 拉取代码 git branch: 'v3-platform-web', url: 'http://192.168.6.191:3000/Onmicro/d2o-web.git', credentialsId: '191-Gitea-Jenkins' } } stage('Install Dependencies & Build') { steps { // 安装 pnpm sh 'npm install -g pnpm' // 设置镜像源 sh 'pnpm config set registry https://registry.npmmirror.com' // 安装项目依赖 sh 'pnpm install' // 构建命令 sh 'pnpm run build' } } stage('Deploy to Nginx') { steps { sh ''' # 部署构建产物 # 构建产物在根目录下的 dist 文件夹中 echo "正在部署 Prod 环境..." rm -rf /home/jenkins-data/nginx/d2o-web/v3/prod/* cp -r apps/web-antd/dist/* /home/jenkins-data/nginx/d2o-web/v3/prod/ ''' } } } post { success { echo '✅ 构建并部署成功!' } failure { echo '❌ 构建或部署失败,请检查控制台输出!' } } } -
修改好之后点击保存,尝试构建即可

2、部署后端微服务项目
- 测试服:最终实现的效果可以达到根据Git提交的服务和docker是否已经部署了这个容器去决定是否需要部署指定服务的容器,并且可以通过再命令定义的集合去进行强制更新部署,这样有效的避免了每次全部更新造成的资源浪费,又可以根据我们的情况去更新指定的服务。
- 正式服:根据测试服更新的服务去更新正式服,并且也可以通过定义的集合去强制更新某个服务。
2.1、部署后端测试服
-
同前端一样,创建一个Pipeline任务

-
先到harbor创建一个项目,用来存放docker镜像

-
宿主机的docker需要修改个配置,编辑Docker的配置文件
/etc/docker/daemon.json
添加insecure-registries,如果前面配置过或者使用的是https就不用管这一步。{ "insecure-registries": ["192.168.6.191:5000"] }保存文件后,执行下面的重启命令,使得配置生效
sudo systemctl daemon-reload
sudo systemctl restart docker -
编写Pipeline 脚本
buildAndPush:为打包镜像到镜像仓库的方法deployService:为从镜像仓库下载镜像然后部署到宿主机服务器docker容器的方法HARBOR_PROJECT:为harbor的项目名称FORCED_UPDATE_SERVICES:为需要强制更新的服务,因为我们是一个微服务项目嘛,每次代码更新发布时不一定全部服务都会更新,默认是根据代码更新的服务去更新发布,但有时需要重新发布一次,就把服务名称写再这个数组里
def buildAndPush(String serviceName, String modulePath) { // 使用 Jenkins 的全局变量 BUILD_NUMBER 作为镜像的版本标签 def imageTag = env.BUILD_NUMBER def versionedImageName = "${env.HARBOR_URL}/${env.HARBOR_PROJECT}/${serviceName}:${imageTag}" def latestImageName = "${env.HARBOR_URL}/${env.HARBOR_PROJECT}/${serviceName}:latest" echo ">>> 开始处理服务: ${serviceName} <<<" echo "1. 登录 Harbor: ${env.HARBOR_URL}" sh "docker login ${env.HARBOR_URL} -u ${HARBOR_USER} -p ${HARBOR_PASS}" dir(modulePath) { echo "2. 进入目录 [${modulePath}] 并开始构建 Docker 镜像..." // 同时使用 -t 参数给镜像打上两个标签 sh "docker build -t ${versionedImageName} -t ${latestImageName} --build-arg JAR_FILE=target/*.jar ." echo "3. 推送带版本号的镜像: ${versionedImageName}" sh "docker push ${versionedImageName}" // 推送带 latest 标签的镜像 echo "4. 推送带 'latest' 标签的镜像: ${latestImageName}" sh "docker push ${latestImageName}" } echo "✅ 服务 ${serviceName} 的镜像已成功推送,标签为: ${imageTag} 和 latest" } def deployService(String serviceName, String containerPort, String hostPort) { def imageTag = env.BUILD_NUMBER def fullImageName = "${env.HARBOR_URL}/${env.HARBOR_PROJECT}/${serviceName}:${imageTag}" def containerName = "${serviceName}-test" echo ">>> 开始部署服务: ${containerName} <<<" echo "1. 停止并删除旧的容器 (如果存在)" sh "docker stop ${containerName} || true" sh "docker rm ${containerName} || true" echo "2. 拉取最新的镜像: ${fullImageName}" sh "docker pull ${fullImageName}" echo "3. 运行新的容器,端口映射 ${hostPort}:${containerPort}" // 这一条命令是部署容器,需要把日志映射到宿主机方便查阅 sh """ docker run -d \\ --name ${containerName} \\ -p ${hostPort}:${containerPort} \\ -v /home/docker/java/d2o/logs:/app/logs \\ -e SPRING_PROFILES_ACTIVE=${env.SPRING_PROFILES_ACTIVE} \\ --restart=always \\ ${fullImageName} """ echo "4. 等待 15 秒,让服务有足够的时间启动..." sh "sleep 15" echo "5. 获取并打印服务 ${serviceName} 的启动日志 (从容器标准输出)" sh "docker logs --tail 100 ${containerName} || true" echo "✅ 服务 ${serviceName} 已成功部署在 http://192.168.6.190:${hostPort}" } pipeline { agent any environment { // --- 全局环境变量定义 --- HARBOR_URL = '192.168.6.191:5000' // Jenkins 中创建的 Harbor 凭据 ID HARBOR_CREDENTIALS_ID = '191-harbor' // Harbor 中的项目名称 HARBOR_PROJECT = 'd2o' // 环境 SPRING_PROFILES_ACTIVE = 'test' // 需要强制更新的服务列表,逗号分隔 (例如 'iam-service,gateway-service') FORCED_UPDATE_SERVICES = '' } stages { // 清理工作空间缓存(如果有问题可以放开这个注释先清理缓存再重新尝试) // stage('Clean Workspace') { // steps { // echo "Cleaning workspace to ensure a fresh build..." // cleanWs() // echo "Workspace cleaned." // } // } // 连接git stage('Checkout') { steps { git branch: 'v3-platform', url: 'http://192.168.6.191:3000/Onmicro/d2o-modular.git', credentialsId: ' 191-Gitea-Jenkins' } } // ===== 检测哪些服务有代码变更 ===== stage('Detect Changed Services') { steps { script { // 定义所有服务及其路径(必须和你 buildAndPush 中一致) def serviceMap = [ 'iam-service' : 'd2o-platform-iam', 'gateway-service' : 'd2o-platform-gateway', 'suite-service' : 'd2o-platform-suite', 'production-service': 'd2o-platform-production' ] def servicesToBuild = new HashSet<String>() // ========== 1. 检测 Git 变更 ========== def changedFiles = sh( script: ''' if git rev-parse HEAD~1 >/dev/null 2>&1; then git diff --name-only HEAD~1 HEAD else echo "" fi ''', returnStdout: true ).trim() if (!changedFiles.isEmpty()) { def filesList = changedFiles.split('\n') echo "🔍 检测到 Git 变更文件: ${filesList}" for (entry in serviceMap) { def serviceName = entry.key def path = entry.value if (filesList.any { file -> file.startsWith("${path}/") || file == path }) { echo "✅ Git 变更触发服务: ${serviceName}" servicesToBuild.add(serviceName) } } } else { echo "⚠️ 首次构建或无法获取 Git 变更,跳过变更检测" } // ========== 2. 检查容器是否存在 ========== echo "🔍 开始检查所有服务的容器状态..." for (serviceName in serviceMap.keySet()) { def containerName = "${serviceName}-test" // ✅ 安全写法:使用单引号包裹 shell 表达式,避免 $ 冲突 def checkCmd = """ docker ps -a --format '{{.Names}}' | grep -E '^${containerName}\$' > /dev/null """ def exitCode = sh(script: checkCmd, returnStatus: true) if (exitCode != 0) { echo "✅ 容器缺失触发服务: ${serviceName} (容器 ${containerName} 未找到)" servicesToBuild.add(serviceName) } else { echo "ℹ️ 容器 ${containerName} 已存在" } } // ========== 3. 检查强制更新服务列表 ========== if (env.FORCED_UPDATE_SERVICES && !env.FORCED_UPDATE_SERVICES.trim().isEmpty()) { def forcedServices = env.FORCED_UPDATE_SERVICES.split(',').collect { it.trim() } forcedServices.each { serviceName -> if (serviceMap.keySet().contains(serviceName)) { // 确保服务名称有效 echo "✅ 强制更新配置触发服务: ${serviceName}" servicesToBuild.add(serviceName) } else { echo "⚠️ 警告: 'FORCED_UPDATE_SERVICES' 中指定的服务 '${serviceName}' 不存在于 'serviceMap' 配置中,已忽略。" } } } else { echo "ℹ️ 未指定强制更新的服务列表。" } // ========== 4. 最终决策 ========== if (servicesToBuild.isEmpty()) { echo "ℹ️ 无 Git 变更,且所有容器均存在。跳过构建和部署。" env.TO_BUILD_SERVICES = '' } else { echo "✅ 本次需要构建和部署的服务: ${servicesToBuild.toList().join(', ')}" env.TO_BUILD_SERVICES = servicesToBuild.join(',') } } } } // ===== 构建有变更的服务===== stage('Build Changed JARs') { when { // 只有当 TO_BUILD_SERVICES 环境变量不为空时才执行 expression { env.TO_BUILD_SERVICES?.trim() } } steps { script { def serviceMap = [ 'iam-service' : 'd2o-platform-iam', 'gateway-service' : 'd2o-platform-gateway', 'suite-service' : 'd2o-platform-suite', 'production-service': 'd2o-platform-production' ] def services = env.TO_BUILD_SERVICES.split(',') def modulesToBuild = services.collect { svc -> serviceMap[svc] } if (modulesToBuild) { echo "✅ 将在项目根目录构建以下模块: ${modulesToBuild.join(',')}" sh "mvn clean package -pl ${modulesToBuild.join(',')} -am -DskipTests" } } } } // ===== 构建有变更的服务的镜像 ===== stage('Build and Push Images') { when { expression { env.TO_BUILD_SERVICES?.trim() } } steps { withCredentials([usernamePassword(credentialsId: env.HARBOR_CREDENTIALS_ID, passwordVariable: 'HARBOR_PASS', usernameVariable: 'HARBOR_USER')]) { script { def serviceMap = [ 'iam-service' : 'd2o-platform-iam', 'gateway-service' : 'd2o-platform-gateway', 'suite-service' : 'd2o-platform-suite', 'production-service': 'd2o-platform-production' ] def services = env.TO_BUILD_SERVICES.split(',') def tasks = [:] services.each { serviceName -> def modulePath = serviceMap[serviceName] if (modulePath) { tasks["Build ${serviceName}"] = { buildAndPush(serviceName, modulePath) } } else { echo "警告:在 serviceMap 中找不到服务 '${serviceName}' 的模块路径,跳过镜像构建。" } } parallel tasks } } } } // ===== 部署有变更的服务 ===== stage('Deploy Services') { when { expression { env.TO_BUILD_SERVICES?.trim() } } steps { withCredentials([usernamePassword(credentialsId: env.HARBOR_CREDENTIALS_ID, passwordVariable: 'HARBOR_PASS', usernameVariable: 'HARBOR_USER')]) { script { // 宿主机端口:服务端口 def servicePorts = [ 'iam-service' : ['5202', '5002'], 'gateway-service' : ['9200', '9000'], 'suite-service' : ['5201', '5001'], 'production-service': ['5204', '5004'] ] def services = env.TO_BUILD_SERVICES.split(',') def tasks = [:] services.each { serviceName -> def ports = servicePorts[serviceName] if (ports) { tasks["Deploy ${serviceName}"] = { deployService(serviceName, ports[1], ports[0]) } } else { echo "警告:在 servicePorts 中找不到服务 '${serviceName}' 的端口配置,跳过部署。" } } parallel tasks } } } } } post { success { echo "✅ 流水线执行成功!" script { //把新发布的写入文件,提供给正式服任务读取。 if (env.TO_BUILD_SERVICES?.trim()) { def builtServices = env.TO_BUILD_SERVICES writeFile file: 'services_built.txt', text: builtServices archiveArtifacts artifacts: 'services_built.txt', fingerprint: true echo "归档了本次构建的服务列表: ${builtServices}" } } } failure { echo "❌ 流水线执行失败,请检查日志!" } } }
4 把上面我的脚本模板改为自己的,然后点击保存退出,点击构建

5. 检查,通过日志可以看到已经构建成功了,然后去docker查看

可以看到4个服务都已经部署到宿主机的docker上去了

范围服务器也正常

2.2、部署后端正式服
-
继续新建一个Pipeline 任务

-
编写Pipeline 脚本
正式服和测试服的区别,正式服去除了去git读取代码打包镜像的步骤,因为测试服已经构建好了镜像到harbor仓库了,正式服只需要去拉取镜像来构建容器就行,然后需要去读取测试服最后一步写入的文件读取有哪些服务归档了构建。SPRING_PROFILES_ACTIVE:改为prod,这个环境变量的作用是用来拼接构建的服务的,拼接后缀为prod或者test便于区分部署的容器stage('Get Latest Test Build Number'):是先去读取测试服写入的文件,看哪些服务需要更新
/** * 部署单个服务到生产环境 * @param serviceName 服务名称 (e.g., 'iam-service') * @param containerPort 容器内部端口 * @param hostPort 映射到主机的外部端口 * @param imageTag 要部署的镜像标签 (来自测试构建号) * @return boolean 部署是否成功 */ def deployService(String serviceName, String containerPort, String hostPort, String imageTag) { // 建议:如果你希望部署特定测试构建号的镜像,这里应该使用 imageTag 而不是 :latest // 例如:def fullImageName = "${env.HARBOR_URL}/${env.HARBOR_PROJECT}/${serviceName}:${imageTag}" // 如果你的测试构建确实是推送到 :latest 标签,则保持不变。 def fullImageName = "${env.HARBOR_URL}/${env.HARBOR_PROJECT}/${serviceName}:latest" def containerName = "${serviceName}-prod" echo ">>> 开始部署服务: ${containerName} (镜像: ${fullImageName}) <<<" try { sh "docker stop ${containerName} || true" sh "docker rm ${containerName} || true" echo "1. 拉取镜像: ${fullImageName}" sh "docker pull ${fullImageName}" echo "2. 运行新容器,端口映射 ${hostPort}:${containerPort}" sh """ docker run -d \\ --name ${containerName} \\ -p ${hostPort}:${containerPort} \\ -v /home/docker/java/d2o/logs:/app/logs \\ -e SPRING_PROFILES_ACTIVE=prod \\ --restart=always \\ ${fullImageName} """ echo "3. 等待 15 秒让服务启动..." sh "sleep 15" echo "4. 检查服务启动日志" sh "docker logs --tail 50 ${containerName} || true" return true } catch (Exception e) { echo "❌ 服务 ${serviceName} 部署失败: ${e.message}" return false } } pipeline { agent any environment { HARBOR_URL = '192.168.6.191:5000' HARBOR_CREDENTIALS_ID = '191-harbor' HARBOR_PROJECT = 'd2o_platform' SPRING_PROFILES_ACTIVE = 'prod' TEST_JOB_NAME = 'd2o-platform(test)' // 正确的测试任务名称 // 需要强制更新的生产环境服务列表,逗号分隔 (例如 'iam-service,suite-service') FORCED_UPDATE_PROD_SERVICES = '' } stages { stage('Get Latest Test Build Number') { steps { script { // 修正点1: 使用 env.TEST_JOB_NAME 获取测试任务名称 def testJobName = env.TEST_JOB_NAME def testJob = Jenkins.instance.getItemByFullName(testJobName) if (testJob == null) { error("❌ Jenkins 任务未找到: ${testJobName}") } def lastSuccessful = testJob.getLastSuccessfulBuild() if (lastSuccessful == null) { error("❌ 任务 ${testJobName} 没有任何成功构建记录!") } env.TEST_BUILD_NUMBER = lastSuccessful.getNumber().toString() echo "✅ 自动获取到 Test 任务最新成功构建号: #${env.TEST_BUILD_NUMBER}" } } } // 合并三种触发条件(是否代码变更/docker是否有对应的服务) stage('Detect Services to Deploy') { steps { script { // 使用 Set 自动去重 def servicesToDeploySet = new HashSet<String>() def serviceMapping = [ // 定义服务名称和端口映射,以便进行强制更新的服务名称有效性检查 'iam-service' : [container: '5002', host: '5102'], 'gateway-service' : [container: '9000', host: '9100'], 'suite-service' : [container: '5001', host: '5101'], 'production-service': [container: '5004', host: '5104'] ] // --- 条件1: 从测试构建获取变更的服务 --- try { echo "🚚 检查 Test 构建 #${env.TEST_BUILD_NUMBER} 的变更..." copyArtifacts( projectName: env.TEST_JOB_NAME, // 修正点2: 使用 env.TEST_JOB_NAME 作为项目名称 selector: specific(env.TEST_BUILD_NUMBER), // 修正点3: 使用 buildNumber 选择器 filter: 'services_built.txt', target: '.' ) def changedServices = readFile('services_built.txt').trim() if (changedServices) { echo "✅ 因测试环境变更而触发的服务: ${changedServices}" servicesToDeploySet.addAll(changedServices.split(',')) } else { echo "ℹ️ 测试构建中无特定服务变更记录。" } } catch (e) { echo "⚠️ 无法从 Test 构建中获取变更列表,将仅检查缺失的容器和强制更新服务。错误: ${e.message}" } // --- 条件2: 检查生产环境缺失的容器 --- echo "\n🔍 开始检查生产环境缺失的容器..." for (serviceName in serviceMapping.keySet()) { def containerName = "${serviceName}-prod" def checkCmd = "docker ps -a --format '{{.Names}}' | grep -E '^${containerName}\$' > /dev/null" def exitCode = sh(script: checkCmd, returnStatus: true) if (exitCode != 0) { echo "✅ 因容器缺失而触发的服务: ${serviceName} (容器 ${containerName} 未找到)" servicesToDeploySet.add(serviceName) } else { echo "ℹ️ 容器 ${containerName} 已存在。" } } // --- 条件3: 检查强制更新服务列表 --- if (env.FORCED_UPDATE_PROD_SERVICES && !env.FORCED_UPDATE_PROD_SERVICES.trim().isEmpty()) { def forcedProdServices = env.FORCED_UPDATE_PROD_SERVICES.split(',').collect { it.trim() } forcedProdServices.each { serviceName -> if (serviceMapping.keySet().contains(serviceName)) { // 确保服务名称有效 echo "✅ 强制更新配置触发服务: ${serviceName}" servicesToDeploySet.add(serviceName) } else { echo "⚠️ 警告: 'FORCED_UPDATE_PROD_SERVICES' 中指定的服务 '${serviceName}' 不存在于有效服务列表中,已忽略。" } } } else { echo "ℹ️ 未指定强制更新的生产环境服务列表。" } // --- 最终决策 --- if (servicesToDeploySet.isEmpty()) { echo "\n✅ 无变更服务,所有容器均存在,也未指定强制更新。无需部署。" // 标记为成功并提前退出 currentBuild.result = 'SUCCESS' // 如果不需要后续的 post 阶段执行,也可以添加 stage.never() 或者在 post 中根据这个结果判断 return } else { env.SERVICES_TO_DEPLOY = servicesToDeploySet.join(',') echo "\n🎯 本次需要部署的最终服务列表: ${env.SERVICES_TO_DEPLOY}" } } } } stage('Deploy Services to Prod') { when { expression { env.SERVICES_TO_DEPLOY?.trim() } } steps { withCredentials([usernamePassword(credentialsId: env.HARBOR_CREDENTIALS_ID, passwordVariable: 'HARBOR_PASS', usernameVariable: 'HARBOR_USER')]) { // 修正 docker login 的安全警告,使用 --password-stdin sh "echo \"$HARBOR_PASS\" | docker login $HARBOR_URL -u $HARBOR_USER --password-stdin" script { def servicePorts = [ 'iam-service' : [container: '5002', host: '5102'], 'gateway-service' : [container: '9000', host: '9100'], 'suite-service' : [container: '5001', host: '5101'], 'production-service': [container: '5004', host: '5104'] ] def results = new java.util.concurrent.ConcurrentHashMap<String, Boolean>() def tasks = [:] def services = env.SERVICES_TO_DEPLOY.split(',') services.each { serviceName -> def ports = servicePorts[serviceName] if (ports) { tasks["Deploy ${serviceName}"] = { boolean success = deployService( serviceName, ports['container'], ports['host'], env.TEST_BUILD_NUMBER // 传入测试构建号作为镜像标签 ) results[serviceName] = success } } else { echo "🤷 警告: 服务 '${serviceName}' 在 servicePorts 中未找到端口配置,已跳过。" results[serviceName] = false } } parallel tasks // 输出汇总结果 echo "\n" + "=".repeat(60) echo "📊 部署结果汇总 (基于 Test 构建 #${env.TEST_BUILD_NUMBER})" echo "=".repeat(60) def allSuccess = true services.each { svc -> if (results.get(svc)) { echo "✅ ${svc} - 部署成功" } else { echo "❌ ${svc} - 部署失败或跳过" allSuccess = false } } echo "=".repeat(60) if (!allSuccess) { error("部分服务部署失败,请检查日志") } } sh 'docker logout $HARBOR_URL' } } } } post { success { echo "🎉 Prod 环境部署全部成功!使用镜像 tag: ${env.TEST_BUILD_NUMBER}" } failure { echo "💥 Prod 环境部署失败!请检查各服务状态。" } } } -
同样的根据自己项目前面改为自己的后,保存退出点击构建。构建后会报错,是因为读取测试服的文件使用了一些内部权限需要去授权

-
点击设置,下滑到Security

-
点击Approvals按钮授权,总共需要授权4个

总共需要授权6次

-
构建成功,检查日志

docker容器


1054

被折叠的 条评论
为什么被折叠?



