基于 Git 的自动集成交付(Git-Driven CI/CD)实战
核心理念 :Every commit is a potential release
技术栈 :Gitea + Webhook + Jenkins + SonarQube + Docker Registry + K3s/Kubernetes
目标 :实现「代码提交 → 自动构建 → 测试 → 部署 → 验证」零人工干预
实战环境:华为云 ecs-6ce9 集群 × 4台 ECS(Ubuntu 24.04 / Docker 29.5.3)
目录
- [第一部分:为什么是 Git 驱动?------现代 CI/CD 的范式转变](#第一部分:为什么是 Git 驱动?——现代 CI/CD 的范式转变)
- [第二部分:Git 事件监听与触发机制](#第二部分:Git 事件监听与触发机制)
- 第三部分:自动化流水线设计
- 第四部分:自动化测试与质量门禁
- [第五部分:自动化交付与部署(CD 核心)](#第五部分:自动化交付与部署(CD 核心))
- 第六部分:企业级实战案例
- 附录:工具与模板库
实战环境总览
本次实战基于华为云 ecs-6ce9 集群 4 台 ECS,采用分层架构设计:
┌──────────────────────────────────────────────────────────────┐
│ Git-Driven CI/CD 架构 │
├─────────────┬───────────────┬────────────────┬───────────────┤
│ ecs-6ce9 │ ecs-6ce9 │ ecs-6ce9 │ ecs-6ce9 │
│ 0001 │ 0002 │ 0003 │ 0004 │
│ │ │ │ │
│ ┌───────┐ │ ┌──────────┐ │ ┌──────────┐ │ ┌──────────┐ │
│ │ Gitea │ │ │ Jenkins │ │ │SonarQube │ │ │ K3s │ │
│ │ Git │──│→ │ CI/CD │─│→ │ Quality │ │ │ Deploy │ │
│ │Server │ │ │ Engine │ │ │ Gate │ │ │ Target │ │
│ └───────┘ │ └──────────┘ │ └──────────┘ │ └──────────┘ │
│ :3000 │ :8080 │ :9000 │ :6443 │
│ │ │ :5000 Registry│ │
├─────────────┼───────────────┼────────────────┼───────────────┤
│ 1.92.95.186 │ 123.249.68.214│ 1.94.247.115 │ 120.46.154.100│
└─────────────┴───────────────┴────────────────┴───────────────┘
↑ ↑ ↑ ↑
代码提交 构建测试 质量扫描 GitOps 部署
| 角色 | 服务器 | 公网 IP | 服务 | 端口 |
|---|---|---|---|---|
| Git 源码仓库 | ecs-6ce9-0001 | 1.92.95.186 | Gitea | 3000 / 2222(SSH) |
| CI/CD 引擎 | ecs-6ce9-0002 | 123.249.68.214 | Jenkins LTS | 8080 |
| 质量门禁 | ecs-6ce9-0003 | 1.94.247.115 | SonarQube + Registry | 9000 / 5000 |
| 部署目标 | ecs-6ce9-0004 | 120.46.154.100 | K3s v1.35.5 | 6443 |
服务器规格:
- 实例:2vCPUs / 4GiB / ac9.large.2
- 系统:Ubuntu 24.04 Server 64bit
- Docker:29.5.3(阿里云镜像加速)
- 网络:弹性公网 IP 5Mbit/s BGP, 私网 192.168.0.0/24
第一部分:为什么是 Git 驱动?------现代 CI/CD 的范式转变
1.1 从"手动触发"到"事件驱动"
传统 CI/CD 模式的核心痛点是 人与流程的脱节。开发者在本地写好代码、推送到仓库后,还需要登录 Jenkins 手动点击"立即构建",这意味着:
❌ 旧流程(手动触发模式):
开发者提交代码 → 打开 Jenkins → 找对应 Job → 点击 Build Now → 等待 → 查看结果
⏱ 耗时:5-15 分钟(含人工操作时间)
⚠ 风险:漏触发 / 错触发 / 环境不一致
✅ 新流程(Git 事件驱动模式):
git push → Webhook → 自动触发流水线 → 3 分钟后服务已更新
⏱ 耗时:2-3 分钟(全自动)
✅ 保证:每次提交都触发 / 分支隔离 / 状态一致
Git 作为单一事实源(Single Source of Truth) 是整个 Git-Driven CI/CD 的基石:
Git 仓库 = 唯一真理之源
│
┌─────────────────┼─────────────────┐
▼ ▼ ▼
代码版本 构建配置 部署清单
(src/main/java) (Jenkinsfile) (k8s/deploy.yaml)
│ │ │
└─────────────────┼──────────────────┘
▼
自动化流水线
(构建 → 测试 → 部署 → 验证)
1.2 Git 事件类型与交付策略映射
并非所有 Git 事件都应该触发相同的流程。根据事件类型和分支,我们需要设计 智能路由:
| Git 事件 | 触发动作 | 适用场景 | 是否需审批 |
|---|---|---|---|
push 到 dev |
构建 + 单元测试 + 静态扫描 | 日常开发集成 | ❌ 自动 |
push 到 main |
构建 + 全量测试 + 部署到 TEST | 集成测试环境 | ❌ 自动 |
创建 Tag(如 v1.2.3) |
构建 + 安全扫描 + 部署到 PROD | 正式发布 | ✅ 需审批 |
创建 PR/MR |
预合并检查(Lint + Unit Test) | 代码审查前拦截 | ❌ 自动 |
Commit Message 含 [ci skip] |
跳过构建 | 文档/注释修改 | ❌ 跳过 |
Commit Message 含 [deploy:prod] |
直接部署生产(权限校验) | 紧急热修复 | ✅ 需权限 |
实战验证:以下是在 ecs-6ce9 集群中实际配置的 Git 事件响应矩阵:
Git Push Event
│
├── refs/heads/dev/* ──→ 构建 + 单元测试 + SonarQube 扫描 + 部署 DEV
│
├── refs/heads/main ───→ 构建 + 全量测试 + 部署 TEST + 通知
│
├── refs/tags/v* ──────→ 构建 + 安全扫描 + 人工审批 ──→ 部署 PROD
│
└── refs/pull/* ───────→ Lint + Test (不构建镜像)
1.3 Git-Driven vs 传统 CI/CD 对比
| 维度 | 传统 Jenkins 触发 | Git-Driven CI/CD |
|---|---|---|
| 触发方式 | 手动点击 / 定时轮询 | Webhook 自动事件 |
| 延迟 | 分钟级(含人工) | 秒级 |
| 准确性 | 可能漏触发 | 100% 覆盖 |
| 分支策略 | 靠人工区分 | 自动分支路由 |
| 环境一致性 | 依赖人工配置 | Git 中一切皆代码 |
| 可追溯性 | 弱(谁点了按钮?) | 强(每次 commit 关联) |
| 回滚能力 | 手动操作 | Git Revert → 自动部署 |
第二部分:Git 事件监听与触发机制(核心基础)
2.1 Webhook 实现原理
Webhook 是 Git 事件驱动的核心------它本质上是一个 HTTP POST 回调,当 Git 仓库发生特定事件时,向指定的 URL 发送 JSON Payload。
Webhook 工作流程
═══════════════
┌──────────┐ ① git push ┌──────────┐
│ Developer │ ──────────────→ │ Gitea │
└──────────┘ └────┬─────┘
│
② Webhook POST
JSON Payload
│
▼
┌──────────┐
│ Jenkins │
│ Generic │
│ Webhook │
│ Trigger │
└────┬─────┘
│
③ Parse JSON
④ Match Token
⑤ Trigger Pipeline
│
▼
┌──────────────┐
│ 流水线执行 │
│ Build/Test │
│ /Deploy │
└──────────────┘
2.2 Gitea Webhook 配置实战
我们在 ecs-6ce9-0001 上部署 Gitea,然后配置 Webhook:
步骤 1:部署 Gitea
bash
# 在 ecs-6ce9-0001 (1.92.95.186) 上执行
docker run -d --name gitea \
-p 3000:3000 -p 2222:22 \
-v /data/gitea:/data \
--restart=always \
gitea/gitea:latest
验证:
bash
$ curl -s http://localhost:3000 | head -3
<!DOCTYPE html>
<html lang="en-US" data-theme="gitea-auto">
<head>
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Installation - Gitea: Git with a cup of tea</title>
注:首次访问 Gitea 时进入安装向导。在本次实战中我们通过 API 完成初始化。
步骤 2:通过 API 初始化 Gitea
bash
# 创建管理员账户
curl -X POST http://1.92.95.186:3000 \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "db_type=SQLite3&db_path=/data/gitea/gitea.db&
app_name=Git-Driven CI/CD&
repo_root_path=/data/gitea/gitea-repositories&
ssh_listen_port=2222&
admin_name=admin&
admin_passwd=admin123&
admin_email=admin@cicd.local"
步骤 3:创建示例仓库并配置 Webhook
bash
# 通过 API 创建仓库
TOKEN=$(curl -s -X POST http://1.92.95.186:3000/api/v1/users/admin/tokens \
-u admin:admin123 \
-H "Content-Type: application/json" \
-d '{"name":"cicd-token"}' | python3 -c "import sys,json; print(json.load(sys.stdin)['sha1'])")
# 创建仓库
curl -X POST http://1.92.95.186:3000/api/v1/user/repos \
-H "Authorization: token $TOKEN" \
-H "Content-Type: application/json" \
-d '{"name":"spring-boot-app","private":false,"auto_init":true}'
# 配置 Webhook
curl -X POST "http://1.92.95.186:3000/api/v1/repos/admin/spring-boot-app/hooks" \
-H "Authorization: token $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"type": "gitea",
"config": {
"url": "http://123.249.68.214:8080/generic-webhook-trigger/invoke?token=cicd-secret-token",
"content_type": "json",
"secret": "my-webhook-secret-2024"
},
"events": ["push", "create", "pull_request"],
"active": true
}'
Webhook Payload 完整解析:
以下是 Gitea 在 git push 时发送的 JSON Payload 关键字段:
json
{
"ref": "refs/heads/main", // 【核心】推送的目标分支
"before": "a1b2c3d4e5f6...", // 推送前的 HEAD SHA
"after": "f6e5d4c3b2a1...", // 推送后的 HEAD SHA
"compare_url": "...", // 差异对比 URL
"commits": [
{
"id": "f6e5d4c3b2a1...", // Commit SHA
"message": "feat: add user API", // Commit Message
"author": {
"name": "Developer",
"email": "dev@example.com"
},
"timestamp": "2026-06-08T10:00:00Z"
}
],
"repository": {
"name": "spring-boot-app",
"full_name": "admin/spring-boot-app",
"clone_url": "http://1.92.95.186:3000/admin/spring-boot-app.git",
"ssh_url": "ssh://git@1.92.95.186:2222/admin/spring-boot-app.git"
},
"pusher": {
"name": "admin",
"email": "admin@cicd.local"
}
}
2.3 安全加固:Secret Token 验证
Webhook 的安全是重中之重。以下是我们实施的 五层防护:
Webhook 安全五层防护模型
═══════════════════════════
Layer 1 ┌─────────────────────────────────────┐
│ Secret Token 签名验证(HMAC-SHA256) │ ← 基础
└─────────────────────────────────────┘
Layer 2 ┌─────────────────────────────────────┐
│ IP 白名单(仅允许 Gitea 服务器 IP) │ ← 网络层
└─────────────────────────────────────┘
Layer 3 ┌─────────────────────────────────────┐
│ 请求速率限制(防重放攻击) │ ← 应用层
└─────────────────────────────────────┘
Layer 4 ┌─────────────────────────────────────┐
│ Payload 完整性校验(Header + Body) │ ← 传输层
└─────────────────────────────────────┘
Layer 5 ┌─────────────────────────────────────┐
│ 仅接收 TLS 加密连接(HTTPS) │ ← 通道层
└─────────────────────────────────────┘
Jenkins Generic Webhook Trigger 插件中的 Token 配置:
groovy
// Jenkins Job 配置中的 Token 验证
pipeline {
triggers {
GenericTrigger(
genericVariables: [
[key: 'GIT_REF', value: '$.ref'],
[key: 'GIT_COMMIT', value: '$.after'],
[key: 'GIT_REPO', value: '$.repository.full_name'],
[key: 'COMMIT_MSG', value: '$.commits[0].message'],
[key: 'PUSHER', value: '$.pusher.name']
],
token: 'cicd-secret-token', // ← Secret Token
causeString: 'Triggered by Git push on $GIT_REF',
printContributedVariables: true,
printPostContent: true
)
}
// ...
}
2.4 内网穿透方案(解决内网 Jenkins 收 Webhook)
在生产环境中,Jenkins 往往部署在内网,无法直接接收来自公网 Git 服务器的 Webhook。以下是企业常用解决方案:
| 方案 | 原理 | 优点 | 缺点 | 推荐度 |
|---|---|---|---|---|
| ngrok | 内网穿透隧道 | 5 分钟搭好 | 免费版域名随机变 | ⭐⭐⭐ 开发/测试 |
| Cloudflare Tunnel | Cloudflare 隧道 | 免费、稳定、自带域名 | 需域名解析到 CF | ⭐⭐⭐⭐⭐ 生产 |
| frp | 自建穿透 | 完全自主可控 | 需公网中转服务器 | ⭐⭐⭐⭐ 企业 |
| API 网关 | 网关代理 | 运维成熟方案 | 架构复杂度高 | ⭐⭐⭐⭐ 大型企业 |
ngrok 快速配置示例:
bash
# 在 Jenkins 所在服务器上启动 ngrok 隧道
ngrok http 8080
# 输出示例:
# Forwarding https://abc123.ngrok-free.app -> http://localhost:8080
然后将 Webhook URL 设置为 https://abc123.ngrok-free.app/generic-webhook-trigger/invoke?token=cicd-secret-token 即可。
本次实战说明:4 台服务器均为公网 ECS,Jenkins 直接暴露在公网,无需穿透。但在生产环境强烈建议使用 Cloudflare Tunnel 或 frp。
2.5 进阶:Git Hooks 本地预检(Pre-commit)
在代码推送到远程仓库之前,本地 Git Hooks 可以进行第一层拦截:
开发流程图:Pre-commit Hook 拦截
═══════════════════════════════════
git add . ──→ git commit ──→ Pre-commit Hook 触发
│
┌───────────┴───────────┐
▼ ▼
检查通过 检查失败
│ │
▼ ▼
git push commit 被拒绝
│ 显示错误信息
▼
Webhook 触发 CI/CD
实战:配置 Husky(Node.js 项目)
bash
# 安装 Husky
npm install --save-dev husky lint-staged
# 初始化 Git Hooks
npx husky init
# 配置 pre-commit
cat > .husky/pre-commit << 'EOF'
#!/bin/sh
npx lint-staged
EOF
# lint-staged 配置(package.json)
json
{
"lint-staged": {
"*.{js,ts,tsx}": ["eslint --fix", "prettier --write"],
"*.{java}": ["mvn spotless:apply"],
"*.{py}": ["black --check", "flake8"]
}
}
企业级:禁止直接 push 到 main
bash
# 在 Gitea 中设置分支保护规则
# Settings → Branches → Branch Protection → Add Rule
# Branch Name Pattern: main
# ☑ Enable Push Restriction
# ☑ Enable Merge Whitelist
# ☑ Require Pull Request Reviews
第三部分:自动化流水线设计(以 Jenkins Pipeline 为例)
3.1 Jenkins 部署(ecs-6ce9-0002)
在 ecs-6ce9-0002 上部署 Jenkins,并通过 Groovy Init Script 实现无人值守初始化:
bash
# 1. 创建 Groovy 安全初始化脚本
mkdir -p /data/jenkins/init.groovy.d
cat > /data/jenkins/init.groovy.d/basic-security.groovy << 'GRVEOF'
import jenkins.model.*
import hudson.security.*
def instance = Jenkins.getInstance()
// 创建管理员账户
def hudsonRealm = new HudsonPrivateSecurityRealm(false)
hudsonRealm.createAccount('admin', 'admin123')
instance.setSecurityRealm(hudsonRealm)
// 授权策略
def strategy = new FullControlOnceLoggedInAuthorizationStrategy()
strategy.setAllowAnonymousRead(false)
instance.setAuthorizationStrategy(strategy)
instance.save()
GRVEOF
# 2. 启动 Jenkins(跳过 Setup Wizard)
chown -R 1000:1000 /data/jenkins
docker run -d --name jenkins \
-p 8080:8080 -p 50000:50000 \
-v /data/jenkins:/var/jenkins_home \
-v /var/run/docker.sock:/var/run/docker.sock \
-e JAVA_OPTS="-Djenkins.install.runSetupWizard=false -Duser.timezone=Asia/Shanghai" \
--restart=always \
jenkins/jenkins:lts
关键参数解释:
-Djenkins.install.runSetupWizard=false:跳过初始化向导,配合 init.groovy.d 实现无人值守-v /var/run/docker.sock:挂载 Docker Socket,使 Jenkins 可以构建 Docker 镜像-Duser.timezone=Asia/Shanghai:时区设置,避免日志时间显示 UTC
3.2 动态流水线:根据 Git 事件智能分支
这是 Git-Driven CI/CD 的核心------一条 Pipeline,根据 Git 事件自动选择执行路径:
groovy
// Jenkinsfile:Git-Driven 智能流水线
pipeline {
agent any
// 定义 Webhook 触发参数
triggers {
GenericTrigger(
genericVariables: [
[key: 'GIT_REF', value: '$.ref'],
[key: 'GIT_COMMIT', value: '$.after'],
[key: 'GIT_REPO', value: '$.repository.full_name'],
[key: 'COMMIT_MSG', value: '$.commits[0].message'],
[key: 'PUSHER', value: '$.pusher.name']
],
token: 'cicd-secret-token',
causeString: 'Triggered by Git $GIT_REF',
printContributedVariables: true,
printPostContent: true
)
}
// 参数化构建变量
parameters {
string(name: 'GIT_REF', defaultValue: '', description: 'Git ref (branch/tag)')
string(name: 'GIT_COMMIT', defaultValue: '', description: 'Commit SHA')
string(name: 'COMMIT_MSG', defaultValue: '', description: 'Commit Message')
}
// 全局环境变量
environment {
// 从 GIT_REF 提取分支名/Tag名
BRANCH = sh(script: "echo ${GIT_REF} | sed 's|refs/heads/||' | sed 's|refs/tags/||'", returnStdout: true).trim()
IS_TAG = sh(script: "echo ${GIT_REF} | grep -q 'refs/tags' && echo true || echo false", returnStdout: true).trim()
IS_MAIN = sh(script: "[ '$BRANCH' = 'main' ] && echo true || echo false", returnStdout: true).trim()
// 语义化版本号
VERSION = "${BRANCH}-${BUILD_NUMBER}-${GIT_COMMIT.substring(0,7)}"
// Docker 镜像标签
DOCKER_IMAGE = "1.94.247.115:5000/spring-boot-app"
DOCKER_TAG = IS_TAG == 'true' ? sh(script: "echo ${GIT_REF} | sed 's|refs/tags/||'", returnStdout: true).trim() : VERSION
}
stages {
// ============ Stage 1:代码检出 ============
stage('Checkout') {
steps {
echo "📦 Checkout: ${GIT_REPO} @ ${BRANCH}"
checkout([
$class: 'GitSCM',
branches: [[name: "${BRANCH}"]],
userRemoteConfigs: [[
url: "http://admin:${GITEA_TOKEN}@1.92.95.186:3000/${GIT_REPO}.git"
]]
])
}
}
// ============ Stage 2:静态代码检查(全分支)============
stage('Static Analysis') {
steps {
echo "🔍 Lint + Checkstyle 分析..."
sh '''
mvn checkstyle:check -Dcheckstyle.config.location=google_checks.xml || true
mvn pmd:check || true
'''
}
}
// ============ Stage 3:单元测试(全分支)============
stage('Unit Test') {
steps {
echo "🧪 运行单元测试..."
sh 'mvn test -B'
}
post {
always {
junit '**/target/surefire-reports/*.xml'
}
}
}
// ============ Stage 4:构建打包(dev/main/tag 分支)============
stage('Build') {
when {
expression { return BRANCH ==~ /dev|main/ || IS_TAG == 'true' }
}
steps {
echo "🏗 构建 Spring Boot JAR..."
sh 'mvn clean package -DskipTests -B'
archiveArtifacts artifacts: 'target/*.jar', fingerprint: true
}
}
// ============ Stage 5:SonarQube 代码分析(dev/main 分支)============
stage('SonarQube Analysis') {
when {
expression { return BRANCH ==~ /dev|main/ }
}
steps {
echo "📊 SonarQube 质量扫描..."
withSonarQubeEnv('SonarQube') {
sh '''
mvn sonar:sonar \
-Dsonar.host.url=http://1.94.247.115:9000 \
-Dsonar.projectKey=spring-boot-app \
-Dsonar.projectName=spring-boot-app:${BRANCH} \
-Dsonar.java.binaries=target/classes
'''
}
}
}
// ============ Stage 6:Quality Gate 质量门禁(main 分支)============
stage('Quality Gate') {
when {
expression { return BRANCH == 'main' }
}
steps {
echo "🚧 等待 SonarQube Quality Gate..."
timeout(time: 5, unit: 'MINUTES') {
waitForQualityGate abortPipeline: true
}
}
}
// ============ Stage 7:Docker 构建并推送(main/tag 分支)============
stage('Docker Build & Push') {
when {
expression { return BRANCH == 'main' || IS_TAG == 'true' }
}
steps {
echo "🐳 构建 Docker 镜像: ${DOCKER_IMAGE}:${DOCKER_TAG}"
sh """
docker build -t ${DOCKER_IMAGE}:${DOCKER_TAG} .
docker tag ${DOCKER_IMAGE}:${DOCKER_TAG} ${DOCKER_IMAGE}:latest
docker push ${DOCKER_IMAGE}:${DOCKER_TAG}
docker push ${DOCKER_IMAGE}:latest
"""
}
}
// ============ Stage 8:部署到 DEV 环境(dev 分支)============
stage('Deploy to DEV') {
when {
expression { return BRANCH ==~ /dev.*/ }
}
steps {
echo "🚀 部署到 DEV 环境"
sh """
ssh root@120.46.154.100 "
kubectl set image deployment/spring-boot-app \
spring-boot-app=${DOCKER_IMAGE}:${DOCKER_TAG} \
-n dev
kubectl rollout status deployment/spring-boot-app -n dev
"
"""
}
}
// ============ Stage 9:部署到 TEST 环境(main 分支)============
stage('Deploy to TEST') {
when {
expression { return BRANCH == 'main' }
}
steps {
echo "🧪 部署到 TEST 环境"
sh """
ssh root@120.46.154.100 "
kubectl set image deployment/spring-boot-app \
spring-boot-app=${DOCKER_IMAGE}:${DOCKER_TAG} \
-n test
kubectl rollout status deployment/spring-boot-app -n test
"
"""
}
}
// ============ Stage 10:生产发布审批 + 部署(Tag 推送)============
stage('Approve PROD Deploy') {
when {
expression { return IS_TAG == 'true' }
}
steps {
echo "⏸ 等待生产发布审批..."
input message: "确认将版本 ${DOCKER_TAG} 部署到生产环境?",
ok: "批准发布",
submitter: 'admin,ops-team'
}
}
stage('Deploy to PROD') {
when {
expression { return IS_TAG == 'true' }
}
steps {
echo "🎯 部署到 PROD 生产环境"
sh """
ssh root@120.46.154.100 "
kubectl set image deployment/spring-boot-app \
spring-boot-app=${DOCKER_IMAGE}:${DOCKER_TAG} \
-n prod
kubectl rollout status deployment/spring-boot-app -n prod
"
"""
}
}
}
// ============ 后处理(通知 + 清理)============
post {
success {
echo "✅ Pipeline 执行成功!"
script {
def msg = "✅ **${JOB_NAME}** #${BUILD_NUMBER}\n" +
"- 分支: ${BRANCH}\n" +
"- 提交: ${GIT_COMMIT}\n" +
"- 镜像: ${DOCKER_IMAGE}:${DOCKER_TAG}\n" +
"- 时长: ${currentBuild.durationString}"
echo msg
// 可接入钉钉/企业微信通知
// dingtalk(robot: 'ci', type: 'MARKDOWN', title: 'Build Success', text: [msg])
}
}
failure {
echo "❌ Pipeline 执行失败!"
}
cleanup {
cleanWs() // 清理工作空间
}
}
}
流水线执行流程可视化:
Git Push
│
┌───────────┴───────────┐
▼ ▼
refs/heads/dev refs/heads/main refs/tags/v*
│ │ │
▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌──────────┐
│ Checkout │ │ Checkout │ │ Checkout │
└────┬─────┘ └────┬─────┘ └────┬─────┘
│ │ │
▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌──────────┐
│Lint+Test │ │Lint+Test │ │Lint+Test │
└────┬─────┘ └────┬─────┘ └────┬─────┘
│ │ │
▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌──────────┐
│ Build │ │ Build │ │ Build │
└────┬─────┘ └────┬─────┘ └────┬─────┘
│ │ │
▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌──────────┐
│ SonarQube │ │ SonarQube │ │ 安全扫描 │
└────┬─────┘ └────┬─────┘ └────┬─────┘
│ │ │
│ ▼ ▼
│ ┌──────────┐ ┌──────────┐
│ │QualityGate│ │⏸ 人工审批 │
│ └────┬─────┘ └────┬─────┘
│ │ │
▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌──────────┐
│Deploy DEV│ │Deploy TEST│ │Deploy PROD│
└──────────┘ └──────────┘ └──────────┘
3.3 多环境自动路由策略
策略一:分支命名规范驱动
| 分支模式 | 部署环境 | 说明 |
|---|---|---|
feat/*, dev* |
DEV | 功能开发分支 |
release/* |
TEST | 发布候选分支 |
main |
TEST → PROD(审批) | 主分支 |
tag v* |
PROD | 正式发布 |
在 Jenkinsfile 中实现:
groovy
def getEnvByBranch(String branch) {
switch (branch) {
case ~/dev.*/:
return 'dev'
case ~/release.*/:
return 'test'
case 'main':
return 'test' // main 默认部署到 TEST
default:
return 'dev'
}
}
策略二:Commit Message 关键字控制
groovy
// 解析 Commit Message 中的指令
def parseCommitDirectives(String msg) {
def directives = [:]
// [ci skip] - 跳过 CI
directives.ciSkip = msg.contains('[ci skip]')
// [deploy:prod] - 直推生产
directives.deployProd = msg.contains('[deploy:prod]')
// [deploy:skip] - 跳过部署
directives.skipDeploy = msg.contains('[deploy:skip]')
// [scan:full] - 全量扫描
directives.fullScan = msg.contains('[scan:full]')
return directives
}
// 使用示例
stage('Deploy') {
when {
expression { return !parseCommitDirectives(COMMIT_MSG).skipDeploy }
}
steps { ... }
}
3.4 版本号自动生成与管理
基于 Git Tag 的 语义化版本(SemVer):
bash
#!/bin/bash
# auto_version.sh - 自动生成版本号
# 获取最新 Tag
LAST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "v0.0.0")
# 解析 SemVer
MAJOR=$(echo $LAST_TAG | cut -d'.' -f1 | sed 's/v//')
MINOR=$(echo $LAST_TAG | cut -d'.' -f2)
PATCH=$(echo $LAST_TAG | cut -d'.' -f3)
# 根据 Commit Message 确定版本递增
COMMIT_MSG=$(git log -1 --pretty=%B)
if echo "$COMMIT_MSG" | grep -qi "BREAKING CHANGE"; then
MAJOR=$((MAJOR + 1)); MINOR=0; PATCH=0
elif echo "$COMMIT_MSG" | grep -qi "^feat"; then
MINOR=$((MINOR + 1)); PATCH=0
else
PATCH=$((PATCH + 1))
fi
NEW_VERSION="v${MAJOR}.${MINOR}.${PATCH}"
echo "New Version: $NEW_VERSION (from $LAST_TAG)"
第四部分:自动化测试与质量门禁
4.1 SonarQube 部署(ecs-6ce9-0003)
bash
# 在 ecs-6ce9-0003 (1.94.247.115) 上执行
# SonarQube 需要提高 mmap 限制
sysctl -w vm.max_map_count=262144
echo 'vm.max_map_count=262144' >> /etc/sysctl.conf
mkdir -p /data/sonarqube/{data,logs,extensions}
chown -R 1000:1000 /data/sonarqube
docker run -d --name sonarqube \
-p 9000:9000 \
-v /data/sonarqube/data:/opt/sonarqube/data \
-v /data/sonarqube/logs:/opt/sonarqube/logs \
-v /data/sonarqube/extensions:/opt/sonarqube/extensions \
-e SONAR_ES_BOOTSTRAP_CHECKS_DISABLE=true \
--restart=always \
sonarqube:lts-community
# 同时部署 Docker Registry 作为镜像仓库
docker run -d --name registry \
-p 5000:5000 \
-v /data/registry:/var/lib/registry \
--restart=always \
registry:2
4.2 测试左移:PR 阶段的自动化检查
测试左移矩阵
═══════════════
开发阶段 测试阶段 生产阶段
───────── ──────── ────────
┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐
│Pre-commit│───→│PR Check │───→│ CI │───→│部署验证 │───→│监控告警 │
│ Hook │ │ API │ │ Pipeline│ │ (CD) │ │(Prom/Gra)│
└────┬────┘ └────┬────┘ └────┬────┘ └────┬────┘ └────┬────┘
│ │ │ │ │
✔ Lint ✔ Lint ✔ Unit Test ✔ E2E ✔ 健康检查
✔ Format ✔ Unit Test ✔ Integration ✔ Smoke ✔ 日志异常
✔ TypeCheck ✔ Coverage⩾80% ✔ SonarQube ✔ Canary ✔ APM
PR 自动化检查 Pipeline:
groovy
// Jenkinsfile-pr.groovy - PR/MR 自动化检查
pipeline {
agent any
stages {
stage('PR Check: Lint') {
steps {
echo "🔍 代码风格检查"
sh 'mvn checkstyle:check'
}
}
stage('PR Check: Unit Test + Coverage') {
steps {
sh 'mvn test jacoco:report -B'
}
post {
always {
junit '**/target/surefire-reports/*.xml'
jacoco(
execPattern: '**/target/jacoco.exec',
classPattern: '**/target/classes',
sourcePattern: '**/src/main/java'
)
}
}
}
stage('PR Check: Coverage Gate') {
steps {
script {
// 检查覆盖率是否达到 80%
def coverage = sh(
script: "grep -oP 'INSTRUCTION.*?covered_ratio=\\\"[0-9.]+\\\"' target/site/jacoco/index.html | head -1",
returnStdout: true
).trim()
if (coverage.toFloat() < 0.80) {
error "代码覆盖率 ${coverage} < 80%,本次 PR 被拒绝!"
}
}
}
}
stage('PR Check: Vulnerability Scan') {
steps {
echo "🔐 OWASP 依赖安全检查"
sh 'mvn org.owasp:dependency-check-maven:check || true'
}
}
}
post {
success {
echo '✅ PR 检查通过,可以合并!'
}
failure {
echo '❌ PR 检查未通过,请修复后重新提交!'
}
}
}
4.3 SonarQube Quality Gate 配置
在 Jenkinsfile 中调用 SonarQube Quality Gate:
groovy
stage('SonarQube Quality Gate') {
steps {
withSonarQubeEnv('SonarQube') {
sh '''
mvn sonar:sonar \
-Dsonar.host.url=http://1.94.247.115:9000 \
-Dsonar.login=admin \
-Dsonar.password=admin \
-Dsonar.projectKey=spring-boot-app
'''
}
// 阻塞等待 Quality Gate 结果
timeout(time: 5, unit: 'MINUTES') {
def qg = waitForQualityGate()
if (qg.status != 'OK') {
error "❌ Quality Gate FAILED: ${qg.status}"
}
}
}
}
Quality Gate 配置项(SonarQube Web UI):
| 指标 | 阈值 | 说明 |
|---|---|---|
bugs |
= 0 | 阻断性 Bug 必须为 0 |
vulnerabilities |
= 0 | 安全漏洞必须为 0 |
code_smells |
< 50 | 代码异味限制 |
coverage |
≥ 80% | 代码覆盖率 |
duplicated_lines_density |
< 3% | 重复代码密度 |
security_hotspots |
Reviewed = 100% | 安全隐患全部审查 |
4.4 环境隔离测试策略
| 环境 | 测试类型 | 工具 | 执行频率 |
|---|---|---|---|
| DEV | 单元测试 + Lint | JUnit, Checkstyle | 每次 push |
| TEST | 集成测试 + API 测试 | RestAssured, Postman | push to main |
| STAGING | E2E + 性能压测 | Cypress, k6 | 发版前 |
| PROD | 监控告警 + A/B 验证 | Prometheus + Grafana | 持续 |
第五部分:自动化交付与部署(CD 核心)
5.1 K3s 部署(ecs-6ce9-0004)
在 ecs-6ce9-0004 上部署 K3s 作为 Kubernetes 集群(单节点):
bash
# 使用 Rancher 国内镜像加速
curl -sfL https://rancher-mirror.rancher.cn/k3s/k3s-install.sh | INSTALL_K3S_MIRROR=cn sh -
# 验证集群状态
kubectl get nodes
部署结果验证:
NAME STATUS ROLES AGE VERSION
ecs-6ce9-0004 Ready control-plane 7m40s v1.35.5+k3s1
NAMESPACE NAME STATUS
kube-system coredns-8db54c48d-sv59t Running
kube-system local-path-provisioner-... Running
kube-system metrics-server-786d997795-... Running
5.2 K8s 部署清单
为 Spring Boot 应用准备 Deployment + Service:
yaml
# k8s/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: spring-boot-app
labels:
app: spring-boot-app
spec:
replicas: 2
selector:
matchLabels:
app: spring-boot-app
template:
metadata:
labels:
app: spring-boot-app
version: "${IMAGE_TAG}"
spec:
containers:
- name: spring-boot-app
image: 1.94.247.115:5000/spring-boot-app:${IMAGE_TAG}
imagePullPolicy: Always
ports:
- containerPort: 8080
env:
- name: SPRING_PROFILES_ACTIVE
value: "${ENV}"
resources:
requests:
memory: "256Mi"
cpu: "100m"
limits:
memory: "512Mi"
cpu: "500m"
livenessProbe:
httpGet:
path: /actuator/health/liveness
port: 8080
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /actuator/health/readiness
port: 8080
initialDelaySeconds: 10
periodSeconds: 5
---
apiVersion: v1
kind: Service
metadata:
name: spring-boot-app-svc
spec:
selector:
app: spring-boot-app
ports:
- port: 80
targetPort: 8080
type: ClusterIP
5.3 蓝绿部署自动化
蓝绿部署是 CD 中的经典策略------新旧版本并存,通过 Service 切换流量:
蓝绿部署流程
════════════
Step 1: 部署新版本(Green)
┌─────────┐ ┌─────────┐
│ Blue(v1)│ │Green(v2)│ ← 新部署
│ 8080 │ │ 8080 │
└────┬────┘ └────┬────┘
│ │
└────────┬───────────┘
│
┌──────┴──────┐
│ Service │ ← 当前指向 Blue
│ selector=v1 │
└──────┬──────┘
│
用户流量
Step 2: 切换流量到 Green
┌─────────┐ ┌─────────┐
│ Blue(v1)│ │Green(v2)│
│ 闲置 │ │ 活跃 │ ← 接收流量
└─────────┘ └────┬────┘
│
┌──────┴──────┐
│ Service │ ← 更新 selector=v2
│ selector=v2 │
└──────┬──────┘
│
用户流量
Step 3: 清理旧版本(保留 10 分钟回滚窗口)
Jenkinsfile 蓝绿部署脚本:
groovy
stage('Blue-Green Deploy') {
steps {
script {
def newTag = "${env.DOCKER_TAG}"
def newVersion = "v${BUILD_NUMBER}"
// 1. 部署新版本(Green)
sh """
sed 's/\${IMAGE_TAG}/${newTag}/g' k8s/deployment.yaml | \
sed 's/\${ENV}/${env.ENV_NAME}/g' | \
sed 's/spring-boot-app/spring-boot-app-${newVersion}/g' | \
kubectl apply -f -
"""
// 2. 等待新版本就绪
sh "kubectl rollout status deployment/spring-boot-app-${newVersion} -n ${env.ENV_NAME} --timeout=120s"
// 3. 流量切换:更新 Service selector
sh """
kubectl patch service spring-boot-app-svc -n ${env.ENV_NAME} \
-p '{"spec":{"selector":{"app":"spring-boot-app","version":"${newVersion}"}}}'
"""
// 4. 旧版本保留 10 分钟后清理
sh """
nohup bash -c '
sleep 600
kubectl delete deployment spring-boot-app --ignore-not-found -n ${env.ENV_NAME}
' > /dev/null 2>&1 &
"""
}
}
}
5.4 回滚机制:一键恢复
groovy
stage('Rollback (Manual)') {
when {
expression { return params.ROLLBACK == true }
}
steps {
script {
echo "⏪ 回滚到上一版本..."
// 方法一:K8s rollout undo
sh "kubectl rollout undo deployment/spring-boot-app -n ${env.ENV_NAME} --to-revision=${params.REVISION}"
// 方法二:指定回滚到某个 Git Commit
sh "kubectl set image deployment/spring-boot-app spring-boot-app=${DOCKER_IMAGE}:${params.ROLLBACK_TAG} -n ${env.ENV_NAME}"
echo "✅ 回滚完成!当前版本: ${params.ROLLBACK_TAG}"
}
}
}
第六部分:企业级实战案例
案例一:微服务多仓库协同交付
问题:Service A 依赖 Service B 的 SNAPSHOT 版本,B 更新后 A 如何自动感知?
协同交付架构
══════════════
Service B 仓库 Service A 仓库
┌──────────┐ ┌──────────┐
│ B 提交代码│ │ A 监听B更新│
└────┬─────┘ └────┬─────┘
│ │
▼ │
┌──────────┐ │
│ B CI/CD │ │
│ Build │ │
│ Test │ │
│ Publish │────→ Nexus ─────────────┘
│ to Nexus │ (Maven仓库) ② A 的 Webhook 监听到
└──────────┘ B Artifact 更新
│ │
① B 构建完成 ▼
推送到 Nexus ┌──────────┐
│ A CI/CD │
│ Build │
│ Test with B│
│ Deploy │
└──────────┘
Jenkins Pipeline 实现:
groovy
// Service B 的 Jenkinsfile
stage('Publish to Nexus') {
steps {
sh 'mvn deploy -DskipTests'
// 部署成功后,触发 Service A 的重建
build job: 'service-a-pipeline', wait: false,
parameters: [
string(name: 'DEPENDENCY_VERSION', value: env.VERSION),
string(name: 'TRIGGERED_BY', value: 'service-b')
]
}
}
案例二:前端静态资源自动发布
git push → Webpack/Vite 构建 → OSS/CDN 上传 → Nginx 配置更新 → Cache Purge
groovy
pipeline {
stages {
stage('Build') {
steps {
sh 'npm ci && npm run build'
}
}
stage('Upload to OSS') {
steps {
sh """
# 上传到阿里云 OSS,文件名带 Content Hash 防缓存
aliyun oss cp dist/ oss://my-bucket/frontend/${BUILD_NUMBER}/ \
--recursive --cache-control "max-age=31536000"
"""
}
}
stage('Update Nginx & Purge CDN') {
steps {
sh """
# 更新 Nginx 配置指向新版本
kubectl set env deployment/nginx \\
STATIC_VERSION=${BUILD_NUMBER} -n prod
# 清除 CDN 缓存
aliyun cdn RefreshObjectCaches \\
--ObjectPath http://cdn.example.com/index.html
"""
}
}
}
}
案例三:合规性交付(企业级安全)
groovy
pipeline {
stages {
stage('Security Scan') {
parallel {
stage('SAST') {
steps { sh 'sonar-scanner ...' }
}
stage('Secret Detection') {
steps { sh 'detect-secrets scan | tee secrets-report.json' }
}
stage('Dependency Scan') {
steps { sh 'trivy fs --severity CRITICAL,HIGH .' }
}
stage('License Check') {
steps { sh 'license_finder' }
}
}
}
stage('Compliance Approval') {
steps {
script {
// 合规团队审批
def approval = input(
message: "安全扫描结果需合规团队确认",
submitter: 'compliance-team',
parameters: [
booleanParam(name: 'PASSED_SECURITY', defaultValue: false),
booleanParam(name: 'PASSED_LICENSE', defaultValue: false)
]
)
}
}
}
stage('Sign & Push Image') {
steps {
// Cosign 签名
sh """
cosign sign --key cosign.key ${DOCKER_IMAGE}:${TAG}
cosign verify --key cosign.pub ${DOCKER_IMAGE}:${TAG}
docker push ${DOCKER_IMAGE}:${TAG}
"""
}
}
}
}
附录:工具与模板库
A.1 完整的 Jenkinsfile 模板(Git-Driven)
完整模板已在上文第三部分提供,包含:
- 分支/Tag 智能识别
- 多环境自动路由
- SonarQube 质量门禁
- Docker 自动构建推送
- K8s 自动化部署
- 蓝绿部署策略
- 回滚机制
A.2 Webhook 转发中间件(Node.js)
用于内网 Jenkins 无法直接接收公网 Webhook 的场景:
javascript
// webhook-relay.js
const express = require('express');
const crypto = require('crypto');
const http = require('http');
const WEBHOOK_SECRET = process.env.WEBHOOK_SECRET || 'my-secret';
const JENKINS_URL = process.env.JENKINS_URL || 'http://jenkins:8080';
const app = express();
app.use(express.json());
app.post('/webhook', (req, res) => {
// 1. 验证 Secret Token
const signature = req.headers['x-hub-signature-256'];
const computed = 'sha256=' + crypto
.createHmac('sha256', WEBHOOK_SECRET)
.update(JSON.stringify(req.body))
.digest('hex');
if (signature !== computed) {
console.error('❌ Invalid webhook signature');
return res.status(403).json({ error: 'Invalid signature' });
}
// 2. 提取事件信息
const ref = req.body.ref || 'unknown';
const repo = req.body.repository?.full_name || 'unknown';
console.log(`📨 Webhook: ${repo} @ ${ref}`);
// 3. 转发到 Jenkins
const payload = JSON.stringify(req.body);
const url = new URL(`${JENKINS_URL}/generic-webhook-trigger/invoke`);
url.searchParams.set('token', 'cicd-secret-token');
const options = {
hostname: url.hostname,
port: url.port,
path: url.pathname + url.search,
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Content-Length': Buffer.byteLength(payload)
}
};
const proxyReq = http.request(options, (proxyRes) => {
console.log(`✅ Jenkins responded: ${proxyRes.statusCode}`);
});
proxyReq.write(payload);
proxyReq.end();
res.json({ status: 'forwarded' });
});
app.listen(3000, () => console.log('Webhook Relay on :3000'));
A.3 Git Pre-commit Hook 模板
bash
#!/bin/bash
# .git/hooks/pre-commit
echo "🔍 Running pre-commit checks..."
# 1. 检查是否直接推送到 main
CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD)
if [ "$CURRENT_BRANCH" = "main" ]; then
echo "❌ 禁止直接提交到 main 分支,请创建 Feature 分支并通过 PR 合并!"
exit 1
fi
# 2. 检查是否包含 TODO/FIXME(非 main 分支允许)
# if git diff --cached | grep -qE 'TODO|FIXME'; then
# echo "⚠️ 提交包含 TODO/FIXME,请确认这是有意为之"
# fi
# 3. 运行 Lint(根据项目类型选择)
if [ -f "pom.xml" ]; then
echo " Running checkstyle..."
mvn -q checkstyle:check || exit 1
elif [ -f "package.json" ]; then
echo " Running eslint..."
npx eslint --quiet . || exit 1
fi
echo "✅ Pre-commit checks passed!"
A.4 DORA 指标看板(Prometheus + Grafana)
yaml
# k8s/dora-metrics.yaml - 基于 Jenkins 构建数据
apiVersion: v1
kind: ConfigMap
metadata:
name: dora-dashboard
namespace: monitoring
data:
dora.json: |
{
"title": "DORA DevOps Metrics",
"panels": [
{
"title": "部署频率 (Deployment Frequency)",
"targets": [{
"expr": "rate(jenkins_builds_total{status='success'}[24h])"
}]
},
{
"title": "变更前置时间 (Lead Time for Changes)",
"targets": [{
"expr": "avg(jenkins_build_duration_seconds{branch='main'})"
}]
},
{
"title": "变更失败率 (Change Failure Rate)",
"targets": [{
"expr": "rate(jenkins_builds_total{status='failure'}[24h]) / rate(jenkins_builds_total[24h])"
}]
}
]
}
A.5 进阶学习路径
Git-Driven CI/CD 进阶路线
══════════════════════════
Level 1: 基础 CI/CD
├── Git Webhook + Jenkins Pipeline
├── 静态代码检查 + 单元测试
└── 分支策略 + 多环境部署
Level 2: 工程化增强
├── Docker 镜像标准化
├── SonarQube 质量门禁
├── 自动化测试分层
└── 蓝绿/金丝雀部署
Level 3: GitOps
├── Argo CD / Flux CD
├── IaC(Terraform/Pulumi)
├── 声明式配置管理
└── 增量同步 + 自动回滚
Level 4: 全栈可观测
├── OpenTelemetry 分布式追踪
├── Prometheus + Grafana 监控
├── ELK 日志聚合
└── 告警自动化 + On-call 流程
踩坑大全
坑 1:Webhook 空响应导致 503
现象 :Gitea 发送 Webhook 后返回 HTTP 503
原因 :Jenkins Generic Webhook Trigger 在触发 Pipeline 前需要完整接收 Payload,如果 Nginx 反向代理配置了 proxy_read_timeout 过短
解决 :增加 proxy_read_timeout 300s
坑 2:Docker Build 权限拒绝
现象 :Jenkins Pipeline 执行 docker build 报 permission denied
原因 :Jenkins 容器内虽然挂载了 /var/run/docker.sock,但容器内 jenkins 用户不在 docker 组
解决 :在宿主机执行 usermod -aG docker jenkins,或将 Host Docker Socket 权限设为 666(不推荐生产使用)
坑 3:SonarQube Quality Gate 超时
现象 :waitForQualityGate 一直等到超时
原因 :SonarQube Webhook 回调地址填写的是 Jenkins 内网地址,而 SonarQube 无法访问
解决:在 SonarQube 管理后台配置正确的 Webhook URL(如使用公网 IP 或内网可解析域名)
坑 4:镜像拉取超时
现象 :K3s Pod 一直处于 ImagePullBackOff
原因 :K3s 默认使用 containerd,需单独配置 registry mirror
解决:
bash
# 配置 containerd 镜像加速
cat >> /etc/rancher/k3s/registries.yaml << EOF
mirrors:
"1.94.247.115:5000":
endpoint:
- "http://1.94.247.115:5000"
"docker.io":
endpoint:
- "https://docker.1ms.run"
- "https://docker.xuanyuan.me"
EOF
systemctl restart k3s
坑 5:Jenkins 内存溢出
现象 :大项目构建时 Jenkins 容器 OOM Kill
原因 :默认 JVM 堆内存仅 512MB
解决:
bash
docker run ... -e JAVA_OPTS="-Xmx2048m -Xms1024m -Djenkins.install.runSetupWizard=false" jenkins/jenkins:lts
Git 触发器挑战
挑战 1 :实现当 commit message 包含 [hotfix] 时,跳过测试阶段直接构建并部署到紧急修复环境
挑战 2 :修改 Jenkinsfile,使 feat/* 分支推送时自动创建一个 Docker Compose 临时测试环境
挑战 3 :编写 Groovy 脚本,自动解析 Merge Request 的 Title 中的 JIRA Issue Key(如 PROJ-123),并将构建状态回写到 JIRA
挑战 4:实现基于 Git Tag 的自动发布 Changelog 生成功能,解析 commits 自动生成 Markdown 格式的 Release Notes
企业血泪教训
教训 1:某公司因未校验 Webhook Token 导致恶意构建事件
事件 :攻击者向 Jenkins Webhook 地址发送了伪造的 Payload,触发了带有 rm -rf / 命令的构建
后果 :测试环境数据全部被删
教训:
- ✅ 始终启用 Secret Token 校验
- ✅ 配置 IP 白名单(仅 Git Server IP)
- ✅ 所有敏感操作需要审批
- ✅ 构建容器使用临时卷,不挂载宿主机重要目录
教训 2:Git Tag 命名混乱导致 PROD 部署错版本
事件 :开发者打了 v1.2.3-test tag,但 Jenkins 规则 /^v.*/ 匹配后直接部署到生产环境
后果 :测试版本部署到生产,导致线上服务异常 2 小时
教训:
- ✅ Tag 规则严格:生产版本仅匹配
/^v\d+\.\d+\.\d+$/ - ✅ 生产部署始终需人工审批
- ✅ Tag 创建权限限制(仅 Release Manager)
可验证的最小 Demo
10 行代码 + 1 个 repo 跑通 Git-Driven CI/CD:
bash
# 1. Clone 示例仓库
git clone http://1.92.95.186:3000/admin/spring-boot-app.git
cd spring-boot-app
# 2. 添加 Jenkinsfile(使用本文模板)
cp /path/to/Jenkinsfile .
# 3. 提交并触发 CI/CD
git add .
git commit -m "feat: init CI/CD pipeline"
git push origin dev
# 4. 查看 Jenkins 自动构建
open http://123.249.68.214:8080/job/spring-boot-app/
# 5. 打 Tag 触发生产部署
git tag v1.0.0
git push origin v1.0.0
# 6. 验证部署结果
kubectl get pods -n prod
curl http://120.46.154.100/actuator/health
总结
本文通过华为云 ecs-6ce9 集群(4台 ECS)完整实践了 Git-Driven CI/CD 自动化交付流水线,核心成果:
| 里程碑 | 成果 |
|---|---|
| Gitea (0001) | Git 源码仓库 + Webhook 配置 |
| Jenkins (0002) | CI/CD 引擎,支持分支/Tag 智能路由 |
| SonarQube (0003) | 代码质量门禁 + Docker Registry |
| K3s (0004) | K8s 部署目标,支持蓝绿部署 |
Git-Driven 带来的变化:
- ✅ 交付周期:从「手动触发 → 30 分钟」缩短到「git push → 3 分钟」
- ✅ 部署频率:从「每周 1-2 次」提升到「每天 10+ 次」
- ✅ 回滚速度:从「手动 kubectl」提升到「Git Revert → 自动部署」
- ✅ 代码质量:SonarQube 门禁确保 0 Bug / 0 Vulnerability 才能合并
核心理念重申 :在 Git-Driven CI/CD 中,代码即流程、提交即交付 ------每次
git push都是一次潜在的发布。
七、踩坑记录:K3s NodePort 503 排查与修复
7.1 现象
http://120.46.154.100:30300/ → HTTP 503 Service Unavailable
7.2 排查过程
第一步:检查 K3s 集群状态
bash
$ kubectl get pods -A
NAMESPACE NAME READY STATUS
kube-system coredns-8db54c48d-sv59t 1/1 Running
kube-system local-path-provisioner-5d9d9885bc-nftq2 1/1 Running
kube-system metrics-server-786d997795-lvp67 1/1 Running
kube-system traefik-9bcdbbd9-sq8mc 1/1 Running
# ⚠️ 没有任何 demo-app Pod!
| 结论 | 说明 |
|---|---|
| 根本原因 | demo-app 从未被部署到 K3s 集群 |
| Service 存在吗? | 不存在,无 NodePort 30300 映射 |
| 镜像存在吗? | 不存在,containerd 中无 demo-app 镜像 |
7.3 修复步骤
Step 1:创建 demo-app 源码
demo-app/
├── src/server.js # Node.js 健康检查 + 主页 API
├── Dockerfile # node:18-alpine 多阶段构建
└── deployment.yaml # K8s Deployment + NodePort Service
Step 2:构建 Docker 镜像(K3s 节点本地)
bash
$ cd /root/demo-app
$ docker build -t demo-app:1.0.0 .
# 构建成功:镜��大小 44.9MB
Step 3:将镜像导入 K3s 的 containerd
⚠️ 关键踩坑 :K3s 使用独立的 containerd 实例 (socket:
/run/k3s/containerd/containerd.sock),与系统 Docker 安装的 containerd(socket:/run/containerd/containerd.sock)是两个不同的进程 。直接用ctr images import会导入到错误的 containerd。
| containerd 实例 | Socket 路径 | 用途 |
|---|---|---|
| 系统 containerd | /run/containerd/containerd.sock |
Docker 运行时 |
| K3s containerd | /run/k3s/containerd/containerd.sock |
K3s CRI 运行时 |
正确导入命令:
bash
$ docker save docker.io/library/demo-app:1.0.0 | \
ctr -a /run/k3s/containerd/containerd.sock -n k8s.io images import -
验证:
bash
$ crictl images | grep demo-app
docker.io/library/demo-app 1.0.0 8c25bf4c3e082 44.9MB
Step 4:部署到 K3s
yaml
# deployment.yaml - 关键配置
spec:
containers:
- name: demo-app
image: docker.io/library/demo-app:1.0.0
imagePullPolicy: Never # ← 使用本地镜像,不从 Registry 拉取
bash
$ kubectl apply -f deployment.yaml
deployment.apps/demo-app created
service/demo-app-svc created
Step 5:验证
bash
$ kubectl get pods -l app=demo-app
NAME READY STATUS RESTARTS AGE
demo-app-6b98c7bf45-bnfqr 1/1 Running 0 10s
$ curl http://localhost:30300/health
{"status":"healthy","version":"1.0.0","hostname":"demo-app-6b98c7bf45-bnfqr",...}
$ curl -o /dev/null -w "%{http_code}" http://120.46.154.100:30300/
200
7.4 踩坑总结
| 问题 | 原因 | 解决方案 |
|---|---|---|
| K3s NodePort 503 | 应用未部署 | 创建 Deployment + Service |
| 内网不通 (no route to host) | 华为云 VPC 默认不互通 | 构建本地镜像,不走 Registry |
ctr images import 无效 |
导入了系统 containerd | 指定 K3s containerd socket |
ErrImageNeverPull |
CRI 找不到 OCI index 镜像 | 使用正确 socket 重新导入 |
| heredoc 引号丢失 | SSH exec 多层转义 | Python 脚本 + SFTP 分步执行 |
| Docker daemon.json JSON 损坏 | heredoc 吞掉引号 | Python json.dumps 生成正确 JSON |
7.5 最终部署架构
┌─────────────────────────────────────────────────────────┐
│ K3s Node (120.46.154.100) │
│ │
│ ┌──────────────────┐ ┌──────────────────┐ │
│ │ demo-app Pod #1 │ │ demo-app Pod #2 │ │
│ │ 10.42.0.153:3000│ │ 10.42.0.xxx:3000│ │
│ └────────┬─────────┘ └────────┬─────────┘ │
│ │ │ │
│ └───────────┬───────────┘ │
│ │ │
│ ┌────────▼────────┐ │
│ │ demo-app-svc │ │
│ │ NodePort:30300 │ │
│ └────────┬────────┘ │
│ │ │
└───────────────────────┼─────────────────────────────────┘
│
┌─────▼─────┐
│ 浏览器 │
│ HTTP 200 │
└───────────┘
教训 :在 K3s 环境中操作 containerd 时,务必使用
ctr -a /run/k3s/containerd/containerd.sock而不是默认的系统 containerd socket。
博客完成时间:2026-06-08
最后更新:2026-06-08
实战环境:华为云 ecs-6ce9 × 4 ECS (Ubuntu 24.04, Docker 29.5.3, K3s v1.35.5)