From 6ec07aa449ceadf6b58d187417d5f264117d137a Mon Sep 17 00:00:00 2001 From: zk Date: Tue, 26 May 2026 21:23:31 +0800 Subject: [PATCH] =?UTF-8?q?=E9=87=8D=E6=96=B0=E6=9E=84=E5=BB=BA=E8=BF=90?= =?UTF-8?q?=E7=BB=B4=E6=B5=81=E7=A8=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client-api/Jenkinsfile | 156 +++++++++++++++++++++++---------- client-api/nginx.conf | 23 ++++- client-api/offerpai.com.cn.crt | 49 +++++++++++ client-api/offerpai.com.cn.key | 5 ++ docker-compose.client-api.yml | 18 +--- 5 files changed, 192 insertions(+), 59 deletions(-) create mode 100644 client-api/offerpai.com.cn.crt create mode 100644 client-api/offerpai.com.cn.key diff --git a/client-api/Jenkinsfile b/client-api/Jenkinsfile index 684eae5..f92a571 100644 --- a/client-api/Jenkinsfile +++ b/client-api/Jenkinsfile @@ -1,29 +1,39 @@ /** * OfferPie Backend Client-API 蓝绿部署流水线 * - * 工作目录说明: - * - Jenkins 会自动为每个项目创建独立的工作空间:/var/jenkins_home/workspace/<项目名>/ - * - docker-compose 文件在项目根目录,通过 -f 指定文件名 - * - Dockerfile 在 client-api/ 子目录下,build context 为项目根目录(因为需要编译 common 和 manager 模块) - * - nginx.conf 在 client-api/ 下,首次部署时 docker cp 进 nginx 容器 + * 部署架构: + * - Jenkins Master 通过 SSH(sshpass) 远程控制目标 Ubuntu 服务器 + * - 目标服务器:Docker Nginx (443/80 SSL + 蓝绿切换) → blue/green 容器 + * - 目标服务器可以是全新裸机,流水线自动完成环境准备 + * + * 前置条件: + * - Jenkins Master 已安装 sshpass */ pipeline { agent any parameters { choice(name: 'BRANCH', choices: ['master', 'pre', 'dev', 'test'], description: '选择要部署的分支') + string(name: 'DEPLOY_HOST', defaultValue: '8.138.12.104', description: '目标服务器 IP') + string(name: 'DEPLOY_PORT', defaultValue: '22', description: 'SSH 端口') + string(name: 'DEPLOY_USER', defaultValue: 'root', description: 'SSH 用户名') + password(name: 'DEPLOY_PASSWORD', defaultValue: 'Mzpy520@126.com', description: 'SSH 密码') } environment { - COMPOSE_FILE = 'docker-compose.client-api.yml' // docker-compose 文件名 - CONTAINER_PREFIX = 'offerpie-backend-client' // 容器名前缀,拼接 -blue / -green / -nginx - HEALTH_URL = 'http://localhost:8080/api/public/actuator/health' // 应用健康检查地址 + COMPOSE_FILE = 'docker-compose.client-api.yml' + CONTAINER_PREFIX = 'offerpie-backend-client' + HEALTH_URL = 'http://localhost:8080/api/public/actuator/health' + REMOTE_WORK_DIR = '/opt/offerpie/backend' + SSH_CMD = "sshpass -p '${params.DEPLOY_PASSWORD}' ssh -o StrictHostKeyChecking=no -p ${params.DEPLOY_PORT} ${params.DEPLOY_USER}@${params.DEPLOY_HOST}" + SCP_CMD = "sshpass -p '${params.DEPLOY_PASSWORD}' scp -o StrictHostKeyChecking=no -P ${params.DEPLOY_PORT}" } stages { stage('开始提示') { steps { echo "OfferPie Backend Client-API 开始构建" + echo "目标服务器: ${params.DEPLOY_HOST}:${params.DEPLOY_PORT}" } } @@ -36,18 +46,84 @@ pipeline { } } + /** + * 环境准备 + * - 检查/安装 Docker + * - 检查/安装 docker-compose + * - 创建必要目录 + */ + stage('环境准备') { + steps { + script { + // 检查/安装 Docker + sh """ + ${SSH_CMD} ' + if ! command -v docker &> /dev/null; then + echo "Docker 未安装,开始安装..." + apt-get update + apt-get install -y docker.io + systemctl start docker + systemctl enable docker + else + echo "Docker 已安装" + fi + ' + """ + + // 检查/安装 docker-compose + sh """ + ${SSH_CMD} ' + if ! command -v docker-compose &> /dev/null; then + echo "docker-compose 未安装,开始安装..." + curl -L "https://github.com/docker/compose/releases/latest/download/docker-compose-linux-x86_64" -o /usr/local/bin/docker-compose + chmod +x /usr/local/bin/docker-compose + else + echo "docker-compose 已安装" + fi + ' + """ + + // 创建必要目录 + sh """ + ${SSH_CMD} ' + mkdir -p /etc/ssl/offerpai + mkdir -p /logs/offerpie-backend-client + mkdir -p ${REMOTE_WORK_DIR} + ' + """ + } + } + } + + /** + * 文件传输 + * - scp 项目源码到目标服务器 + * - scp SSL 证书到目标服务器 + */ + stage('文件传输') { + steps { + script { + // 传输项目源码 + sh "${SCP_CMD} -r . ${params.DEPLOY_USER}@${params.DEPLOY_HOST}:${REMOTE_WORK_DIR}/" + + // 传输 SSL 证书 + sh "${SCP_CMD} client-api/offerpai.com.cn.crt ${params.DEPLOY_USER}@${params.DEPLOY_HOST}:/etc/ssl/offerpai/" + sh "${SCP_CMD} client-api/offerpai.com.cn.key ${params.DEPLOY_USER}@${params.DEPLOY_HOST}:/etc/ssl/offerpai/" + } + } + } + /** * 检测部署目标 - * - 检查 blue 和 green 容器的运行状态 - * - blue 在运行 → 部署 green;green 在运行 → 部署 blue - * - 都不在或都在 → 默认部署 blue + * - blue 在运行 → 部署 green + * - green 在运行 → 部署 blue + * - 都不在 → 默认部署 blue */ stage('检测部署目标') { steps { - echo "检查当前容器状态" script { - def blueRunning = sh(script: "docker ps -q -f name=${CONTAINER_PREFIX}-blue", returnStdout: true).trim() - def greenRunning = sh(script: "docker ps -q -f name=${CONTAINER_PREFIX}-green", returnStdout: true).trim() + def blueRunning = sh(script: "${SSH_CMD} 'docker ps -q -f name=${CONTAINER_PREFIX}-blue'", returnStdout: true).trim() + def greenRunning = sh(script: "${SSH_CMD} 'docker ps -q -f name=${CONTAINER_PREFIX}-green'", returnStdout: true).trim() env.DEPLOY_TARGET = '' @@ -70,44 +146,42 @@ pipeline { /** * 构建新版本 - * - 先清理目标颜色的旧容器(如果残留) + * - 清理目标颜色的旧容器(如果残留) * - docker-compose build 编译新镜像 */ stage('构建新版本') { steps { script { - def existingContainer = sh(script: "docker ps -aq -f name=${CONTAINER_PREFIX}-${env.DEPLOY_TARGET}", returnStdout: true).trim() + def existingContainer = sh(script: "${SSH_CMD} 'docker ps -aq -f name=${CONTAINER_PREFIX}-${env.DEPLOY_TARGET}'", returnStdout: true).trim() if (existingContainer) { echo "清理已存在的容器: ${CONTAINER_PREFIX}-${env.DEPLOY_TARGET}" - sh "docker rm -f ${CONTAINER_PREFIX}-${env.DEPLOY_TARGET}" + sh "${SSH_CMD} 'docker rm -f ${CONTAINER_PREFIX}-${env.DEPLOY_TARGET}'" } - sh "docker-compose -f ${COMPOSE_FILE} build ${env.DEPLOY_TARGET}" + sh "${SSH_CMD} 'cd ${REMOTE_WORK_DIR} && docker-compose -f ${COMPOSE_FILE} build ${env.DEPLOY_TARGET}'" } } } /** * 检查 Nginx - * - 检查 nginx 容器是否存在 - * - 不存在则启动并复制配置文件(首次部署) - * - 存在则确保容器运行中 + * - 不存在:启动 → docker cp 配置 → reload + * - 已存在:确保运行中 */ stage('检查Nginx') { steps { script { - def nginxExists = sh(script: "docker ps -aq -f name=${CONTAINER_PREFIX}-nginx", returnStdout: true).trim() + def nginxExists = sh(script: "${SSH_CMD} 'docker ps -aq -f name=${CONTAINER_PREFIX}-nginx'", returnStdout: true).trim() if (!nginxExists) { echo "首次部署,初始化 Nginx 容器" - sh "docker-compose -f ${COMPOSE_FILE} up -d nginx" + sh "${SSH_CMD} 'cd ${REMOTE_WORK_DIR} && docker-compose -f ${COMPOSE_FILE} up -d nginx'" sh "sleep 3" - // 复制配置文件到容器内 - sh "docker cp client-api/nginx.conf ${CONTAINER_PREFIX}-nginx:/etc/nginx/nginx.conf" - sh "docker exec ${CONTAINER_PREFIX}-nginx nginx -s reload" + sh "${SSH_CMD} 'docker cp ${REMOTE_WORK_DIR}/client-api/nginx.conf ${CONTAINER_PREFIX}-nginx:/etc/nginx/nginx.conf'" + sh "${SSH_CMD} 'docker exec ${CONTAINER_PREFIX}-nginx nginx -s reload'" } else { - def nginxRunning = sh(script: "docker ps -q -f name=${CONTAINER_PREFIX}-nginx", returnStdout: true).trim() + def nginxRunning = sh(script: "${SSH_CMD} 'docker ps -q -f name=${CONTAINER_PREFIX}-nginx'", returnStdout: true).trim() if (!nginxRunning) { echo "Nginx 容器已停止,重新启动" - sh "docker start ${CONTAINER_PREFIX}-nginx" + sh "${SSH_CMD} 'docker start ${CONTAINER_PREFIX}-nginx'" sh "sleep 2" } echo "Nginx 容器已存在且运行中" @@ -119,8 +193,7 @@ pipeline { stage('启动新版本') { steps { script { - sh "docker-compose -f ${COMPOSE_FILE} up -d ${env.DEPLOY_TARGET}" - // 等待 Spring Boot 应用完全启动 + sh "${SSH_CMD} 'cd ${REMOTE_WORK_DIR} && docker-compose -f ${COMPOSE_FILE} up -d ${env.DEPLOY_TARGET}'" sh 'sleep 35' } } @@ -128,10 +201,8 @@ pipeline { /** * 健康检查 - * - 进入容器执行 curl 检测应用是否正常响应 - * - curl -f 参数:HTTP 错误码(404、500等)时返回失败 + * - 容器内 curl 检测应用是否正常 * - 最多重试 3 次,每次间隔 5 秒 - * - 全部失败则终止部署流程 */ stage('健康检查') { steps { @@ -142,7 +213,7 @@ pipeline { while (retryCount < maxRetries && !healthy) { try { - sh "docker exec ${CONTAINER_PREFIX}-${env.DEPLOY_TARGET} curl -f ${HEALTH_URL}" + sh "${SSH_CMD} 'docker exec ${CONTAINER_PREFIX}-${env.DEPLOY_TARGET} curl -f ${HEALTH_URL}'" healthy = true } catch (Exception e) { retryCount++ @@ -163,31 +234,28 @@ pipeline { /** * 切换流量 - * - 修改 nginx 配置中的 proxy_pass 指向新版本容器 - * - nginx 不挂载宿主机文件,直接 sed -i 修改容器内配置 - * - reload 使新配置生效,实现零停机切换 + * - sed 修改 nginx proxy_pass 指向新色 + * - reload 生效 */ stage('切换流量') { steps { script { - sh "docker exec ${CONTAINER_PREFIX}-nginx sed -i 's/proxy_pass http:\\/\\/\\(blue\\|green\\):8080;/proxy_pass http:\\/\\/${env.DEPLOY_TARGET}:8080;/' /etc/nginx/nginx.conf" - sh "docker exec ${CONTAINER_PREFIX}-nginx nginx -s reload" + sh "${SSH_CMD} \"docker exec ${CONTAINER_PREFIX}-nginx sed -i 's/proxy_pass http:\\/\\/\\(blue\\|green\\):8080;/proxy_pass http:\\/\\/${env.DEPLOY_TARGET}:8080;/' /etc/nginx/nginx.conf\"" + sh "${SSH_CMD} 'docker exec ${CONTAINER_PREFIX}-nginx nginx -s reload'" echo "✅ 流量已切换到 ${env.DEPLOY_TARGET}" } } } /** - * 停止并删除旧版本 - * - 不能用 docker-compose down,会删除所有服务(包括 nginx) - * - 用 docker rm -f 只删除指定的旧颜色容器 + * 删除旧版本 */ stage('删除旧版本') { steps { script { - def otherExists = sh(script: "docker ps -aq -f name=${CONTAINER_PREFIX}-${env.OTHER_TARGET}", returnStdout: true).trim() + def otherExists = sh(script: "${SSH_CMD} 'docker ps -aq -f name=${CONTAINER_PREFIX}-${env.OTHER_TARGET}'", returnStdout: true).trim() if (otherExists) { - sh "docker rm -f ${CONTAINER_PREFIX}-${env.OTHER_TARGET}" + sh "${SSH_CMD} 'docker rm -f ${CONTAINER_PREFIX}-${env.OTHER_TARGET}'" echo "旧版本 ${env.OTHER_TARGET} 已删除" } } diff --git a/client-api/nginx.conf b/client-api/nginx.conf index 28cf8a6..8e520dd 100644 --- a/client-api/nginx.conf +++ b/client-api/nginx.conf @@ -23,9 +23,10 @@ http { client_max_body_size 20m; + # HTTP → HTTPS 跳转 server { listen 80; - server_name _; + server_name api.offerpai.com.cn; # Nginx 自身健康检查 location /health { @@ -34,6 +35,26 @@ http { add_header Content-Type text/plain; } + location / { + return 301 https://$host$request_uri; + } + } + + # HTTPS + 蓝绿切换 + server { + listen 443 ssl; + http2 on; + server_name api.offerpai.com.cn; + + ssl_certificate /etc/nginx/ssl/offerpai.com.cn.crt; + ssl_certificate_key /etc/nginx/ssl/offerpai.com.cn.key; + + ssl_protocols TLSv1.2 TLSv1.3; + ssl_ciphers HIGH:!aNULL:!MD5; + ssl_prefer_server_ciphers on; + ssl_session_cache shared:SSL:10m; + ssl_session_timeout 10m; + # 默认代理到 blue,部署时通过 sed 切换 location / { proxy_pass http://blue:8080; diff --git a/client-api/offerpai.com.cn.crt b/client-api/offerpai.com.cn.crt new file mode 100644 index 0000000..e348193 --- /dev/null +++ b/client-api/offerpai.com.cn.crt @@ -0,0 +1,49 @@ +-----BEGIN CERTIFICATE----- +MIIDnTCCAyKgAwIBAgISBVWuRhhOaiBkuQnFxKyCJDMFMAoGCCqGSM49BAMDMDIx +CzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1MZXQncyBFbmNyeXB0MQswCQYDVQQDEwJF +NzAeFw0yNjA1MjIxMDI2MjRaFw0yNjA4MjAxMDI2MjNaMBoxGDAWBgNVBAMTD29m +ZmVycGFpLmNvbS5jbjBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABFf/rMowrqMf ++sD6s2QBnj3uT2CjxxjQkVnuKXGlV8QCQvLhCNiPqyVaXhwQRnW4/A4DDygQ0SjG +7S3qKPBrCXCjggIuMIICKjAOBgNVHQ8BAf8EBAMCB4AwEwYDVR0lBAwwCgYIKwYB +BQUHAwEwDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQUMUW5y29bkHDb++PS0DfpFe/k +ubYwHwYDVR0jBBgwFoAUrkie3IcdRKBv2qLlYHQEeMKcAIAwMgYIKwYBBQUHAQEE +JjAkMCIGCCsGAQUFBzAChhZodHRwOi8vZTcuaS5sZW5jci5vcmcvMC0GA1UdEQQm +MCSCESoub2ZmZXJwYWkuY29tLmNugg9vZmZlcnBhaS5jb20uY24wEwYDVR0gBAww +CjAIBgZngQwBAgEwLAYDVR0fBCUwIzAhoB+gHYYbaHR0cDovL2U3LmMubGVuY3Iu +b3JnLzYuY3JsMIIBDQYKKwYBBAHWeQIEAgSB/gSB+wD5AHYAwjF+V0UZo0Xufzje +spBB68fCIVoiv3/Vta12mtkOUs0AAAGeT25fsAAABAMARzBFAiEA+wB1nILWmjta +TYVKK0O3dy/XYw8yN9tAHU2m48ILxFgCIEhNQOZOSQcJckwuPUkAnz+3EwCo95d3 +8KR2hjCv7KkkAH8AqCbL4wrGNRJGUz/gZfFPGdluGQgTxB3ZbXkAsxI8VScAAAGe +T25jRQAIAAAFAAwCz/kEAwBIMEYCIQDhvNquPNk6cnZru4zQeX9fd9JAqPbVk76l +mLrC5QWiRAIhAMf1zNpuUAg9sz8Ft0szAbdmGwT7B7nBftIVyU2LseO6MAoGCCqG +SM49BAMDA2kAMGYCMQC8wvsgPm4l+i1nGGfNph7QVnu1hc4giTqHco5dp+sLr9Qi +E0Cm013ub291oy92sYECMQCMshoYY/LnubmjMxnkAIV9/WbhZ2FJCtWmKWSNdGnU +l+l/l6tgc1a1iHB8E0lr31I= +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIEVzCCAj+gAwIBAgIRAKp18eYrjwoiCWbTi7/UuqEwDQYJKoZIhvcNAQELBQAw +TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh +cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMjQwMzEzMDAwMDAw +WhcNMjcwMzEyMjM1OTU5WjAyMQswCQYDVQQGEwJVUzEWMBQGA1UEChMNTGV0J3Mg +RW5jcnlwdDELMAkGA1UEAxMCRTcwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAARB6AST +CFh/vjcwDMCgQer+VtqEkz7JANurZxLP+U9TCeioL6sp5Z8VRvRbYk4P1INBmbef +QHJFHCxcSjKmwtvGBWpl/9ra8HW0QDsUaJW2qOJqceJ0ZVFT3hbUHifBM/2jgfgw +gfUwDgYDVR0PAQH/BAQDAgGGMB0GA1UdJQQWMBQGCCsGAQUFBwMCBggrBgEFBQcD +ATASBgNVHRMBAf8ECDAGAQH/AgEAMB0GA1UdDgQWBBSuSJ7chx1EoG/aouVgdAR4 +wpwAgDAfBgNVHSMEGDAWgBR5tFnme7bl5AFzgAiIyBpY9umbbjAyBggrBgEFBQcB +AQQmMCQwIgYIKwYBBQUHMAKGFmh0dHA6Ly94MS5pLmxlbmNyLm9yZy8wEwYDVR0g +BAwwCjAIBgZngQwBAgEwJwYDVR0fBCAwHjAcoBqgGIYWaHR0cDovL3gxLmMubGVu +Y3Iub3JnLzANBgkqhkiG9w0BAQsFAAOCAgEAjx66fDdLk5ywFn3CzA1w1qfylHUD +aEf0QZpXcJseddJGSfbUUOvbNR9N/QQ16K1lXl4VFyhmGXDT5Kdfcr0RvIIVrNxF +h4lqHtRRCP6RBRstqbZ2zURgqakn/Xip0iaQL0IdfHBZr396FgknniRYFckKORPG +yM3QKnd66gtMst8I5nkRQlAg/Jb+Gc3egIvuGKWboE1G89NTsN9LTDD3PLj0dUMr +OIuqVjLB8pEC6yk9enrlrqjXQgkLEYhXzq7dLafv5Vkig6Gl0nuuqjqfp0Q1bi1o +yVNAlXe6aUXw92CcghC9bNsKEO1+M52YY5+ofIXlS/SEQbvVYYBLZ5yeiglV6t3S +M6H+vTG0aP9YHzLn/KVOHzGQfXDP7qM5tkf+7diZe7o2fw6O7IvN6fsQXEQQj8TJ +UXJxv2/uJhcuy/tSDgXwHM8Uk34WNbRT7zGTGkQRX0gsbjAea/jYAoWv0ZvQRwpq +Pe79D/i7Cep8qWnA+7AE/3B3S/3dEEYmc0lpe1366A/6GEgk3ktr9PEoQrLChs6I +tu3wnNLB2euC8IKGLQFpGtOO/2/hiAKjyajaBP25w1jF0Wl8Bbqne3uZ2q1GyPFJ +YRmT7/OXpmOH/FVLtwS+8ng1cAmpCujPwteJZNcDG0sF2n/sc0+SQf49fdyUK0ty ++VUwFj9tmWxyR/M= +-----END CERTIFICATE----- diff --git a/client-api/offerpai.com.cn.key b/client-api/offerpai.com.cn.key new file mode 100644 index 0000000..06d5019 --- /dev/null +++ b/client-api/offerpai.com.cn.key @@ -0,0 +1,5 @@ +-----BEGIN EC PRIVATE KEY----- +MHcCAQEEIAj3lWeQ/L+GVhLzcfgJe6oak4e3L3y1sSowG5k3HGvUoAoGCCqGSM49 +AwEHoUQDQgAEV/+syjCuox/6wPqzZAGePe5PYKPHGNCRWe4pcaVXxAJC8uEI2I+r +JVpeHBBGdbj8DgMPKBDRKMbtLeoo8GsJcA== +-----END EC PRIVATE KEY----- diff --git a/docker-compose.client-api.yml b/docker-compose.client-api.yml index c7ae5bd..ee3e67a 100644 --- a/docker-compose.client-api.yml +++ b/docker-compose.client-api.yml @@ -3,13 +3,11 @@ services: image: nginx:alpine container_name: offerpie-backend-client-nginx restart: unless-stopped - networks: - - xxl-job-network ports: - - "10202:80" - depends_on: - - blue - - green + - "443:443" + - "80:80" + volumes: + - /etc/ssl/offerpai:/etc/nginx/ssl:ro healthcheck: test: ["CMD", "wget", "--spider", "-q", "http://localhost/health"] interval: 30s @@ -27,8 +25,6 @@ services: dockerfile: client-api/Dockerfile container_name: offerpie-backend-client-blue restart: unless-stopped - networks: - - xxl-job-network expose: - "8080" environment: @@ -55,8 +51,6 @@ services: dockerfile: client-api/Dockerfile container_name: offerpie-backend-client-green restart: unless-stopped - networks: - - xxl-job-network expose: - "8080" environment: @@ -76,7 +70,3 @@ services: limits: memory: 2G cpus: '2' - -networks: - xxl-job-network: - external: true