From 13eb299316221329aeeafa32707f9a11bb7794bb Mon Sep 17 00:00:00 2001 From: wwweww <2646787260@qq.com> Date: Sun, 3 May 2026 22:41:40 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0jenkins?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- deploy/jenkins/Jenkinsfile.poll | 255 +++++++++++++++++++++++++++++ deploy/jenkins/Jenkinsfile.webhook | 164 +++++++++++++++++++ deploy/jenkins/jenkins-jobs.yaml | 55 +++++++ 3 files changed, 474 insertions(+) create mode 100644 deploy/jenkins/Jenkinsfile.poll create mode 100644 deploy/jenkins/Jenkinsfile.webhook create mode 100644 deploy/jenkins/jenkins-jobs.yaml diff --git a/deploy/jenkins/Jenkinsfile.poll b/deploy/jenkins/Jenkinsfile.poll new file mode 100644 index 0000000..72c5846 --- /dev/null +++ b/deploy/jenkins/Jenkinsfile.poll @@ -0,0 +1,255 @@ +// ============================================================ +// juwan-backend CD Pipeline — Harbor 主动轮询模式 +// 适用场景:本地开发机无公网,Jenkins 主动检测 Harbor 镜像更新 +// +// 工作原理: +// 1. Jenkins 定时(默认每 2 分钟)调用 Harbor Registry API +// 2. 对比每个镜像的最新 digest 与上次记录的 digest +// 3. 发现变化则触发对应服务的 kubectl rollout restart +// +// 前置条件(见 docs/jenkins-cd/01-local-dev-setup.md): +// - Jenkins 凭据:harbor-credentials(用户名/密码) +// - Jenkins 凭据:kubeconfig-dev(Secret file,k3s kubeconfig) +// - Jenkins 节点上已安装 kubectl 并可访问 k3s 集群 +// ============================================================ + +pipeline { + agent any + + // ── 可调参数 ────────────────────────────────────────────── + parameters { + // Harbor 地址,不含协议前缀 + string( + name: 'HARBOR_REGISTRY', + defaultValue: '103.236.53.208:4418', + description: 'Harbor 镜像仓库地址(host:port)' + ) + string( + name: 'HARBOR_PROJECT', + defaultValue: 'juwan', + description: 'Harbor 项目名' + ) + string( + name: 'K8S_NAMESPACE', + defaultValue: 'juwan', + description: 'Kubernetes 命名空间' + ) + // 轮询时只检查 latest tag + string( + name: 'IMAGE_TAG', + defaultValue: 'latest', + description: '要监听的镜像 Tag' + ) + // 逗号分隔,留空则自动扫描所有服务 + string( + name: 'TARGET_SERVICES', + defaultValue: '', + description: '指定要部署的服务(逗号分隔,如 user-api,user-rpc)。留空则部署所有有变化的服务' + ) + } + + // ── 触发器:每 2 分钟轮询一次 ──────────────────────────── + triggers { + // cron 表达式:H/2 * * * * → 每 2 分钟 + // 生产环境改用 Webhook,删除此 triggers 块即可 + cron('H/2 * * * *') + } + + environment { + // Harbor API v2 基础路径 + HARBOR_API = "http://${params.HARBOR_REGISTRY}/api/v2.0" + // digest 状态文件存放目录(Jenkins workspace 内持久化) + DIGEST_STATE_DIR = "${WORKSPACE}/.digest-state" + // kubectl 命令(节点上的实际路径) + KUBECTL = 'kubectl' + } + + stages { + + // ── Stage 1:初始化 ─────────────────────────────────── + stage('Init') { + steps { + script { + echo "=== juwan-backend CD Pipeline (Poll Mode) ===" + echo "Harbor: ${params.HARBOR_REGISTRY}/${params.HARBOR_PROJECT}" + echo "Namespace: ${params.K8S_NAMESPACE}" + echo "Tag: ${params.IMAGE_TAG}" + + // 创建 digest 状态目录(跨构建持久化) + sh "mkdir -p ${DIGEST_STATE_DIR}" + + // 构建要检查的服务列表 + if (params.TARGET_SERVICES?.trim()) { + env.SERVICE_LIST = params.TARGET_SERVICES.trim() + } else { + // 自动从 deploy/k8s/service 目录扫描所有 Deployment 名称 + // 命名规律:{service}-{api|rpc|mq} + def services = sh( + script: ''' + find deploy/k8s/service -name "*.yaml" \ + | xargs grep -l "kind: Deployment" \ + | xargs -I{} sh -c 'grep "^ name:" {} | head -1 | awk "{print \\$2}"' \ + | sort -u \ + | tr "\\n" "," + ''', + returnStdout: true + ).trim().replaceAll(/,$/, '') + env.SERVICE_LIST = services + } + echo "监控服务列表: ${env.SERVICE_LIST}" + } + } + } + + // ── Stage 2:检测镜像变化 ───────────────────────────── + stage('Detect Changes') { + steps { + script { + withCredentials([ + usernamePassword( + credentialsId: 'harbor-credentials', + usernameVariable: 'HARBOR_USER', + passwordVariable: 'HARBOR_PASS' + ) + ]) { + def serviceList = env.SERVICE_LIST.split(',').collect { it.trim() }.findAll { it } + def changedServices = [] + + serviceList.each { svc -> + if (!svc) return + + echo "检查镜像: ${svc}:${params.IMAGE_TAG}" + + // 调用 Harbor API 获取最新 digest + def digestResult = sh( + script: """ + curl -s -u "\${HARBOR_USER}:\${HARBOR_PASS}" \\ + --connect-timeout 10 \\ + --max-time 30 \\ + "${HARBOR_API}/projects/${params.HARBOR_PROJECT}/repositories/${svc}/artifacts/${params.IMAGE_TAG}" \\ + | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('digest',''))" 2>/dev/null || echo "" + """, + returnStdout: true + ).trim() + + if (!digestResult) { + echo " ⚠️ 无法获取 ${svc} 的 digest,跳过(服务可能尚未推送)" + return + } + + // 读取上次记录的 digest + def stateFile = "${DIGEST_STATE_DIR}/${svc}.digest" + def lastDigest = "" + if (fileExists(stateFile)) { + lastDigest = readFile(stateFile).trim() + } + + echo " 当前 digest: ${digestResult}" + echo " 上次 digest: ${lastDigest ?: '(首次检测)'}" + + if (digestResult != lastDigest) { + echo " ✅ 检测到变化,加入部署队列" + changedServices << svc + // 立即更新 digest 记录,防止重复触发 + writeFile file: stateFile, text: digestResult + } else { + echo " — 无变化,跳过" + } + } + + // 将变化列表传递给下一个 Stage + env.CHANGED_SERVICES = changedServices.join(',') + echo "需要更新的服务: ${env.CHANGED_SERVICES ?: '(无变化)'}" + } + } + } + } + + // ── Stage 3:部署到 k3s ─────────────────────────────── + stage('Deploy') { + when { + expression { env.CHANGED_SERVICES?.trim() } + } + steps { + script { + withCredentials([ + file(credentialsId: 'kubeconfig-dev', variable: 'KUBECONFIG_FILE') + ]) { + def changedList = env.CHANGED_SERVICES.split(',').collect { it.trim() }.findAll { it } + + changedList.each { svc -> + echo "=== 部署 ${svc} ===" + + // 验证 Deployment 是否存在 + def exists = sh( + script: """ + KUBECONFIG=\${KUBECONFIG_FILE} ${KUBECTL} get deployment ${svc} \\ + -n ${params.K8S_NAMESPACE} \\ + --ignore-not-found \\ + -o name 2>/dev/null || echo "" + """, + returnStdout: true + ).trim() + + if (!exists) { + echo " ⚠️ Deployment ${svc} 不存在于 ${params.K8S_NAMESPACE},跳过" + return + } + + // 触发滚动重启(imagePullPolicy: Always 会拉取最新 latest 镜像) + sh """ + KUBECONFIG=\${KUBECONFIG_FILE} ${KUBECTL} rollout restart deployment/${svc} \\ + -n ${params.K8S_NAMESPACE} + """ + + // 等待滚动更新完成(超时 3 分钟) + def rolloutStatus = sh( + script: """ + KUBECONFIG=\${KUBECONFIG_FILE} ${KUBECTL} rollout status deployment/${svc} \\ + -n ${params.K8S_NAMESPACE} \\ + --timeout=180s + """, + returnStatus: true + ) + + if (rolloutStatus == 0) { + echo " ✅ ${svc} 部署成功" + } else { + // 部署失败:回滚并标记失败 + echo " ❌ ${svc} 部署超时或失败,执行回滚..." + sh """ + KUBECONFIG=\${KUBECONFIG_FILE} ${KUBECTL} rollout undo deployment/${svc} \\ + -n ${params.K8S_NAMESPACE} + """ + error("${svc} 部署失败,已回滚") + } + } + } + } + } + } + + // ── Stage 4:部署摘要 ───────────────────────────────── + stage('Summary') { + steps { + script { + if (env.CHANGED_SERVICES?.trim()) { + echo "=== 本次部署完成 ===" + echo "已更新服务: ${env.CHANGED_SERVICES}" + } else { + echo "=== 无服务需要更新 ===" + } + } + } + } + } + + post { + failure { + echo "❌ Pipeline 失败,请检查上方日志" + } + success { + echo "✅ Pipeline 执行完成" + } + } +} diff --git a/deploy/jenkins/Jenkinsfile.webhook b/deploy/jenkins/Jenkinsfile.webhook new file mode 100644 index 0000000..1c74cbf --- /dev/null +++ b/deploy/jenkins/Jenkinsfile.webhook @@ -0,0 +1,164 @@ +// ============================================================ +// juwan-backend CD Pipeline — Harbor Webhook 模式 +// 适用场景:生产环境,Harbor 推送镜像后主动通知 Jenkins +// +// 工作原理: +// 1. Harbor 配置 Webhook,推送事件发送 POST 到 Jenkins +// 2. Jenkins Generic Webhook Trigger 插件解析 payload +// 3. 提取镜像名称,触发对应 Deployment 的滚动更新 +// +// 前置条件(见 docs/jenkins-cd/02-production-webhook-setup.md): +// - Jenkins 插件:Generic Webhook Trigger +// - Jenkins 凭据:kubeconfig-prod(Secret file,生产 kubeconfig) +// - Harbor Webhook 配置指向本 Jenkins URL +// ============================================================ + +pipeline { + agent any + + // ── Generic Webhook Trigger 插件配置 ───────────────────── + // Harbor push 事件 payload 结构: + // { + // "type": "PUSH_ARTIFACT", + // "event_data": { + // "resources": [{ + // "resource_url": "harbor.example.com/juwan/user-api:abc1234" + // }], + // "repository": { "name": "user-api" } + // } + // } + triggers { + GenericTrigger( + genericVariables: [ + // 提取镜像仓库名(即服务名,如 user-api) + [key: 'REPO_NAME', value: '$.event_data.repository.name'], + // 提取完整镜像 URL(含 tag) + [key: 'RESOURCE_URL', value: '$.event_data.resources[0].resource_url'], + // 提取事件类型(只处理 PUSH_ARTIFACT) + [key: 'EVENT_TYPE', value: '$.type'] + ], + // Webhook token,在 Harbor 配置 Webhook URL 时附加: + // http://jenkins.example.com/generic-webhook-trigger/invoke?token=JUWAN_CD_TOKEN + token: 'JUWAN_CD_TOKEN', + // 只处理推送事件 + regexpFilterText: '$EVENT_TYPE', + regexpFilterExpression: 'PUSH_ARTIFACT', + causeString: 'Harbor push: $REPO_NAME ($RESOURCE_URL)', + printContributedVariables: true, + printPostContent: true + ) + } + + parameters { + string( + name: 'HARBOR_REGISTRY', + defaultValue: 'harbor.example.com', + description: 'Harbor 镜像仓库地址' + ) + string( + name: 'HARBOR_PROJECT', + defaultValue: 'juwan', + description: 'Harbor 项目名' + ) + string( + name: 'K8S_NAMESPACE', + defaultValue: 'juwan', + description: 'Kubernetes 命名空间' + ) + string( + name: 'KUBECONFIG_CREDENTIAL', + defaultValue: 'kubeconfig-prod', + description: 'Jenkins 中存储生产 kubeconfig 的凭据 ID' + ) + } + + environment { + KUBECTL = 'kubectl' + } + + stages { + + stage('Validate Trigger') { + steps { + script { + echo "=== Harbor Webhook 触发 ===" + echo "事件类型: ${env.EVENT_TYPE}" + echo "仓库名称: ${env.REPO_NAME}" + echo "镜像 URL: ${env.RESOURCE_URL}" + + if (!env.REPO_NAME?.trim()) { + error("Webhook payload 中未找到 repository.name,请检查 Harbor Webhook 配置") + } + } + } + } + + stage('Deploy') { + steps { + script { + def svc = env.REPO_NAME.trim() + + withCredentials([ + file(credentialsId: params.KUBECONFIG_CREDENTIAL, variable: 'KUBECONFIG_FILE') + ]) { + // 验证 Deployment 存在 + def exists = sh( + script: """ + KUBECONFIG=\${KUBECONFIG_FILE} ${KUBECTL} get deployment ${svc} \\ + -n ${params.K8S_NAMESPACE} \\ + --ignore-not-found \\ + -o name 2>/dev/null || echo "" + """, + returnStdout: true + ).trim() + + if (!exists) { + error("Deployment ${svc} 不存在于命名空间 ${params.K8S_NAMESPACE}") + } + + echo "触发滚动更新: ${svc}" + + // 用 image tag 中的 digest/sha 更新镜像,确保精确版本 + // resource_url 格式: harbor.example.com/juwan/user-api:abc1234 + def imageRef = env.RESOURCE_URL.trim() + + sh """ + KUBECONFIG=\${KUBECONFIG_FILE} ${KUBECTL} set image deployment/${svc} \\ + ${svc}=${imageRef} \\ + -n ${params.K8S_NAMESPACE} + """ + + def rolloutStatus = sh( + script: """ + KUBECONFIG=\${KUBECONFIG_FILE} ${KUBECTL} rollout status deployment/${svc} \\ + -n ${params.K8S_NAMESPACE} \\ + --timeout=300s + """, + returnStatus: true + ) + + if (rolloutStatus == 0) { + echo "✅ ${svc} 部署成功 → ${imageRef}" + } else { + echo "❌ ${svc} 部署失败,执行回滚..." + sh """ + KUBECONFIG=\${KUBECONFIG_FILE} ${KUBECTL} rollout undo deployment/${svc} \\ + -n ${params.K8S_NAMESPACE} + """ + error("${svc} 部署失败,已回滚") + } + } + } + } + } + } + + post { + success { + echo "✅ ${env.REPO_NAME} 部署完成" + } + failure { + echo "❌ ${env.REPO_NAME} 部署失败,请检查日志" + } + } +} diff --git a/deploy/jenkins/jenkins-jobs.yaml b/deploy/jenkins/jenkins-jobs.yaml new file mode 100644 index 0000000..26a2b6c --- /dev/null +++ b/deploy/jenkins/jenkins-jobs.yaml @@ -0,0 +1,55 @@ +# Jenkins Job 配置说明 +# 本文件描述如何在 Jenkins 中创建对应的 Pipeline Job +# 详细步骤见 docs/jenkins-cd/01-local-dev-setup.md + +# ── 轮询模式 Job(本地开发)──────────────────────────────── +poll_job: + name: "juwan-backend-cd-poll" + type: Pipeline + pipeline_script_from_scm: true + scm: + type: Git + # 填写你的 Gitea 仓库地址 + url: "http://103.236.53.208:3000/YOUR_ORG/juwan-backend.git" + branch: "*/main" + credentials_id: "gitea-credentials" + script_path: "deploy/jenkins/Jenkinsfile.poll" + # 不勾选 SCM 轮询(触发器在 Jenkinsfile 内用 cron 定义) + build_triggers: + poll_scm: false + +# ── Webhook 模式 Job(生产环境)────────────────────────────── +webhook_job: + name: "juwan-backend-cd-webhook" + type: Pipeline + pipeline_script_from_scm: true + scm: + type: Git + url: "https://YOUR_PROD_GIT_REPO/juwan-backend.git" + branch: "*/main" + credentials_id: "gitea-credentials-prod" + script_path: "deploy/jenkins/Jenkinsfile.webhook" + build_triggers: + generic_webhook_trigger: true + # token 与 Jenkinsfile.webhook 中的 token 字段一致 + token: "JUWAN_CD_TOKEN" + +# ── 所需 Jenkins 凭据 ───────────────────────────────────── +credentials: + - id: "harbor-credentials" + type: "Username with password" + description: "Harbor 镜像仓库登录凭据" + # 在 Jenkins → Manage Credentials 中创建 + + - id: "kubeconfig-dev" + type: "Secret file" + description: "k3s 开发集群 kubeconfig 文件" + # 文件内容:cat ~/.kube/config(或 /etc/rancher/k3s/k3s.yaml) + + - id: "kubeconfig-prod" + type: "Secret file" + description: "生产 Kubernetes 集群 kubeconfig 文件" + + - id: "gitea-credentials" + type: "Username with password" + description: "Gitea 仓库访问凭据(用于 SCM checkout)"