Jenkins 自动化部署实战
这篇文档从零搭建一套完整的 Jenkins 自动化部署流程,适用于当前 SaaS 柔性供应链项目,也适用于大多数 Spring Boot / Spring Cloud 微服务项目。
目标是实现:
开发者 push 代码到 GitLab
↓
GitLab Webhook 通知 Jenkins
↓
Jenkins 自动拉取代码
↓
Maven 执行测试和打包
↓
Docker 构建镜像
↓
推送镜像到镜像仓库
↓
Jenkins 通过 SSH 登录服务器
↓
执行部署脚本 SSH的脚本
↓
服务器拉取新镜像并重启容器
↓
健康检查
↓
成功通知或失败回滚
这套流程的价值不是“省几条命令”,而是把部署过程标准化。没有流水线时,部署经常依赖人工操作:手动拉代码、手动打包、手动上传 jar、手动重启服务。只要漏一步或者传错包,就可能造成线上事故。Jenkins 的作用是把这些步骤固化成可重复执行、可审计、可回滚的流水线。
第一节 整体架构
1.1 自动化部署全链路
flowchart LR
DEV["开发者<br>git push"] --> GL["GitLab<br>代码仓库"]
GL -->|"Webhook 事件<br>Push / Merge"| JK["Jenkins<br>Pipeline Job"]
JK -->|"git clone / checkout"| SRC["工作空间<br>拉取源码"]
SRC -->|"mvn test / package"| PKG["Maven<br>测试与打包"]
PKG -->|"docker build"| IMG["Docker 镜像<br>flexchain-xxx:commitId"]
IMG -->|"docker push"| REG["镜像仓库<br>Harbor / Docker Registry"]
JK -->|"ssh 执行部署脚本"| APP["应用服务器"]
APP -->|"docker pull"| REG
APP -->|"docker compose up -d"| RUN["运行新容器"]
RUN -->|"curl /actuator/health"| HC["健康检查"]
核心组件说明:
| 组件 | 作用 |
|---|---|
| GitLab | 保存代码,代码 push 后发送 Webhook 事件 |
| Jenkins | 接收事件,执行流水线 |
| Maven | 编译、测试、打包 Java 项目 |
| Docker | 构建应用镜像,保证运行环境一致 |
| 镜像仓库 | 保存构建后的 Docker 镜像,例如 Harbor、Docker Registry |
| SSH | Jenkins 远程登录应用服务器执行部署脚本 |
| 应用服务器 | 拉取镜像、启动容器、执行健康检查 |
1.2 本文约定的环境信息
下面的地址都是示例,真实部署时替换成自己的环境。
| 项目 | 示例 |
|---|---|
| GitLab 地址 | http://gitlab.flexchain.local |
| Jenkins 地址 | http://jenkins.flexchain.local:8080 |
| 镜像仓库 | harbor.flexchain.local |
| 后端镜像名 | harbor.flexchain.local/flexchain/flexchain-backend |
| 前端镜像名 | harbor.flexchain.local/flexchain/flexchain-web |
| 部署服务器 | 192.168.10.20 |
| 部署目录 | /opt/flexchain |
| Jenkins 凭据 ID:GitLab | gitlab-ssh-key |
| Jenkins 凭据 ID:Harbor | harbor-user-pass |
| Jenkins 凭据 ID:部署服务器 | prod-server-ssh-key |
1.3 推荐仓库结构
flexchain/
├── Jenkinsfile
├── pom.xml
├── flexchain-auth/
├── flexchain-gateway/
├── flexchain-srm/
├── flexchain-pms/
├── flexchain-wms/
├── flexchain-oms/
├── flexchain-tms/
├── flexchain-fms/
├── flexchain-bi/
├── flexchain-common/
├── deploy/
│ ├── docker-compose.prod.yml
│ ├── deploy-backend.sh
│ ├── rollback-backend.sh
│ └── nginx.conf
└── docker/
├── Dockerfile.backend
└── Dockerfile.frontend
Jenkinsfile 放在仓库根目录,由 Jenkins Pipeline 读取。deploy 目录放部署脚本和生产编排文件。docker 目录放 Dockerfile。
第二节 准备应用服务器
2.1 安装 Docker 和 Docker Compose
应用服务器负责运行容器,需要安装 Docker。
# 1. 安装依赖
sudo yum install -y yum-utils device-mapper-persistent-data lvm2
# 2. 添加 Docker 源
sudo yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
# 3. 安装 Docker
sudo yum install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
# 4. 启动 Docker
sudo systemctl enable docker
sudo systemctl start docker
# 5. 验证
docker version
docker compose version
Ubuntu 系统命令略有不同:
sudo apt update
sudo apt install -y ca-certificates curl gnupg
sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo $VERSION_CODENAME) stable" | sudo tee /etc/apt/sources.list.d/docker.list
sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
sudo systemctl enable docker
sudo systemctl start docker
2.2 创建部署目录
sudo mkdir -p /opt/flexchain
sudo mkdir -p /opt/flexchain/logs
sudo mkdir -p /opt/flexchain/backups
sudo mkdir -p /opt/flexchain/scripts
sudo chown -R deploy:deploy /opt/flexchain
这里建议创建一个专门的部署用户 deploy,不要直接用 root 执行部署。
sudo useradd deploy
sudo usermod -aG docker deploy
用户加入 docker 组后,需要重新登录才生效。
2.3 准备生产环境配置
在应用服务器 /opt/flexchain/.env.prod 放生产配置。
APP_ENV=prod
SPRING_PROFILES_ACTIVE=prod
MYSQL_HOST=mysql
MYSQL_PORT=3306
MYSQL_DATABASE=flexchain
MYSQL_USERNAME=flexchain_app
MYSQL_PASSWORD=change_me
REDIS_HOST=redis
REDIS_PORT=6379
REDIS_PASSWORD=change_me
NACOS_HOST=nacos
NACOS_PORT=8848
ROCKETMQ_NAMESRV=rocketmq-namesrv:9876
IMAGE_BACKEND=harbor.flexchain.local/flexchain/flexchain-backend
IMAGE_TAG=latest
.env.prod 不能提交到 GitLab。生产密码、Redis 密码、OSS 密钥、短信密钥等都应该由服务器配置或配置中心管理。
2.4 准备 docker-compose.prod.yml
生产环境的 docker-compose.prod.yml 放在 /opt/flexchain,也可以由 Jenkins 首次部署时上传。
version: "3.9"
services:
flexchain-backend:
image: ${IMAGE_BACKEND}:${IMAGE_TAG}
container_name: flexchain-backend
restart: always
env_file:
- .env.prod
ports:
- "8080:8080"
volumes:
- ./logs/backend:/app/logs
networks:
- flexchain-net
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/actuator/health"]
interval: 30s
timeout: 5s
retries: 5
start_period: 60s
networks:
flexchain-net:
external: true
生产环境中 MySQL、Redis、Nacos、RocketMQ 可以和业务服务一起用 compose 编排,也可以由独立中间件集群提供。真实生产更推荐中间件独立部署,应用服务只连接这些中间件。
第三节 准备 Jenkins
3.1 Jenkins 安装方式
Jenkins 可以直接安装到服务器,也可以用 Docker 运行。这里使用 Docker Compose 部署 Jenkins。
在 Jenkins 服务器创建目录:
mkdir -p /opt/jenkins/jenkins_home
mkdir -p /opt/jenkins/maven_repo
创建 /opt/jenkins/docker-compose.yml:
version: "3.9"
services:
jenkins:
image: jenkins/jenkins:lts-jdk17
container_name: jenkins
restart: always
user: root
ports:
- "8080:8080"
- "50000:50000"
volumes:
- ./jenkins_home:/var/jenkins_home
- ./maven_repo:/root/.m2
- /var/run/docker.sock:/var/run/docker.sock
- /usr/bin/docker:/usr/bin/docker
启动 Jenkins:
cd /opt/jenkins
docker compose up -d
docker logs -f jenkins
首次启动时日志里会打印初始化密码:
Please use the following password to proceed to installation:
xxxxxxxxxxxxxxxxxxxxxxxx
浏览器访问:
http://jenkins.flexchain.local:8080
输入初始密码,安装推荐插件。
3.2 Jenkins 必装插件
进入 Jenkins 后安装以下插件:
| 插件 | 用途 |
|---|---|
| Git | 拉取 Git 仓库代码 |
| GitLab | 接收 GitLab Webhook,展示提交状态 |
| Pipeline | 支持 Jenkinsfile 流水线 |
| Credentials Binding | 在流水线中安全读取凭据 |
| SSH Agent | 使用 SSH 私钥登录远程服务器 |
| Docker Pipeline | 在 Pipeline 中执行 Docker 构建 |
| Maven Integration | 支持 Maven 构建 |
| Blue Ocean | 可视化查看流水线阶段 |
安装路径:
Manage Jenkins
-> Plugins
-> Available plugins
-> 搜索插件名称
-> Install
安装完成后重启 Jenkins。
3.3 配置 Maven 和 JDK
如果 Jenkins 容器里已经有 JDK,可以直接使用容器自带 JDK。Maven 可以在 Jenkins 全局工具里配置。
路径:
Manage Jenkins
-> Tools
-> JDK installations
-> Maven installations
配置 Maven:
Name: Maven-3.9
Install automatically: 勾选
Version: 3.9.x
如果公司内网有 Maven 私服,需要准备 settings.xml,并在 Jenkins 中配置 Maven 使用私服。
示例 settings.xml:
<settings>
<mirrors>
<mirror>
<id>aliyunmaven</id>
<mirrorOf>*</mirrorOf>
<name>阿里云公共仓库</name>
<url>https://maven.aliyun.com/repository/public</url>
</mirror>
</mirrors>
</settings>
3.4 配置 Jenkins 凭据
Jenkins 不应该把密码、私钥写到 Jenkinsfile 中,而是统一放在 Credentials。
路径:
Manage Jenkins
-> Credentials
-> System
-> Global credentials
-> Add Credentials
需要创建三类凭据。
GitLab SSH 私钥
Kind: SSH Username with private key
ID: gitlab-ssh-key
Username: git
Private Key: GitLab 可访问仓库的私钥
这个凭据用于 Jenkins 拉取 GitLab 代码。
镜像仓库账号密码
Kind: Username with password
ID: harbor-user-pass
Username: harbor 用户名
Password: harbor 密码
这个凭据用于 docker login。
生产服务器 SSH 私钥
Kind: SSH Username with private key
ID: prod-server-ssh-key
Username: deploy
Private Key: deploy 用户对应的私钥
这个凭据用于 Jenkins 登录应用服务器执行部署脚本。
3.5 Jenkins 访问 GitLab 的 SSH Key
在 Jenkins 服务器生成 SSH Key:
ssh-keygen -t rsa -b 4096 -C "jenkins@gitlab" -f ~/.ssh/jenkins_gitlab
把公钥内容复制到 GitLab:
cat ~/.ssh/jenkins_gitlab.pub
GitLab 配置位置:
GitLab
-> User Settings
-> SSH Keys
-> Add new key
如果使用 Deploy Key,也可以添加到具体项目:
Project
-> Settings
-> Repository
-> Deploy keys
第四节 GitLab Webhook 触发 Jenkins
4.1 Jenkins 创建 Pipeline Job
Jenkins 首页点击:
New Item
输入任务名:
flexchain-backend-prod
选择:
Pipeline
进入任务配置。
4.2 配置 GitLab 仓库
Pipeline 定义选择:
Pipeline script from SCM
SCM 选择:
Git
Repository URL:
git@gitlab.flexchain.local:flexchain/flexchain.git
Credentials:
gitlab-ssh-key
Branches to build:
*/main
Script Path:
Jenkinsfile
这表示 Jenkins 每次构建时,会先从 GitLab 拉取 main 分支,然后读取仓库根目录的 Jenkinsfile 执行流水线。
4.3 配置构建触发器
在 Jenkins Job 配置中找到:
Build Triggers
勾选:
Build when a change is pushed to GitLab
Jenkins 会显示一个 Webhook 地址,通常类似:
http://jenkins.flexchain.local:8080/project/flexchain-backend-prod
同时建议配置一个 Secret Token,例如:
flexchain-prod-webhook-secret
Secret Token 用于防止别人伪造 GitLab Webhook 请求。
4.4 GitLab 配置 Webhook
进入 GitLab 项目:
Project
-> Settings
-> Webhooks
填写 URL:
http://jenkins.flexchain.local:8080/project/flexchain-backend-prod
填写 Secret Token:
flexchain-prod-webhook-secret
勾选触发事件:
Push events
Merge request events
Tag push events
如果只希望 main 分支部署生产,可以在 Jenkinsfile 中判断分支,也可以在 GitLab Webhook 中限制分支。
点击:
Test -> Push events
如果 Jenkins 收到请求,会触发一次构建。
4.5 Webhook 触发后的完整过程
sequenceDiagram
participant Dev as 开发者
participant GitLab as GitLab
participant Jenkins as Jenkins
participant Repo as Jenkins工作空间
Dev->>GitLab: git push origin main
GitLab->>Jenkins: Webhook 推送事件
Jenkins->>Jenkins: 校验 Secret Token
Jenkins->>GitLab: 根据 Jenkinsfile SCM 配置拉取代码
GitLab-->>Repo: 返回最新代码
Jenkins->>Repo: 执行 Jenkinsfile
这一步的核心是:Jenkins 不是一直轮询代码仓库,而是 GitLab 在代码变化后主动通知 Jenkins。
第五节 Dockerfile 设计
5.1 后端 Dockerfile
docker/Dockerfile.backend:
FROM eclipse-temurin:21-jre
WORKDIR /app
ARG JAR_FILE
COPY ${JAR_FILE} /app/app.jar
RUN mkdir -p /app/logs
ENV JAVA_OPTS="-Xms512m -Xmx512m"
ENV SPRING_PROFILES_ACTIVE=prod
EXPOSE 8080
ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -Dspring.profiles.active=$SPRING_PROFILES_ACTIVE -jar /app/app.jar"]
这个 Dockerfile 不负责 Maven 打包,只负责运行 jar。打包由 Jenkins 的 Maven 阶段完成,这样构建过程更清晰。
5.2 为什么镜像里不写生产密码
镜像应该只包含应用程序和运行环境,不应该包含数据库密码、Redis 密码、OSS 密钥等敏感配置。
正确方式是:
镜像:保存代码和运行环境
.env.prod:保存生产配置
Nacos:保存业务配置
Jenkins Credentials:保存流水线敏感凭据
同一个镜像可以部署到测试环境、预发环境、生产环境,只要加载不同配置即可。
第六节 Jenkinsfile 完整示例
6.1 单体后端服务 Pipeline
下面的 Jenkinsfile 适合先讲清楚完整链路。微服务项目可以在这个基础上扩展成多模块构建。
pipeline {
agent any
tools {
maven 'Maven-3.9'
}
environment {
APP_NAME = 'flexchain-backend'
REGISTRY = 'harbor.flexchain.local'
IMAGE_REPO = 'harbor.flexchain.local/flexchain/flexchain-backend'
DEPLOY_HOST = '192.168.10.20'
DEPLOY_DIR = '/opt/flexchain'
DOCKERFILE = 'docker/Dockerfile.backend'
}
options {
timestamps()
disableConcurrentBuilds()
buildDiscarder(logRotator(numToKeepStr: '20'))
}
stages {
stage('Checkout') {
steps {
checkout scm
script {
env.GIT_COMMIT_SHORT = sh(
script: "git rev-parse --short HEAD",
returnStdout: true
).trim()
env.IMAGE_TAG = "${env.BRANCH_NAME ?: 'main'}-${env.GIT_COMMIT_SHORT}-${env.BUILD_NUMBER}"
}
echo "当前构建镜像标签: ${IMAGE_TAG}"
}
}
stage('Maven Test') {
steps {
sh 'mvn clean test -B --no-transfer-progress'
}
post {
always {
junit allowEmptyResults: true, testResults: '**/target/surefire-reports/*.xml'
}
}
}
stage('Maven Package') {
steps {
sh 'mvn package -DskipTests -B --no-transfer-progress'
}
}
stage('Docker Build') {
steps {
sh '''
JAR_FILE=$(find . -path "*/target/*.jar" ! -name "*sources.jar" ! -name "*javadoc.jar" | head -n 1)
echo "使用 JAR 文件: ${JAR_FILE}"
docker build \
-f ${DOCKERFILE} \
--build-arg JAR_FILE=${JAR_FILE} \
-t ${IMAGE_REPO}:${IMAGE_TAG} \
-t ${IMAGE_REPO}:latest \
.
'''
}
}
stage('Docker Push') {
steps {
withCredentials([usernamePassword(
credentialsId: 'harbor-user-pass',
usernameVariable: 'HARBOR_USER',
passwordVariable: 'HARBOR_PASS'
)]) {
sh '''
echo "${HARBOR_PASS}" | docker login ${REGISTRY} -u "${HARBOR_USER}" --password-stdin
docker push ${IMAGE_REPO}:${IMAGE_TAG}
docker push ${IMAGE_REPO}:latest
'''
}
}
}
stage('Deploy') {
when {
branch 'main'
}
steps {
sshagent(credentials: ['prod-server-ssh-key']) {
sh '''
ssh -o StrictHostKeyChecking=no deploy@${DEPLOY_HOST} "
cd ${DEPLOY_DIR} &&
IMAGE_TAG=${IMAGE_TAG} IMAGE_BACKEND=${IMAGE_REPO} bash scripts/deploy-backend.sh
"
'''
}
}
}
stage('Health Check') {
when {
branch 'main'
}
steps {
sshagent(credentials: ['prod-server-ssh-key']) {
sh '''
ssh -o StrictHostKeyChecking=no deploy@${DEPLOY_HOST} "
curl -f http://localhost:8080/actuator/health
"
'''
}
}
}
}
post {
success {
echo "流水线执行成功,镜像: ${IMAGE_REPO}:${IMAGE_TAG}"
}
failure {
echo "流水线执行失败,请查看 Jenkins 控制台日志"
}
always {
sh 'docker image prune -f || true'
}
}
}
6.2 每个阶段在做什么
| 阶段 | 作用 |
|---|---|
| Checkout | 从 GitLab 拉取代码,生成镜像标签 |
| Maven Test | 执行单元测试,失败则停止流水线 |
| Maven Package | 打包 jar |
| Docker Build | 根据 Dockerfile 构建镜像 |
| Docker Push | 登录 Harbor,推送镜像 |
| Deploy | SSH 到服务器执行部署脚本 |
| Health Check | 调用健康检查接口确认服务可用 |
6.3 为什么镜像标签要带 commitId
不要只使用 latest。latest 无法追溯具体代码版本,也不方便回滚。
推荐标签格式:
main-8f3a21c-156
含义:
分支名-commit短ID-Jenkins构建号
这样线上容器运行哪个版本,一眼就能追溯到 GitLab 提交记录和 Jenkins 构建记录。
第七节 部署脚本
7.1 deploy-backend.sh
应用服务器 /opt/flexchain/scripts/deploy-backend.sh:
#!/bin/bash
set -e
APP_NAME="flexchain-backend"
DEPLOY_DIR="/opt/flexchain"
COMPOSE_FILE="${DEPLOY_DIR}/docker-compose.prod.yml"
ENV_FILE="${DEPLOY_DIR}/.env.prod"
BACKUP_DIR="${DEPLOY_DIR}/backups"
if [ -z "${IMAGE_TAG}" ]; then
echo "IMAGE_TAG 不能为空"
exit 1
fi
if [ -z "${IMAGE_BACKEND}" ]; then
echo "IMAGE_BACKEND 不能为空"
exit 1
fi
cd "${DEPLOY_DIR}"
echo "开始部署 ${APP_NAME}"
echo "镜像: ${IMAGE_BACKEND}:${IMAGE_TAG}"
mkdir -p "${BACKUP_DIR}"
if docker ps --format '{{.Names}}' | grep -q "^${APP_NAME}$"; then
OLD_IMAGE=$(docker inspect --format='{{.Config.Image}}' "${APP_NAME}")
echo "${OLD_IMAGE}" > "${BACKUP_DIR}/${APP_NAME}.last-image"
echo "当前旧镜像: ${OLD_IMAGE}"
else
echo "当前没有运行中的旧容器"
fi
echo "更新 .env.prod 中的镜像配置"
grep -v '^IMAGE_TAG=' "${ENV_FILE}" > "${ENV_FILE}.tmp"
echo "IMAGE_TAG=${IMAGE_TAG}" >> "${ENV_FILE}.tmp"
mv "${ENV_FILE}.tmp" "${ENV_FILE}"
grep -v '^IMAGE_BACKEND=' "${ENV_FILE}" > "${ENV_FILE}.tmp"
echo "IMAGE_BACKEND=${IMAGE_BACKEND}" >> "${ENV_FILE}.tmp"
mv "${ENV_FILE}.tmp" "${ENV_FILE}"
echo "拉取新镜像"
docker pull "${IMAGE_BACKEND}:${IMAGE_TAG}"
echo "启动新版本容器"
docker compose --env-file "${ENV_FILE}" -f "${COMPOSE_FILE}" up -d --no-deps "${APP_NAME}"
echo "等待服务启动"
sleep 20
echo "执行健康检查"
for i in {1..10}; do
if curl -f http://localhost:8080/actuator/health; then
echo "健康检查通过"
docker image prune -f
exit 0
fi
echo "第 ${i} 次健康检查失败,等待重试"
sleep 6
done
echo "健康检查失败,开始回滚"
bash "${DEPLOY_DIR}/scripts/rollback-backend.sh"
exit 1
给脚本执行权限:
chmod +x /opt/flexchain/scripts/deploy-backend.sh
7.2 rollback-backend.sh
应用服务器 /opt/flexchain/scripts/rollback-backend.sh:
#!/bin/bash
set -e
APP_NAME="flexchain-backend"
DEPLOY_DIR="/opt/flexchain"
COMPOSE_FILE="${DEPLOY_DIR}/docker-compose.prod.yml"
ENV_FILE="${DEPLOY_DIR}/.env.prod"
BACKUP_FILE="${DEPLOY_DIR}/backups/${APP_NAME}.last-image"
if [ ! -f "${BACKUP_FILE}" ]; then
echo "没有找到上一版本镜像记录,无法自动回滚"
exit 1
fi
OLD_IMAGE=$(cat "${BACKUP_FILE}")
if [ -z "${OLD_IMAGE}" ]; then
echo "上一版本镜像为空,无法回滚"
exit 1
fi
OLD_REPO=$(echo "${OLD_IMAGE}" | cut -d ':' -f 1)
OLD_TAG=$(echo "${OLD_IMAGE}" | cut -d ':' -f 2)
echo "开始回滚到 ${OLD_IMAGE}"
grep -v '^IMAGE_BACKEND=' "${ENV_FILE}" > "${ENV_FILE}.tmp"
echo "IMAGE_BACKEND=${OLD_REPO}" >> "${ENV_FILE}.tmp"
mv "${ENV_FILE}.tmp" "${ENV_FILE}"
grep -v '^IMAGE_TAG=' "${ENV_FILE}" > "${ENV_FILE}.tmp"
echo "IMAGE_TAG=${OLD_TAG}" >> "${ENV_FILE}.tmp"
mv "${ENV_FILE}.tmp" "${ENV_FILE}"
docker pull "${OLD_IMAGE}"
docker compose --env-file "${ENV_FILE}" -f "${COMPOSE_FILE}" up -d --no-deps "${APP_NAME}"
sleep 20
curl -f http://localhost:8080/actuator/health
echo "回滚完成"
给脚本执行权限:
chmod +x /opt/flexchain/scripts/rollback-backend.sh
7.3 为什么部署脚本要放服务器
Jenkinsfile 负责流水线控制,服务器脚本负责具体部署动作。这样有几个好处:
- Jenkinsfile 不会塞满复杂 shell。
- 服务器部署逻辑可以单独测试。
- 手动应急时也可以直接执行脚本。
- 回滚逻辑和部署逻辑放在同一台服务器,更容易读取当前容器状态。
第八节 微服务项目如何扩展
8.1 单服务构建
如果只构建某一个微服务,例如 OMS:
mvn clean package -pl flexchain-oms -am -DskipTests
含义:
| 参数 | 说明 |
|---|---|
-pl flexchain-oms | 只构建 OMS 模块 |
-am | 同时构建 OMS 依赖的模块 |
-DskipTests | 打包阶段跳过测试,测试已在前面阶段执行 |
8.2 Jenkins 参数化构建
可以把 Jenkins Job 做成参数化,选择部署哪个服务。
Jenkinsfile 示例:
parameters {
choice(
name: 'SERVICE_NAME',
choices: ['flexchain-auth', 'flexchain-gateway', 'flexchain-oms', 'flexchain-wms', 'flexchain-tms'],
description: '选择要构建和部署的服务'
)
}
打包命令:
sh "mvn clean package -pl ${params.SERVICE_NAME} -am -DskipTests -B --no-transfer-progress"
镜像名:
environment {
IMAGE_REPO = "harbor.flexchain.local/flexchain/${params.SERVICE_NAME}"
}
8.3 多服务批量部署
如果一次提交影响多个服务,可以设计服务列表:
def services = ['flexchain-auth', 'flexchain-gateway', 'flexchain-oms']
for (svc in services) {
sh "mvn clean package -pl ${svc} -am -DskipTests"
sh "docker build -f docker/Dockerfile.backend -t ${REGISTRY}/flexchain/${svc}:${IMAGE_TAG} ."
sh "docker push ${REGISTRY}/flexchain/${svc}:${IMAGE_TAG}"
}
批量部署要谨慎。生产环境更推荐一次只发布一个或少量服务,避免多个服务同时异常时难以定位。
第九节 部署过程中的关键细节
9.1 为什么测试失败不能继续部署
流水线最重要的原则是:
测试失败,禁止部署。
如果单元测试或集成测试失败还继续构建镜像,就会把已知有问题的代码推到测试环境甚至生产环境。Jenkinsfile 中 mvn test 失败时,后续阶段会自动停止。
9.2 为什么 Jenkins 要使用凭据
GitLab 私钥、Harbor 密码、服务器 SSH 私钥都属于敏感信息,不能写在 Jenkinsfile、shell 脚本或 Git 仓库里。
正确做法是:
Jenkins Credentials 保存敏感信息。
Jenkinsfile 通过 credentialsId 引用。
构建日志中不打印密码。
9.3 为什么要健康检查
容器启动成功不代表服务可用。Spring Boot 启动可能失败,数据库连接可能失败,Redis 可能不可用,Nacos 配置可能拉取失败。
健康检查接口:
GET /actuator/health
只有健康检查通过,才能认为部署成功。
9.4 为什么要保留上一版本镜像
部署失败时需要回滚。回滚不是重新打包,而是让服务器重新启动上一版本镜像。
所以部署前要记录旧镜像:
docker inspect --format='{{.Config.Image}}' flexchain-backend
保存到:
/opt/flexchain/backups/flexchain-backend.last-image
9.5 为什么不要把 .env.prod 提交到 GitLab
.env.prod 里有数据库密码、Redis 密码、OSS 密钥、短信密钥等敏感配置。提交到 GitLab 会造成安全风险。正确做法是:
- 本地提交
.env.example作为模板。 - 生产服务器保存真实
.env.prod。 - Jenkins 只传镜像版本,不传生产密码。
9.6 为什么 Jenkins 服务器需要访问 Docker
Jenkins 要执行:
docker build
docker push
如果 Jenkins 运行在 Docker 容器里,需要挂载宿主机 Docker Socket:
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- /usr/bin/docker:/usr/bin/docker
这样 Jenkins 容器里的 docker 命令实际操作的是宿主机 Docker。
这种方式方便,但也要注意安全:拥有 Docker Socket 权限基本等价于拥有宿主机较高权限,所以 Jenkins 服务器要限制访问。
第十节 前端项目如何部署
前端一般是 Vue 或 React 项目。部署方式是:Jenkins 拉代码,执行 npm install 和 npm run build,然后构建 Nginx 镜像或把 dist 上传到服务器。
10.1 前端 Dockerfile
docker/Dockerfile.frontend:
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm config set registry https://registry.npmmirror.com
RUN npm ci
COPY . .
RUN npm run build
FROM nginx:1.25-alpine
COPY --from=builder /app/dist /usr/share/nginx/html
COPY deploy/nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
10.2 前端 Jenkins 阶段
stage('Build Frontend Image') {
steps {
sh '''
docker build \
-f docker/Dockerfile.frontend \
-t harbor.flexchain.local/flexchain/flexchain-web:${IMAGE_TAG} \
-t harbor.flexchain.local/flexchain/flexchain-web:latest \
.
'''
}
}
stage('Push Frontend Image') {
steps {
withCredentials([usernamePassword(
credentialsId: 'harbor-user-pass',
usernameVariable: 'HARBOR_USER',
passwordVariable: 'HARBOR_PASS'
)]) {
sh '''
echo "${HARBOR_PASS}" | docker login harbor.flexchain.local -u "${HARBOR_USER}" --password-stdin
docker push harbor.flexchain.local/flexchain/flexchain-web:${IMAGE_TAG}
docker push harbor.flexchain.local/flexchain/flexchain-web:latest
'''
}
}
}
前端和后端可以放在同一个 Jenkinsfile 里,也可以拆成两个 Job。中小项目可以合并,微服务较多时建议拆分。
第十一节 常见问题排查
11.1 GitLab Webhook 没有触发 Jenkins
检查点:
- GitLab Webhook URL 是否正确。
- Jenkins Job 是否勾选 GitLab 触发器。
- Secret Token 是否一致。
- Jenkins 地址是否能被 GitLab 访问。
- Jenkins GitLab 插件是否安装。
- GitLab Webhook 测试返回是否 200。
GitLab Webhook 页面会显示最近请求记录,可以查看返回状态码和错误信息。
11.2 Jenkins 拉代码失败
常见原因:
- GitLab SSH Key 没配置。
- Jenkins Credentials 选错。
- 仓库地址写错。
- Jenkins 服务器无法访问 GitLab。
- GitLab Deploy Key 没有项目权限。
可以在 Jenkins 服务器测试:
ssh -T git@gitlab.flexchain.local
11.3 Maven 下载依赖很慢
解决方式:
- 配置 Maven 镜像源。
- 使用公司 Nexus 私服。
- 挂载 Maven 本地仓库目录。
- Jenkins Pipeline 开启 Maven 缓存。
11.4 Docker build 失败
常见原因:
- Dockerfile 路径错误。
- jar 文件路径没找到。
- Jenkins 容器没有 docker 命令。
- Jenkins 没有访问 Docker Socket 权限。
- 基础镜像拉取失败。
排查命令:
docker version
docker images
find . -path "*/target/*.jar"
11.5 Docker push 失败
常见原因:
- Harbor 地址错误。
- 账号密码错误。
- 镜像命名不符合仓库规则。
- Harbor 项目不存在。
- Jenkins 服务器无法访问 Harbor。
手动测试:
docker login harbor.flexchain.local
docker push harbor.flexchain.local/flexchain/flexchain-backend:test
11.6 SSH 部署失败
常见原因:
- Jenkins 凭据 ID 不正确。
- deploy 用户私钥错误。
- 服务器防火墙不允许 22 端口。
- deploy 用户没有 docker 权限。
/opt/flexchain权限不足。
手动测试:
ssh deploy@192.168.10.20
docker ps
cd /opt/flexchain
11.7 容器启动成功但健康检查失败
常见原因:
- Spring Boot 启动失败。
- 端口映射错误。
.env.prod配置错误。- 数据库、Redis、Nacos 不可用。
- Actuator 没开放 health 端点。
排查命令:
docker logs -f flexchain-backend
docker inspect flexchain-backend
curl -v http://localhost:8080/actuator/health
第十二节 面试怎么讲
12.1 简历写法
可以这样写:
负责项目自动化部署流水线建设,基于 Jenkins + GitLab Webhook + Maven + Docker + Harbor + SSH 实现从代码提交到测试、打包、镜像构建、镜像推送、服务器部署、健康检查和失败回滚的完整 CI/CD 流程。通过 Jenkins Credentials 管理 GitLab、镜像仓库和生产服务器凭据,避免敏感信息写入代码仓库。
12.2 面试表达
可以这样讲:
“我们项目使用 Jenkins 做自动化部署。开发者把代码 push 到 GitLab 后,GitLab 通过 Webhook 通知 Jenkins。Jenkins 接收到事件后,根据 Pipeline Job 配置从 GitLab 拉取代码,然后执行 Jenkinsfile。
流水线分成几个阶段。第一步是 Checkout,拉取代码并生成镜像标签,标签里包含分支、commitId 和 Jenkins 构建号,方便追溯版本。第二步是 Maven Test,执行单元测试,测试失败就直接终止流水线。第三步是 Maven Package,把 Spring Boot 项目打成 jar。第四步是 Docker Build,根据 Dockerfile 构建镜像。第五步是 Docker Push,把镜像推送到 Harbor。第六步是 Deploy,Jenkins 使用 SSH 凭据登录生产服务器,执行部署脚本。服务器脚本会拉取新镜像、更新 docker-compose、启动新容器,并调用 /actuator/health 做健康检查。如果健康检查失败,就自动回滚到上一版本镜像。
这里有几个关键点。第一,敏感信息不写在 Jenkinsfile 里,而是放在 Jenkins Credentials。第二,镜像标签不用单纯 latest,而是带 commitId 和 build number,方便追溯和回滚。第三,部署脚本放在服务器上,Jenkins 只负责触发,这样应急时也能手动执行。第四,容器启动不等于服务可用,必须做健康检查。”
12.3 高频问题
Q1:GitLab push 后 Jenkins 是怎么知道的?
答:通过 GitLab Webhook。GitLab 项目配置 Jenkins Job 的 Webhook URL。代码 push 或合并请求发生时,GitLab 主动向 Jenkins 发送 HTTP 请求,Jenkins 收到后触发 Pipeline。
Q2:Jenkins 如何拉取 GitLab 代码?
答:Jenkins Job 配置 SCM 为 Git,填写 GitLab SSH 仓库地址,并选择 GitLab SSH 私钥凭据。构建时 Jenkins 使用这个凭据执行 git clone 或 checkout。
Q3:为什么测试失败不能继续部署?
答:测试失败说明当前代码已经存在确定问题。如果继续打包部署,会把有问题的代码推到环境中。流水线应该把测试作为质量门禁,测试失败后直接停止。
Q4:为什么要构建 Docker 镜像?
答:Docker 镜像把应用、JRE、启动命令和运行环境打包在一起,避免不同服务器环境不一致。镜像可以在测试、预发、生产复用,只需要加载不同配置。
Q5:为什么镜像标签不能只用 latest?
答:latest 无法追溯具体代码版本,也不方便回滚。推荐使用 分支-commitId-构建号 作为镜像标签,线上运行哪个版本可以直接对应到 GitLab 提交和 Jenkins 构建记录。
Q6:Jenkins 如何登录生产服务器?
答:通过 SSH Agent 插件和 Jenkins Credentials 中保存的 SSH 私钥。Jenkinsfile 中使用 sshagent(credentials: ['prod-server-ssh-key']),然后执行 ssh 命令登录服务器运行部署脚本。
Q7:为什么部署脚本不全部写在 Jenkinsfile 里?
答:Jenkinsfile 负责流水线编排,服务器部署脚本负责具体部署动作。分开后结构更清晰,也方便在服务器上手动应急执行部署或回滚。
Q8:如何实现失败回滚?
答:部署前记录当前运行容器的旧镜像。新镜像启动后执行健康检查,如果连续失败,就读取旧镜像记录,修改 .env.prod 中的镜像标签,重新启动旧版本容器。
Q9:如何保证生产配置安全?
答:生产密码不写在 GitLab 和 Jenkinsfile 里。流水线凭据放 Jenkins Credentials,应用配置放服务器 .env.prod 或 Nacos 配置中心。Git 仓库只提交 .env.example。
Q10:Jenkins 和 GitLab、Harbor、服务器之间需要哪些凭据?
答:Jenkins 拉 GitLab 需要 GitLab SSH 私钥;推送镜像到 Harbor 需要 Harbor 账号密码;SSH 登录服务器需要部署用户私钥。这些都放在 Jenkins Credentials 中,通过 credentialsId 引用。
第十三节 最终流程清单
上线前检查:
- GitLab 仓库已创建,Jenkins SSH Key 有读取权限。
- Jenkins 已安装 Git、GitLab、Pipeline、Docker、SSH Agent、Credentials Binding 插件。
- Jenkins 已配置 GitLab、Harbor、生产服务器三类凭据。
- GitLab Webhook 已配置并测试成功。
- Jenkinsfile 已提交到仓库根目录。
- Dockerfile 能在 Jenkins 服务器构建成功。
- Harbor 项目已创建,Jenkins 能登录并 push 镜像。
- 应用服务器已安装 Docker 和 Docker Compose。
/opt/flexchain/.env.prod已准备完成。/opt/flexchain/docker-compose.prod.yml已准备完成。/opt/flexchain/scripts/deploy-backend.sh有执行权限。/opt/flexchain/scripts/rollback-backend.sh有执行权限。- Spring Boot 已开放
/actuator/health。 - Jenkins 构建成功后能 SSH 到服务器执行部署。
- 新容器启动后健康检查通过。
完整部署链路:
git push
-> GitLab Webhook
-> Jenkins Pipeline
-> checkout
-> mvn test
-> mvn package
-> docker build
-> docker push
-> ssh deploy@server
-> docker pull
-> docker compose up -d
-> curl /actuator/health
-> success or rollback 