欢迎光临
我们一直在努力

基于 Git 的自动集成交付(Git-Driven CI/CD)实战

基于 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 事件监听与触发机制
  • 第三部分:自动化流水线设计
  • 第四部分:自动化测试与质量门禁
  • 第五部分:自动化交付与部署(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

# 在 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

验证:

$ 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

# 创建管理员账户
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

# 通过 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 关键字段:

{
"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 配置:

// 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 快速配置示例:

# 在 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 项目)

# 安装 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)

{
"lint-staged": {
"*.{js,ts,tsx}": ["eslint –fix", "prettier –write"],
"*.{java}": ["mvn spotless:apply"],
"*.{py}": ["black –check", "flake8"]
}
}

企业级:禁止直接 push 到 main

# 在 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 实现无人值守初始化:

# 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 事件自动选择执行路径:

// 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 中实现:

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 关键字控制

// 解析 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):

#!/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)

# 在 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:

// 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:

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 集群(单节点):

# 使用 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:

# k8s/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: springbootapp
labels:
app: springbootapp
spec:
replicas: 2
selector:
matchLabels:
app: springbootapp
template:
metadata:
labels:
app: springbootapp
version: "${IMAGE_TAG}"
spec:
containers:
name: springbootapp
image: 1.94.247.115:5000/springbootapp:${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: springbootappsvc
spec:
selector:
app: springbootapp
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 蓝绿部署脚本:

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 回滚机制:一键恢复

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 实现:

// 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

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
"""
}
}
}
}

案例三:合规性交付(企业级安全)

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 的场景:

// 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 模板

#!/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)

# k8s/dora-metrics.yaml – 基于 Jenkins 构建数据
apiVersion: v1
kind: ConfigMap
metadata:
name: doradashboard
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 解决:

# 配置 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 解决:

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:

    # 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 集群状态

    $ 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 节点本地)

    $ 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 运行时

    正确导入命令:

    $ docker save docker.io/library/demo-app:1.0.0 | \\
    ctr -a /run/k3s/containerd/containerd.sock -n k8s.io images import

    验证:

    $ crictl images | grep demo-app
    docker.io/library/demo-app 1.0.0 8c25bf4c3e082 44.9MB

    Step 4:部署到 K3s

    # deployment.yaml – 关键配置
    spec:
    containers:
    name: demoapp
    image: docker.io/library/demoapp:1.0.0
    imagePullPolicy: Never # ← 使用本地镜像,不从 Registry 拉取

    $ kubectl apply -f deployment.yaml
    deployment.apps/demo-app created
    service/demo-app-svc created

    Step 5:验证

    $ 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)

    赞(0)
    未经允许不得转载:171主机测评 » 基于 Git 的自动集成交付(Git-Driven CI/CD)实战
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址