基于 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)


目录


实战环境总览

本次实战基于华为云 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 事件 触发动作 适用场景 是否需审批
pushdev 构建 + 单元测试 + 静态扫描 日常开发集成 ❌ 自动
pushmain 构建 + 全量测试 + 部署到 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 buildpermission 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 / 命令的构建

后果 :测试环境数据全部被删

教训

  1. ✅ 始终启用 Secret Token 校验
  2. ✅ 配置 IP 白名单(仅 Git Server IP)
  3. ✅ 所有敏感操作需要审批
  4. ✅ 构建容器使用临时卷,不挂载宿主机重要目录

教训 2:Git Tag 命名混乱导致 PROD 部署错版本

事件 :开发者打了 v1.2.3-test tag,但 Jenkins 规则 /^v.*/ 匹配后直接部署到生产环境

后果 :测试版本部署到生产,导致线上服务异常 2 小时

教训

  1. ✅ Tag 规则严格:生产版本仅匹配 /^v\d+\.\d+\.\d+$/
  2. ✅ 生产部署始终需人工审批
  3. ✅ 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)

相关推荐
夜雪闻竹4 小时前
版本管理:npm 发布 + Electron 打包 + CI/CD
ci/cd·npm·node.js·代码规范·chatcrystal
Dontla4 小时前
.gitkeep文件作用(让Git追踪空目录,使该目录能被纳入版本控制)!.gitkeep
大数据·git·elasticsearch
shandianchengzi4 小时前
【记录】VSCode|Windows 下 VS Code 配置 Git Bash 为默认终端完整教程
windows·git·vscode·bash
EleganceJiaBao4 小时前
【Git】现代开发工作流(Main + Feature Branch)
git·github
小怪不太怪~4 小时前
本地项目上传到GitHub--小怪教程(Git Bash实操+常见报错解决
git·github·bash
_codemonster14 小时前
git 容易混淆的点
git
_codemonster18 小时前
Git 最常用操作和原理
大数据·git·elasticsearch
_codemonster1 天前
.git文件夹里所有文件详解
git
01杭呐1 天前
一次错误分支合并导致 `master` 变脏的排查与修复
git