Jenkins / CI/CD

本文范围
这篇讲 Jenkins 如何把“代码提交到生产回滚”串成可治理的 CI/CD 流水线:Jenkins LTS 选择、Java 运行时、Linux/Docker 安装、插件、凭证、权限、controller/agent 分工、Jenkinsfile、Maven/Node 构建、镜像构建推送、部署脚本、发布验证、回滚、质量门禁、日志排障和离线交付。
不展开 Docker、K8S、Harbor、Nexus、SonarQube 的完整教程。它们在本文里只是流水线节点:构建在哪里跑、制品放哪里、镜像推哪里、部署到哪里、失败怎么查。
前置条件
落地 Jenkins 前,先把这些边界定好:
| 项目 | 要求 |
|---|---|
| 服务器 | controller 使用独立节点,JENKINS_HOME 独立磁盘并纳入备份 |
| Java | 以上线当天 Jenkins Java 支持策略为准;新 LTS 基线要同步升级 controller 和 agent JVM |
| 账号 | 管理员、开发、发布、只读、机器人账号分开,不开匿名危险权限 |
| 仓库 | Git、Maven/Nexus、npm/Nexus、Harbor 或镜像仓库可访问 |
| 凭证 | Git token、仓库账号、SSH key、Kubeconfig、部署密钥进入 Jenkins 凭证管理 |
| 网络 | 中国大陆、企业内网或离线环境要提前准备插件、JDK、构建依赖、镜像和制品仓库 |
Jenkins 是发布入口,不是一个随便重装的工具。先把版本、插件、凭证、备份和回滚设计好,再让团队把生产发布交给它。
先把这条流水线跑通
很多团队第一次用 Jenkins,容易把它当成一个“远程点按钮”的工具:页面上点一下,服务器上拉代码、打包、重启服务。这样当然比手工登录服务器好一点,但还不算真正的 CI/CD。
真正要管的是这一条链路:
代码提交
-> Jenkins 触发
-> 拉取源码
-> Maven 或 Node 构建
-> 执行测试和质量检查
-> 生成 jar 或前端 dist
-> 构建 Docker 镜像
-> 推送到镜像仓库
-> 部署到目标环境
-> 健康检查
-> 记录本次版本
-> 失败时回滚这里要注意,Jenkins 只是把这些步骤串起来。它不会自动帮你设计版本号,不会自动保护密码,也不会自动判断服务是否真的可用。命令能跑完,只代表命令返回了 0;服务能不能访问、版本能不能追溯、失败能不能回滚,还得我们自己补齐。
先看一张简化图:
后面的配置都围绕这张图展开。每一段都按一个固定思路来做:先说明它解决什么问题,然后给命令或配置,接着看结果,最后讲常见坑和生产建议。
本文不展开 Docker、K8S、GitLab CI、Harbor、Nexus、SonarQube 的完整教程。它们只作为流水线里的工具出现。重点是把从提交到回滚的工程闭环打通。
Jenkins 版本先别乱选
装 Jenkins 前,先看两件事:Jenkins 版本和 Java 版本。
截至 2026-07-10,Jenkins 官网下载页显示的 LTS 版本是 2.568.1,weekly 版本是 2.572。生产环境一般选 LTS,不要为了“新”直接上 weekly。CI/CD 平台坏一次,影响的是整个团队的构建和发布入口。
从 Jenkins 2.555.1 LTS 线开始,Jenkins controller 和 agent JVM 运行时要求 Java 21 或 Java 25。一般建议用 Java 21。原因很简单:稳定、资料多、插件兼容性更容易排查。
先在服务器上确认 Java:
java -version正常应该能看到类似结果:
openjdk version "21.x.x"如果这里还是 Java 8、Java 11 或 Java 17,就不要继续装当前 LTS Jenkins。先把 Jenkins 运行时 Java 升到 21。
这里容易混淆:Jenkins 自己运行用的 Java,和业务项目构建用的 JDK,不一定是同一个。比如 Jenkins controller 用 Java 21 运行,老项目仍然可以在 agent 上用 JDK 8 或 JDK 11 构建。这个要靠节点和全局工具来区分,不要把 Jenkins runtime 和项目 build JDK 混在一起。
在 Linux 上安装 Jenkins
下面用 Ubuntu / Debian 演示。生产上建议优先用官方 LTS 包管理仓库安装,便于 systemd 管理、日志查看和升级。
先安装 Java 21:
sudo apt update
sudo apt install -y fontconfig openjdk-21-jre wget
java -version这一步解决的是 Jenkins 运行时依赖。执行后要看 java -version,确认版本是 21。不是 21 就先停下来排查,不然后面 Jenkins 可能直接启动失败。
添加 Jenkins LTS 仓库:
sudo install -d -m 0755 /etc/apt/keyrings
sudo wget -O /etc/apt/keyrings/jenkins-keyring.asc \
https://pkg.jenkins.io/debian-stable/jenkins.io-2026.key
echo "deb [signed-by=/etc/apt/keyrings/jenkins-keyring.asc] https://pkg.jenkins.io/debian-stable binary/" \
| sudo tee /etc/apt/sources.list.d/jenkins.list > /dev/null
sudo apt update这里做了两件事:导入 Jenkins 官方仓库签名,把 LTS 仓库加入 apt 源。执行 sudo apt update 时如果报证书、DNS 或连接超时,先处理网络和代理,不要换一堆来历不明的脚本。
安装 Jenkins:
sudo apt install -y jenkins启动并设置开机自启:
sudo systemctl enable --now jenkins
sudo systemctl status jenkins --no-pager正常应该看到 active (running)。如果不是,马上看日志:
sudo journalctl -u jenkins.service -n 100 --no-pager最常见的失败有三个:
- Java 版本不对。
- 8080 端口被占用。
- 服务器访问 Jenkins 仓库或插件源有问题。
看端口:
sudo ss -lntp | grep ':8080'如果 8080 被占用了,可以给 Jenkins 换端口:
sudo systemctl edit jenkins写入:
[Service]
Environment="JENKINS_PORT=8081"重启:
sudo systemctl daemon-reload
sudo systemctl restart jenkins
sudo systemctl status jenkins --no-pager第一次访问 Jenkins,需要初始化密码:
sudo cat /var/lib/jenkins/secrets/initialAdminPassword浏览器访问:
http://服务器IP:8080/如果页面打不开,按这个顺序查:
sudo systemctl status jenkins --no-pager
sudo ss -lntp | grep ':8080'
curl -I http://127.0.0.1:8080/本机 curl 能通,外部浏览器不能通,一般就是防火墙、安全组、反向代理或服务器网络策略的问题。
生产建议:初始化完成后,不要长期使用初始管理员账号。新建个人管理员账号,关闭匿名用户危险权限,再继续配置插件和凭证。
用 Docker 跑 Jenkins 时要注意权限
有些团队喜欢用 Docker 起 Jenkins,这样迁移方便,目录也清楚。学习环境可以这样做:
docker volume create jenkins_home
docker run -d \
--name jenkins \
--restart=unless-stopped \
-p 8080:8080 \
-p 50000:50000 \
-v jenkins_home:/var/jenkins_home \
jenkins/jenkins:lts-jdk21查看初始化密码:
docker exec jenkins cat /var/jenkins_home/secrets/initialAdminPassword
docker logs --tail=100 jenkins这里要注意两点。
第一,jenkins/jenkins:lts-jdk21 是浮动标签,适合快速体验。生产环境经过验证后要固定版本或 digest,避免某次重建容器时 Jenkins 版本悄悄变了。
第二,不建议把宿主机 Docker socket 直接挂给 controller:
-v /var/run/docker.sock:/var/run/docker.sock这样 Jenkins 容器几乎就拥有了操作宿主机 Docker 的高权限。学习环境可以理解它的机制,生产上更稳的做法是:controller 只负责编排,Docker 构建放到专用 agent、远程构建机或企业构建平台。
如果你确实需要 agent 构建镜像,一般把 Docker 能力放到 agent:
docker version
docker info这两个命令在 agent 上能正常执行,说明 agent 具备 Docker 构建基础。失败时先查 Docker 服务、当前用户权限、socket 权限和磁盘空间。
中国大陆、内网和离线环境先准备仓库
Jenkins 真正落地时,最烦人的往往不是 Jenkinsfile,而是依赖下载。
常见现场问题:
插件下载一半超时
Maven 依赖拉不下来
npm install 卡在二进制包下载
Docker 基础镜像拉取失败
部署服务器不能访问公网镜像仓库所以企业里一般需要先准备四类仓库:
- Jenkins 插件代理或离线插件包。
- Maven 私服,例如 Nexus 或 Artifactory。
- npm 私服或代理仓库。
- Docker 私有镜像仓库,例如 Harbor。
Jenkins 插件建议维护一个 plugins.txt,不要完全靠页面临时点击:
git
workflow-aggregator
pipeline-stage-view
credentials-binding
ssh-agent
matrix-auth
role-strategy
timestamper
ws-cleanup
junit
mailer
sonar第一次可以这样准备插件包:
java -jar jenkins-plugin-manager-*.jar \
--war ./jenkins.war \
--plugin-download-directory ./plugins \
--plugin-file ./plugins.txt \
--latest false \
--list这个命令解决的是“把插件和依赖提前下载完整”。执行后应该在 plugins 目录看到一批 .jpi 或 .hpi 文件。如果中途失败,先看失败的是哪个插件、依赖哪个 Jenkins core 版本,再看网络和代理。
准备好以后打包:
tar -czf jenkins-plugins.tar.gz plugins plugins.txt
sha256sum jenkins-plugins.tar.gz > jenkins-plugins.tar.gz.sha256
sha256sum -c jenkins-plugins.tar.gz.sha256离线服务器安装时:
sudo systemctl stop jenkins
sudo rsync -a ./plugins/ /var/lib/jenkins/plugins/
sudo chown -R jenkins:jenkins /var/lib/jenkins/plugins
sudo systemctl start jenkins
sudo journalctl -u jenkins.service -n 100 --no-pager这里不要偷懒省掉 chown。插件文件权限不对,Jenkins 可能启动了,但插件加载失败,页面上会出现一堆依赖错误。
生产建议:插件版本要和 Jenkins LTS 版本一起锁定。国内镜像站可以作为下载加速入口,但不要当成永久稳定方案。更稳的是企业内部缓存、插件清单、版本锁定和校验文件。
插件别一股脑全装
Jenkins 插件很多,但不是越多越好。插件越多,升级越麻烦,兼容问题越多,安全面也越大。
一个基础流水线一般先装这些就够用:
| 类别 | 插件 | 解决什么问题 |
|---|---|---|
| Git | Git | 拉取代码仓库 |
| Pipeline | Pipeline、Stage View | 支持 Jenkinsfile 和阶段视图 |
| 凭证 | Credentials Binding | 在流水线中安全使用密码、Token |
| SSH | SSH Agent | 用 SSH Key 拉代码或部署 |
| 权限 | Matrix Authorization、Role Strategy | 做角色权限 |
| 日志 | Timestamper | 控制台日志带时间 |
| 清理 | Workspace Cleanup | 构建后清理工作区 |
| 测试 | JUnit | 收集单测报告 |
| 质量 | SonarQube Scanner for Jenkins | 接入质量门禁 |
老项目如果还在用 Freestyle Job,可能会看到 Publish Over SSH、Maven Integration 这类插件。新项目建议优先用 Pipeline,把流水线写进代码仓库。
插件安装完以后,到 Jenkins 页面里看:
Manage Jenkins
-> Plugins
-> Installed plugins如果插件显示 failed 或 disabled,不要继续配 job,先把插件依赖处理好。流水线平台自身不稳定,后面的构建失败会很难判断是代码问题还是 Jenkins 问题。
凭证只让流水线看到 ID
发布流水线一定会接触密钥:Git 私钥、镜像仓库账号、Maven 私服账号、npm token、部署服务器 SSH Key。这里最重要的原则是:Jenkinsfile 里只写凭证 ID,不写明文。
常见凭证类型:
| 类型 | 用途 |
|---|---|
| Username with password | 镜像仓库、制品库账号 |
| Secret text | token、API key |
| SSH Username with private key | Git SSH、部署服务器 |
| Secret file | kubeconfig、证书文件 |
建议凭证 ID 命名清楚:
git-ssh-readonly
harbor-robot-demo-push
nexus-maven-deploy
npm-registry-token
deploy-ssh-test
deploy-ssh-prod
sonarqube-token在 Pipeline 中使用账号密码:
withCredentials([usernamePassword(
credentialsId: 'harbor-robot-demo-push',
usernameVariable: 'REGISTRY_USER',
passwordVariable: 'REGISTRY_PASSWORD'
)]) {
sh '''
set +x
printf '%s' "$REGISTRY_PASSWORD" | docker login "$REGISTRY" \
-u "$REGISTRY_USER" \
--password-stdin
set -x
'''
}这里 set +x 是为了避免 shell 把变量展开过程打到日志里。Jenkins 会做脱敏,但生产上不要把安全完全交给脱敏功能。
使用 SSH Key:
sshagent(credentials: ['deploy-ssh-test']) {
sh '''
ssh -o BatchMode=yes deploy@app-01.example.com 'hostname && date'
'''
}执行后应该看到远程机器的主机名和时间。如果失败:
Permission denied (publickey) -> 私钥不匹配、公钥没放到目标账号
Host key verification failed -> known_hosts 没准备或主机指纹变化
Connection timed out -> 网络、防火墙、安全组、端口不通生产建议:测试和生产凭证分开,流水线账号不要用个人账号,更不要用 admin。人员离职、项目下线、流水线废弃时,要轮换或删除对应凭证。
权限:不是所有人都能发生产
Jenkins 初始化以后,很多人会图省事给所有登录用户很高权限。这样前期方便,后期很危险。
一般至少拆成四类角色:
| 角色 | 能做什么 |
|---|---|
| Reader | 看构建记录和日志 |
| Developer | 触发开发、测试环境构建 |
| Releaser | 触发预发、生产发布 |
| Admin | 管 Jenkins、插件、节点、凭证 |
生产发布可以加人工确认:
stage('Approve Production') {
when {
expression { params.ENV_NAME == 'prod' && params.DEPLOY_ACTION == 'deploy' }
}
steps {
timeout(time: 10, unit: 'MINUTES') {
input message: "确认发布 ${env.IMAGE} 到生产环境?", ok: '确认发布'
}
}
}访问这个阶段时,Jenkins 页面会停住,等待有权限的人确认。这里不是为了让自动化变慢,而是为了留下“谁在什么时间确认发布了哪个版本”的记录。
测试环境可以自动发布,生产环境要看业务风险。有些内部工具可以自动,有些交易链路必须走审批和变更窗口。
controller 和 agent 要分工
Jenkins controller 是大脑,负责页面、配置、权限、调度。agent 是干活的机器,负责执行 Maven、Node、Docker build、部署命令。
生产上一般不要让 controller 直接跑业务构建。打开 Jenkins 节点配置,把内置节点 executor 设置为 0,然后增加专用 agent。
在 agent 上先检查基础命令:
java -version
mvn -version
node -v
npm -v
docker version
git --version
df -h
free -h这些命令解决的是“构建环境是否具备”。执行后要重点看:
Java 是否符合项目要求
Maven 和 Node 是否是团队约定版本
Docker daemon 是否可用
磁盘是否足够
内存是否足够如果 agent 经常离线,先查资源:
df -h
df -i
free -h
uptime
timedatectlJenkins 会监控 agent 的磁盘、临时目录、swap、时钟同步和响应时间。节点被 Jenkins 自动标记 offline 时,不要只点上线,要查为什么离线。
Docker 构建节点还要看缓存占用:
docker system df
docker builder prune -af --filter "until=168h"
docker image prune -af --filter "until=168h"清理命令要放在低峰维护任务里,不要在每次业务构建前无脑执行。否则缓存刚建好又被删掉,构建会越来越慢。
Jenkins 发布控制面与制品流图
到这里,Jenkins 的角色就不只是“点一下构建”了。发布评审时要把控制面和制品流放在一张图里看:谁触发、谁调度、在哪个 agent 上执行、凭证从哪里注入、制品如何归档、镜像推到哪里、部署失败时从哪条链路回滚。上线前按这张图逐项核对,能避免把构建、制品、镜像、部署、审计混成一团。
排障时也按这张图反推。agent 不可用,就先看节点离线原因、executor、磁盘和 workspace;checkout 失败,就看 SCM 凭证和触发 commit;依赖拉取失败,回到 Maven/npm 私服和 agent 网络;制品缺失,就看 archiveArtifacts 匹配模式和构建输出;镜像推送失败,就看 Harbor robot 账号、项目路径和 tag;部署失败或回滚失败,就看脚本退出码、健康检查、上一版制品和数据库/配置兼容性。
先用 Jenkinsfile 管住流程
Jenkins 有两种常见用法:Freestyle Job 和 Pipeline。
Freestyle 是在页面上点:Git 仓库、分支、构建命令、构建后操作。它适合临时任务,但长期维护会很痛苦。因为配置都在 Jenkins UI 里,代码仓库看不到,评审也看不到。
新项目建议用 Jenkinsfile。把 Jenkinsfile 放到仓库根目录,然后 Jenkins job 配成:
Pipeline script from SCM
-> SCM: Git
-> Repository URL
-> Credentials
-> Branch
-> Script Path: Jenkinsfile这样流水线逻辑和业务代码一起提交、评审、回滚。谁改了发布流程,Git 里能看见。
触发方式一般这样选:
- 开发分支:push 或 merge 后 webhook 自动触发。
- 测试环境:可以参数化手动触发,也可以合并后自动触发。
- 生产环境:手动参数化触发,加审批或变更单。
- 夜间任务:定时触发,例如依赖扫描、全量测试。
这里要注意,生产发布不要只靠“点 Build Now”。至少要让用户明确选择环境、分支、模块、动作和回滚版本。
版本号一定要能追到代码
发布事故里最怕一句话:“我刚才发的那个包”。一旦说不清线上运行的是哪次提交,排障和回滚都会很痛苦。
在流水线里先生成版本号:
GIT_SHA="$(git rev-parse --short=12 HEAD)"
BUILD_TIME="$(date +%Y%m%d%H%M%S)"
VERSION="${BUILD_NUMBER}-${GIT_SHA}-${BUILD_TIME}"
echo "$VERSION"这个版本号解决的是追溯问题。看到 42-a1b2c3d4e5f6-20260709113001,你至少能知道它来自第 42 次构建、某个 Git commit、某个构建时间。
jar 包可以这样命名:
demo-api-42-a1b2c3d4e5f6-20260709113001.jar镜像可以这样命名:
harbor.example.com/team/demo-api:42-a1b2c3d4e5f6这里不要把 latest 当生产版本。latest 只是一个会变的标签,今天指向 A,明天可能指向 B。生产部署和回滚必须使用明确 tag。
一份可以改造的 Jenkinsfile
下面这份 Jenkinsfile 按 Java 后端服务写。你可以先复制到项目里跑通,再按自己的模块名、仓库地址、镜像仓库和部署脚本调整。
pipeline {
agent none
options {
timestamps()
ansiColor('xterm')
disableConcurrentBuilds()
timeout(time: 60, unit: 'MINUTES')
buildDiscarder(logRotator(daysToKeepStr: '14', numToKeepStr: '30'))
}
parameters {
choice(name: 'ENV_NAME', choices: ['test', 'staging', 'prod'], description: '目标环境')
choice(name: 'DEPLOY_ACTION', choices: ['deploy', 'rollback'], description: '发布或回滚')
string(name: 'BRANCH_NAME', defaultValue: 'main', description: '构建分支')
string(name: 'MODULE_NAME', defaultValue: 'demo-api', description: '模块名')
string(name: 'ROLLBACK_IMAGE', defaultValue: '', description: '回滚时指定镜像')
booleanParam(name: 'SKIP_TESTS', defaultValue: false, description: '是否跳过测试')
}
environment {
REGISTRY = 'harbor.example.com'
REGISTRY_PROJECT = 'team'
APP_PORT = '8080'
HEALTH_PATH = '/actuator/health'
}
stages {
stage('Init') {
agent { label 'linux && maven && docker' }
steps {
script {
env.APP_NAME = params.MODULE_NAME.trim()
env.IMAGE_REPO = "${env.REGISTRY}/${env.REGISTRY_PROJECT}/${env.APP_NAME}"
}
sh '''
set -Eeuo pipefail
echo "env=${ENV_NAME}"
echo "action=${DEPLOY_ACTION}"
echo "module=${APP_NAME}"
java -version
mvn -version
docker version
'''
}
}
stage('Checkout') {
when {
expression { params.DEPLOY_ACTION == 'deploy' }
}
agent { label 'linux && maven && docker' }
steps {
checkout([
$class: 'GitSCM',
branches: [[name: "*/${params.BRANCH_NAME}"]],
userRemoteConfigs: [[
url: 'git@example.com:team/demo-service.git',
credentialsId: 'git-ssh-readonly'
]]
])
script {
env.GIT_SHA = sh(script: 'git rev-parse --short=12 HEAD', returnStdout: true).trim()
env.VERSION = "${env.BUILD_NUMBER}-${env.GIT_SHA}"
env.IMAGE = "${env.IMAGE_REPO}:${env.VERSION}"
currentBuild.displayName = "#${env.BUILD_NUMBER} ${params.ENV_NAME} ${params.MODULE_NAME} ${env.GIT_SHA}"
}
}
}
stage('Maven Build') {
when {
expression { params.DEPLOY_ACTION == 'deploy' }
}
agent { label 'linux && maven && docker' }
steps {
script {
env.TEST_ARGS = params.SKIP_TESTS ? '-DskipTests' : ''
}
sh '''
set -Eeuo pipefail
mvn -B -s .ci/settings.xml clean verify ${TEST_ARGS}
'''
}
post {
always {
junit allowEmptyResults: true, testResults: '**/target/surefire-reports/*.xml'
}
}
}
stage('Archive Artifact') {
when {
expression { params.DEPLOY_ACTION == 'deploy' }
}
agent { label 'linux && maven && docker' }
steps {
sh '''
set -Eeuo pipefail
mkdir -p dist
cp ${APP_NAME}/target/*.jar dist/${APP_NAME}-${VERSION}.jar
sha256sum dist/${APP_NAME}-${VERSION}.jar > dist/${APP_NAME}-${VERSION}.jar.sha256
'''
archiveArtifacts artifacts: 'dist/*', fingerprint: true
}
}
stage('Build And Push Image') {
when {
expression { params.DEPLOY_ACTION == 'deploy' }
}
agent { label 'linux && maven && docker' }
steps {
withCredentials([usernamePassword(
credentialsId: 'harbor-robot-demo-push',
usernameVariable: 'REGISTRY_USER',
passwordVariable: 'REGISTRY_PASSWORD'
)]) {
sh '''
set -Eeuo pipefail
set +x
printf '%s' "$REGISTRY_PASSWORD" | docker login "$REGISTRY" \
-u "$REGISTRY_USER" \
--password-stdin
set -x
docker build \
--pull \
--build-arg JAR_FILE=dist/${APP_NAME}-${VERSION}.jar \
-t "$IMAGE" \
.
docker push "$IMAGE"
docker logout "$REGISTRY"
'''
}
}
}
stage('Deploy') {
when {
expression { params.DEPLOY_ACTION == 'deploy' }
}
agent { label 'linux && deploy' }
steps {
sshagent(credentials: ["deploy-ssh-${params.ENV_NAME}"]) {
sh '''
set -Eeuo pipefail
TARGET_HOST="app-01.${ENV_NAME}.example.com"
RELEASE_DIR="/opt/releases/${APP_NAME}/${VERSION}"
ssh -o BatchMode=yes deploy@${TARGET_HOST} "mkdir -p ${RELEASE_DIR}"
scp scripts/deploy-docker.sh deploy@${TARGET_HOST}:${RELEASE_DIR}/deploy-docker.sh
ssh -o BatchMode=yes deploy@${TARGET_HOST} \
"APP_NAME='${APP_NAME}' IMAGE='${IMAGE}' ENV_NAME='${ENV_NAME}' APP_PORT='${APP_PORT}' HEALTH_PATH='${HEALTH_PATH}' bash ${RELEASE_DIR}/deploy-docker.sh"
'''
}
}
}
stage('Rollback') {
when {
expression { params.DEPLOY_ACTION == 'rollback' }
}
agent { label 'linux && deploy' }
steps {
sshagent(credentials: ["deploy-ssh-${params.ENV_NAME}"]) {
sh '''
set -Eeuo pipefail
test -n "${ROLLBACK_IMAGE}" || {
echo "ROLLBACK_IMAGE is required"
exit 2
}
TARGET_HOST="app-01.${ENV_NAME}.example.com"
ssh -o BatchMode=yes deploy@${TARGET_HOST} \
"APP_NAME='${MODULE_NAME}' IMAGE='${ROLLBACK_IMAGE}' ENV_NAME='${ENV_NAME}' APP_PORT='${APP_PORT}' HEALTH_PATH='${HEALTH_PATH}' bash /opt/apps/${MODULE_NAME}/bin/rollback-docker.sh"
'''
}
}
}
}
post {
always {
cleanWs deleteDirs: true, disableDeferredWipeout: true
}
}
}先不要急着把它改复杂。第一次落地时,建议只跑到 Archive Artifact,确认 jar 能生成;第二次再加 Docker build;第三次再接部署。每加一段,就让它真的失败一次,看日志能不能定位。
这里几个配置要理解:
| 配置 | 解决什么问题 |
|---|---|
agent none | 不让整个流水线默认占住某台机器 |
disableConcurrentBuilds() | 防止同一个服务并发发布 |
buildDiscarder | 防止 Jenkins 日志和制品撑爆磁盘 |
credentialsId | 让密钥只以 ID 形式出现 |
archiveArtifacts | Jenkins 页面能下载本次构建制品 |
cleanWs | 构建后清理工作区 |
如果 Jenkinsfile 一上来就报语法错误,可以用页面里的 Pipeline Syntax 生成片段,也可以先把复杂 stage 注释掉,从最小流水线开始:
pipeline {
agent any
stages {
stage('Hello') {
steps {
sh 'echo hello'
}
}
}
}最小流水线能跑,说明 Jenkins、agent、shell 基本通了。再逐步加 Git、Maven、Docker、部署。
Maven 构建先管住 settings.xml
Java 项目在 CI 里最常见的问题,不是 mvn clean package 不会写,而是依赖来源不稳定。
建议在仓库里准备 .ci/settings.xml,不要依赖某个开发机上的 ~/.m2/settings.xml。
一个企业私服镜像配置示例:
<settings>
<mirrors>
<mirror>
<id>corp-maven-public</id>
<name>Corporate Maven Public</name>
<url>https://nexus.example.com/repository/maven-public/</url>
<mirrorOf>*</mirrorOf>
</mirror>
</mirrors>
</settings>构建命令:
mvn -B -s .ci/settings.xml clean verify这里 -B 是 batch mode,适合 CI,不要让 Maven 等交互输入。-s .ci/settings.xml 是明确告诉 Maven 使用项目里的配置。
如果是多模块项目,可以只构建目标模块和它依赖的模块:
mvn -B -s .ci/settings.xml -pl demo-api -am clean package执行后应该看到:
BUILD SUCCESS如果失败,先按现象分:
| 现象 | 一般原因 | 怎么查 |
|---|---|---|
Could not transfer artifact | 私服不可达、DNS、证书、代理 | curl -I 私服地址 |
401 Unauthorized | server id 不匹配、账号错 | 对照 settings 里的 server id |
Unsupported class file major version | JDK 版本不对 | java -version |
| 本地能构建,CI 失败 | 本地缓存掩盖了问题 | 清理 agent .m2 后复现 |
这里要注意,Maven 的 mirror 不是写多个就会自动聚合。同一个仓库最终只会匹配一个 mirror。企业里要用 Nexus 或 Artifactory 做 group repository,再让 Maven 指向这个 group。
生产建议:不要默认 -DskipTests。测试慢要拆测试、做缓存、做并行,不要一上来把测试关掉。可以把 SKIP_TESTS 做成测试环境临时参数,但生产主链路要保留一条不跳测试的门禁。
Node 构建优先用 npm ci
前端项目在 Jenkins 上失败,经常是因为依赖不一致。本地 node_modules 里什么都有,CI 是干净环境,一跑就露馅。
如果仓库里有 package-lock.json,CI 里优先执行:
npm ci
npm run buildnpm ci 解决的是依赖可重复问题。它会严格按 lockfile 安装,如果 package.json 和 lockfile 不一致,会直接失败。这个失败是好事,说明依赖记录没有同步。
内网环境一般需要 .npmrc:
registry=https://npm.example.com/repository/npm-group/
always-auth=true如果需要 token,不要写死在仓库里,用 Jenkins 凭证生成临时 .npmrc:
withCredentials([string(credentialsId: 'npm-registry-token', variable: 'NPM_TOKEN')]) {
sh '''
set -Eeuo pipefail
set +x
cat > .npmrc <<EOF
registry=https://npm.example.com/repository/npm-group/
always-auth=true
//npm.example.com/repository/npm-group/:_authToken=${NPM_TOKEN}
EOF
set -x
npm ci --cache "$WORKSPACE/.npm-cache" --prefer-offline
npm run build
tar -czf dist/${APP_NAME}-${VERSION}.tar.gz dist
sha256sum dist/${APP_NAME}-${VERSION}.tar.gz > dist/${APP_NAME}-${VERSION}.tar.gz.sha256
rm -f .npmrc
'''
}执行后应该看到构建产物目录,比如 dist。如果失败,常见问题是:
| 现象 | 一般原因 | 怎么查 |
|---|---|---|
| lockfile 不一致 | 改了 package.json 没提交 lockfile | 本地重新生成 lockfile |
| 二进制包下载失败 | sharp、puppeteer、node-sass 等依赖外部下载 | 配置企业镜像或预置缓存 |
| 本地成功 CI 失败 | Node/npm 版本不同 | node -v、npm -v |
| 构建内存溢出 | 前端构建占内存 | 设置 NODE_OPTIONS |
内存不够时可以先这样验证:
export NODE_OPTIONS="--max-old-space-size=4096"
npm ci --cache "$WORKSPACE/.npm-cache" --prefer-offline
npm run build生产建议:缓存 key 要带上操作系统、Node 版本、npm 版本和 lockfile 哈希。不要所有项目共享一个大缓存目录,后面会很难排查。
Docker 镜像构建和推送
构建镜像这一步解决的是“部署环境不用重新编译代码”。Jenkins 构建出镜像,推到企业镜像仓库,服务器只负责拉镜像和启动。
一个简单 Java 服务 Dockerfile:
FROM eclipse-temurin:21-jre
ARG JAR_FILE
WORKDIR /app
COPY ${JAR_FILE} /app/app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "/app/app.jar"]构建:
REGISTRY="harbor.example.com"
IMAGE_REPO="team/demo-api"
VERSION="42-a1b2c3d4e5f6"
IMAGE="${REGISTRY}/${IMAGE_REPO}:${VERSION}"
docker build \
--pull \
--build-arg JAR_FILE=dist/demo-api-${VERSION}.jar \
-t "$IMAGE" \
.这里 -t "$IMAGE" 给镜像打标签,最后的点 . 表示 Dockerfile 所在的构建上下文。执行后用下面命令看本地镜像:
docker image ls | grep demo-api
docker image inspect "$IMAGE"登录并推送:
printf '%s' "$REGISTRY_PASSWORD" | docker login "$REGISTRY" \
-u "$REGISTRY_USER" \
--password-stdin
docker push "$IMAGE"推送后验证:
docker manifest inspect harbor.example.com/team/demo-api:42-a1b2c3d4e5f6 >/dev/null常见失败:
| 现象 | 一般原因 | 怎么查 |
|---|---|---|
Cannot connect to the Docker daemon | Docker 服务没启动、权限不够 | docker version、systemctl status docker |
pull access denied | 基础镜像没权限或地址错 | 手动 docker pull |
denied: requested access | Harbor 项目权限不够 | 看机器人账号权限 |
| 磁盘满 | build cache 太多 | docker system df |
生产建议:Jenkins 用 Harbor 机器人账号,不用个人账号,更不用 admin。镜像 tag 绑定 commit 和 build number。旧镜像保留周期要覆盖回滚窗口,不能今天发完明天就清掉。
部署脚本让 Jenkins 调,不要散在页面里
Jenkinsfile 负责串流程,真正的部署动作建议写成脚本放进仓库,比如 scripts/deploy-docker.sh。这样脚本也能 code review。
示例脚本:
#!/usr/bin/env bash
set -Eeuo pipefail
IFS=$'\n\t'
APP_NAME="${APP_NAME:?APP_NAME is required}"
IMAGE="${IMAGE:?IMAGE is required}"
ENV_NAME="${ENV_NAME:?ENV_NAME is required}"
APP_PORT="${APP_PORT:-8080}"
HEALTH_PATH="${HEALTH_PATH:-/actuator/health}"
BASE_DIR="/opt/apps/${APP_NAME}"
LOG_DIR="${BASE_DIR}/logs"
STATE_DIR="${BASE_DIR}/state"
LOCK_FILE="${BASE_DIR}/deploy.lock"
CURRENT_IMAGE_FILE="${STATE_DIR}/current-image"
PREVIOUS_IMAGE_FILE="${STATE_DIR}/previous-image"
mkdir -p "$LOG_DIR" "$STATE_DIR"
log() {
printf '%s [%s] %s\n' "$(date '+%F %T')" "$1" "$2" | tee -a "${LOG_DIR}/deploy.log" >&2
}
(
flock -n 9 || {
log ERROR "another deployment is running"
exit 100
}
log INFO "deploy app=${APP_NAME}, env=${ENV_NAME}, image=${IMAGE}"
if [[ -f "$CURRENT_IMAGE_FILE" ]]; then
cp "$CURRENT_IMAGE_FILE" "$PREVIOUS_IMAGE_FILE"
fi
docker pull "$IMAGE"
if docker ps -a --format '{{.Names}}' | grep -Fxq "$APP_NAME"; then
docker rm -f "$APP_NAME"
fi
docker run -d \
--name "$APP_NAME" \
--restart=always \
-p "${APP_PORT}:8080" \
--env "SPRING_PROFILES_ACTIVE=${ENV_NAME}" \
"$IMAGE"
HEALTH_URL="http://127.0.0.1:${APP_PORT}${HEALTH_PATH}"
for i in $(seq 1 30); do
if curl -fsS --max-time 5 "$HEALTH_URL" >/dev/null; then
echo "$IMAGE" > "$CURRENT_IMAGE_FILE"
log INFO "health check passed: ${HEALTH_URL}"
exit 0
fi
sleep 2
done
log ERROR "health check failed: ${HEALTH_URL}"
docker logs --tail=200 "$APP_NAME" | tee -a "${LOG_DIR}/deploy.log" >&2
exit 1
) 9>"$LOCK_FILE"这段脚本解决几个现场问题:
- 参数缺失直接失败,不会糊里糊涂部署。
flock防止两个人同时发布同一个服务。- 发布前保存上一版镜像。
- 启动后做健康检查。
- 失败时输出容器日志并返回非 0。
Jenkins 调用脚本:
ssh -o BatchMode=yes deploy@app-01.test.example.com "mkdir -p /opt/releases/demo-api/${VERSION}"
scp scripts/deploy-docker.sh deploy@app-01.test.example.com:/opt/releases/demo-api/${VERSION}/deploy-docker.sh
ssh -o BatchMode=yes deploy@app-01.test.example.com \
"APP_NAME='demo-api' IMAGE='${IMAGE}' ENV_NAME='test' APP_PORT='8080' HEALTH_PATH='/actuator/health' bash /opt/releases/demo-api/${VERSION}/deploy-docker.sh"执行后应该看到 Jenkins 控制台里有部署日志,目标服务器上也有 /opt/apps/demo-api/logs/deploy.log。如果 Jenkins 显示成功,但服务访问失败,说明脚本没有把健康检查纳入退出码,要改脚本。
如果是 systemd 服务,核心步骤可以换成:
sudo install -m 0644 "dist/${APP_NAME}.jar" "/opt/apps/${APP_NAME}/${APP_NAME}.jar"
sudo systemctl restart "${APP_NAME}"
sudo systemctl status "${APP_NAME}" --no-pager
journalctl -u "${APP_NAME}" -n 100 --no-pager如果是 K8S,Jenkins 这里先只管到发布命令和 rollout 状态:
kubectl -n prod set image deployment/demo-api demo-api="$IMAGE"
kubectl -n prod rollout status deployment/demo-api --timeout=180s
kubectl -n prod rollout undo deployment/demo-apiK8S 的 Deployment、Service、Ingress、Helm 模板不在这里展开。
发布后一定要验证
部署命令返回 0,不代表服务真的好了。发布后至少验证三层:
- 进程或容器是否在。
- 健康检查接口是否通。
- 关键业务接口是否通。
容器状态:
docker ps --filter "name=demo-api"
docker inspect demo-api --format '{{.State.Status}} {{.State.ExitCode}}'
docker logs --tail=100 demo-api健康检查:
curl -fsS --max-time 5 http://127.0.0.1:8080/actuator/health
curl -fsS --max-time 5 https://api.example.com/actuator/health关键接口:
curl -fsS --max-time 5 \
-H "X-Request-Id: ci-${BUILD_NUMBER}" \
https://api.example.com/api/ping访问后应该看到 2xx 响应。如果 curl 报错,先区分是本机不通、内网不通,还是网关外部不通:
curl -v http://127.0.0.1:8080/actuator/health
curl -v http://内网IP:8080/actuator/health
curl -v https://api.example.com/actuator/health三层都查,是为了把问题定位到应用、主机网络、网关或域名,而不是只说“服务挂了”。
生产建议:健康检查必须进入 Jenkins 流水线。检查失败,Pipeline 就失败;Pipeline 失败,通知里要带环境、版本、构建号、日志入口和目标主机。
回滚不是事故后现写脚本
回滚要提前设计。真正事故来了以后,大家都紧张,这时候再找上一版镜像、再写回滚命令,很容易二次事故。
容器回滚脚本可以这样写:
#!/usr/bin/env bash
set -Eeuo pipefail
IFS=$'\n\t'
APP_NAME="${APP_NAME:?APP_NAME is required}"
IMAGE="${IMAGE:?IMAGE is required}"
ENV_NAME="${ENV_NAME:?ENV_NAME is required}"
APP_PORT="${APP_PORT:-8080}"
HEALTH_PATH="${HEALTH_PATH:-/actuator/health}"
BASE_DIR="/opt/apps/${APP_NAME}"
LOG_DIR="${BASE_DIR}/logs"
STATE_DIR="${BASE_DIR}/state"
LOCK_FILE="${BASE_DIR}/deploy.lock"
CURRENT_IMAGE_FILE="${STATE_DIR}/current-image"
mkdir -p "$LOG_DIR" "$STATE_DIR"
log() {
printf '%s [%s] %s\n' "$(date '+%F %T')" "$1" "$2" | tee -a "${LOG_DIR}/rollback.log" >&2
}
(
flock -n 9 || {
log ERROR "another deployment or rollback is running"
exit 100
}
log INFO "rollback app=${APP_NAME}, env=${ENV_NAME}, image=${IMAGE}"
docker pull "$IMAGE"
if docker ps -a --format '{{.Names}}' | grep -Fxq "$APP_NAME"; then
docker rm -f "$APP_NAME"
fi
docker run -d \
--name "$APP_NAME" \
--restart=always \
-p "${APP_PORT}:8080" \
--env "SPRING_PROFILES_ACTIVE=${ENV_NAME}" \
"$IMAGE"
HEALTH_URL="http://127.0.0.1:${APP_PORT}${HEALTH_PATH}"
for i in $(seq 1 30); do
if curl -fsS --max-time 5 "$HEALTH_URL" >/dev/null; then
echo "$IMAGE" > "$CURRENT_IMAGE_FILE"
log INFO "rollback health check passed"
exit 0
fi
sleep 2
done
log ERROR "rollback health check failed"
docker logs --tail=200 "$APP_NAME" | tee -a "${LOG_DIR}/rollback.log" >&2
exit 1
) 9>"$LOCK_FILE"Jenkins 回滚参数要明确:
DEPLOY_ACTION=rollback
ENV_NAME=prod
MODULE_NAME=demo-api
ROLLBACK_IMAGE=harbor.example.com/team/demo-api:41-f0e1d2c3b4a5回滚后也要健康检查。不能只把旧容器启动起来就宣布回滚成功。
这里还有一个关键点:数据库变更不一定能回滚。生产变更要尽量用兼容方式做:
- 先加字段,不要马上删字段。
- 代码先兼容新旧结构。
- 数据迁移脚本要可重复执行。
- 确认旧版本能读新结构,再允许代码回滚。
- 真正不可逆的数据库变更,要走变更窗口和备份。
前端回滚也要注意缓存。静态资源文件名要带 hash,HTML 入口和资源缓存策略要分开,CDN 刷新也要进入发布流程。
质量门禁要能阻断
质量门禁不是摆设。它的作用是:不满足条件就停下来。
基础门禁一般包括:
编译必须通过
单元测试必须通过
制品必须生成
镜像必须构建并推送
健康检查必须通过
阻断级扫描问题不能放行Jenkins 收集 JUnit:
post {
always {
junit allowEmptyResults: true, testResults: '**/target/surefire-reports/*.xml'
}
}接 SonarQube:
withSonarQubeEnv('sonarqube') {
sh 'mvn -B sonar:sonar'
}
timeout(time: 10, unit: 'MINUTES') {
waitForQualityGate abortPipeline: true
}如果 waitForQualityGate 一直超时,先查 SonarQube webhook 到 Jenkins 是否能访问,不要只把 timeout 调大。
一般检查:
SonarQube webhook URL 是否正确
Jenkins 外部访问地址是否正确
SonarQube token 是否有效
SonarQube 后台任务是否完成
Jenkins job 是否能接收质量门禁结果老项目不要一上来追求“全量问题清零”。可以先卡新增代码质量,保证不继续变差,然后逐步治理历史问题。
参数化构建别做成自由输入框
Jenkins 参数化构建很好用,但参数太自由也危险。生产参数最好是选择项和白名单。
建议保留这些参数:
| 参数 | 示例 | 作用 |
|---|---|---|
ENV_NAME | test、staging、prod | 目标环境 |
BRANCH_NAME | main、release/1.2.0 | 构建分支 |
MODULE_NAME | demo-api | 构建模块 |
DEPLOY_ACTION | deploy、rollback | 发布或回滚 |
ROLLBACK_IMAGE | 完整镜像 tag | 回滚目标 |
脚本里要校验:
case "$ENV_NAME" in
test|staging|prod) ;;
*) echo "invalid ENV_NAME: $ENV_NAME"; exit 2 ;;
esac
case "$MODULE_NAME" in
demo-api|demo-admin|demo-job) ;;
*) echo "invalid MODULE_NAME: $MODULE_NAME"; exit 2 ;;
esac如果支持多模块:
if [[ "$MODULE_LIST" == "all" ]]; then
MODULE_LIST="demo-api,demo-admin,demo-job"
fi
IFS=',' read -r -a MODULES <<< "$MODULE_LIST"
for module in "${MODULES[@]}"; do
echo "deploy module=${module}"
done这里要注意,不要让用户在参数里输入任意主机、任意路径、任意 shell 命令。灵活性应该来自清晰参数模型,不是把命令行权限交给页面输入框。
日志要能让人跟着排查
Jenkins 排障一般看四类日志:
- Jenkins controller 服务日志。
- Jenkins 构建控制台输出。
- agent 机器资源和命令输出。
- 目标服务器应用日志。
controller 日志:
sudo journalctl -u jenkins.service -n 200 --no-pager
sudo journalctl -u jenkins.service -fagent 资源:
df -h
free -h
uptime
docker system df目标服务:
docker ps
docker logs --tail=200 demo-api
systemctl status demo-api --no-pager
journalctl -u demo-api -n 200 --no-pagerJenkinsfile 里建议开启:
options {
timestamps()
ansiColor('xterm')
}Shell 脚本里统一日志格式:
log() {
printf '%s [%s] %s\n' "$(date '+%F %T')" "$1" "$2" >&2
}日志里不要输出密码、token、私钥、数据库连接串和用户隐私数据。日志要能排查问题,但不能变成泄密通道。
常见问题按现象查
Jenkins 启动失败
先看状态:
sudo systemctl status jenkins --no-pager
sudo journalctl -u jenkins.service -n 100 --no-pager如果日志里有 Java 版本错误,执行:
java -version当前 LTS 线不要用 Java 8、11、17 去启动 controller。先升级到 Java 21。
如果是端口冲突:
sudo ss -lntp | grep ':8080'找到占用进程后再决定是改 Jenkins 端口,还是释放已有端口。
插件安装失败
先看是不是网络:
curl -I https://updates.jenkins.io/update-center.actual.json
df -h如果网络不可达,走企业代理或离线插件包。如果插件提示需要更高 Jenkins core,就不要硬装,先确认 Jenkins LTS 和插件版本组合。
Git 拉取失败
常见报错:
Permission denied (publickey)
Authentication failed
Couldn't find any revision to build在 agent 上测试:
ssh -T git@git.example.com
git ls-remote git@example.com:team/demo-service.git如果 ls-remote 都失败,问题在凭证、网络或代码平台权限,不在 Jenkinsfile。
Maven 构建失败
先确认版本:
java -version
mvn -version再看依赖:
mvn -B -s .ci/settings.xml -X dependency:resolve本地成功、CI 失败时,不要马上改 Jenkins。先用干净环境复现,因为开发机 .m2 缓存经常会把问题盖住。
Node 构建失败
先看版本和 lockfile:
node -v
npm -v
npm ci --verbose如果 npm ci 报 lockfile 不一致,就修 lockfile,不要把命令改成 npm install 糊过去。
Docker 推送失败
先看 Docker:
docker version
docker info
docker system df再看登录:
printf '%s' "$REGISTRY_PASSWORD" | docker login "$REGISTRY" \
-u "$REGISTRY_USER" \
--password-stdinaccess denied 大多是镜像仓库项目名、tag 路径或机器人账号权限问题。
部署后访问失败
先在目标机本机查:
docker ps -a --filter "name=demo-api"
docker logs --tail=200 demo-api
ss -lntp | grep 8080
curl -v http://127.0.0.1:8080/actuator/health本机通、外部不通,再查 Nginx、网关、防火墙、安全组、域名和证书。不要一开始就怀疑代码。
回滚失败
先确认旧镜像还在:
docker pull harbor.example.com/team/demo-api:41-f0e1d2c3b4a5再确认配置和数据库兼容。很多回滚失败不是脚本错,而是旧版本已经无法适配新配置或新表结构。
落地工程深水区
Jenkins 深水区在于:它把团队发布权集中起来以后,任何小洞都会变成大事故。
controller 和 agent 分工:controller 只负责调度、凭证、配置和流水线状态,不跑业务构建。Maven、Node、Docker build、K8S 发布放到 agent。否则一个大构建就可能拖垮整个 Jenkins。
插件和升级:插件不能一股脑升级。先在测试 Jenkins 里升级核心和插件,导出插件清单,确认 Jenkinsfile、凭证、agent 连接、权限策略都正常。生产升级前备份 JENKINS_HOME,保留回退版本。
凭证和权限:凭证只暴露 ID,流水线不打印 token。生产发布权限单独控制,参数化构建不要做成自由输入命令。能枚举的环境、分支、版本,就不要让用户随便填字符串。
制品和可追溯:每次发布要能回答:commit、构建号、制品地址、sha256、镜像 tag/digest、目标环境、操作者、验证结果和回滚版本。否则 Jenkins 只是“自动执行脚本”,不是 CI/CD 治理。
并发和锁:同一个服务、同一个环境不能并发部署。流水线要加锁或队列,防止两次发布互相覆盖配置、镜像和回滚点。
日志和告警:Jenkins 日志、构建日志、部署脚本日志、目标服务日志要能串起来。构建失败、部署失败、健康检查失败、磁盘高水位、agent 离线、插件下载失败都应该有告警。
离线和内网:插件、JDK、Maven/npm 依赖、Docker 镜像、基础镜像、部署脚本都要纳入离线包或内网仓库。不要让 Jenkins 在生产发布时临时访问公网。
离线交付要交付一套包
完全离线环境下,不要只带一份 Jenkinsfile。要带一套能闭环的交付包:
Jenkins 安装包或 war
Jenkins 插件包和 plugins.txt
JDK 安装包
Maven 安装包和 settings.xml
Node 安装包
npm 离线缓存或私服同步包
基础 Docker 镜像
业务 Docker 镜像
部署脚本
回滚脚本
环境检查脚本
SHA256 校验文件
验证清单生成校验文件:
find delivery-bundle -type f -print0 | sort -z | xargs -0 sha256sum > SHA256SUMS
sha256sum -c SHA256SUMS离线迁移镜像:
docker save -o demo-api-42-a1b2c3d4e5f6.tar harbor.example.com/team/demo-api:42-a1b2c3d4e5f6
sha256sum demo-api-42-a1b2c3d4e5f6.tar > demo-api-42-a1b2c3d4e5f6.tar.sha256
sha256sum -c demo-api-42-a1b2c3d4e5f6.tar.sha256
docker load -i demo-api-42-a1b2c3d4e5f6.tar加载后检查:
docker image ls | grep demo-api离线环境最忌讳“现场再下载”。一旦网络不通,发布窗口就会变成临时救火。所有依赖、镜像、插件、脚本都要提前准备,并且能校验。
生产上线检查清单
Jenkins:
LTS 版本固定
Java 21 运行正常
controller 不跑业务构建
JENKINS_HOME 有备份
插件版本有记录
匿名危险权限已关闭
凭证按环境拆分构建:
Jenkinsfile 入库
Maven 使用企业 settings.xml
Node 使用 npm ci
制品带版本号和 sha256
测试报告归档
构建历史有保留策略镜像:
基础镜像版本固定
镜像 tag 绑定 commit 和 build number
镜像推送到企业 registry
流水线使用机器人账号
旧镜像保留周期覆盖回滚窗口部署:
部署脚本入库
参数缺失会失败
并发发布有锁
远程执行使用专用账号
部署后健康检查
失败返回非 0回滚:
上一版本可查询
上一镜像未清理
配置兼容旧版本
数据库变更有兼容策略
回滚脚本可执行
回滚后也做健康检查Jenkins 做到最后,不是页面上多几个漂亮的 stage,而是团队每次发布都能回答这几个问题:
这次发的是哪次提交?
制品在哪里?
镜像 tag 是什么?
谁触发了发布?
部署到了哪些机器?
健康检查有没有通过?
失败后看哪份日志?
上一版怎么回去?这些问题都有答案,Jenkins 才真正从“自动打包工具”变成“CI/CD 工程闭环”。
官方文档入口
- Jenkins 下载:https://www.jenkins.io/download/
- Jenkins Java 支持策略:https://www.jenkins.io/doc/book/platform-information/support-policy-java/
- Jenkins Linux 安装:https://www.jenkins.io/doc/book/installing/linux/
- Jenkins Docker 安装:https://www.jenkins.io/doc/book/installing/docker/
- Jenkinsfile:https://www.jenkins.io/doc/book/pipeline/jenkinsfile/
- Pipeline Syntax:https://www.jenkins.io/doc/book/pipeline/syntax/
- Jenkins Credentials:https://www.jenkins.io/doc/book/using/using-credentials/
- Jenkins Offline Installations:https://www.jenkins.io/doc/book/installing/offline/
- Jenkins Managing Nodes:https://www.jenkins.io/doc/book/managing/nodes/
- Jenkins Plugin Installation Manager:https://github.com/jenkinsci/plugin-installation-manager-tool
- Maven mirror settings:https://maven.apache.org/guides/mini/guide-mirror-settings.html
- npm ci:https://docs.npmjs.com/cli/v8/commands/npm-ci/
- Docker build/tag/push:https://docs.docker.com/get-started/docker-concepts/building-images/build-tag-and-publish-an-image/
- Docker login:https://docs.docker.com/reference/cli/docker/login/
- SonarQube Jenkins Pipeline Steps:https://www.jenkins.io/doc/pipeline/steps/sonar/
- Harbor Robot Accounts:https://goharbor.io/docs/2.11.0/administration/robot-accounts/
