/** * OfferPie Backend Client-API 蓝绿部署流水线 * * 部署架构: * - 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' 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}" } } stage('拉取代码') { steps { echo "拉取 ${params.BRANCH} 分支代码" git branch: "${params.BRANCH}", credentialsId: 'gitea-fab089c1-b55d-4b58-9fad', url: 'http://git.jianshixingqiu.com/offerpai/offerpai_backend.git' } } /** * 环境准备 * - 检查/安装 Docker * - 检查/安装 docker-compose * - 创建必要目录 */ stage('环境准备') { steps { script { // 确保 Jenkins 容器内有 sshpass sh "which sshpass || (apt-get update && apt-get install -y sshpass)" // 检查/安装 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 * - green 在运行 → 部署 blue * - 都不在 → 默认部署 blue */ stage('检测部署目标') { steps { script { 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 = '' if (blueRunning && !greenRunning) { env.DEPLOY_TARGET = 'green' } if (greenRunning && !blueRunning) { env.DEPLOY_TARGET = 'blue' } if (!env.DEPLOY_TARGET) { echo "当前环境未部署服务或状态异常,默认部署 blue" env.DEPLOY_TARGET = 'blue' } env.OTHER_TARGET = (env.DEPLOY_TARGET == 'blue') ? 'green' : 'blue' echo "当前激活: ${env.OTHER_TARGET},即将部署: ${env.DEPLOY_TARGET}" } } } /** * 构建新版本 * - 清理目标颜色的旧容器(如果残留) * - docker-compose build 编译新镜像 */ stage('构建新版本') { steps { script { 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 "${SSH_CMD} 'docker rm -f ${CONTAINER_PREFIX}-${env.DEPLOY_TARGET}'" } sh "${SSH_CMD} 'cd ${REMOTE_WORK_DIR} && docker-compose -f ${COMPOSE_FILE} build ${env.DEPLOY_TARGET}'" } } } stage('启动新版本') { steps { script { sh "${SSH_CMD} 'cd ${REMOTE_WORK_DIR} && docker-compose -f ${COMPOSE_FILE} up -d ${env.DEPLOY_TARGET}'" sh 'sleep 35' } } } /** * 健康检查 * - 容器内 curl 检测应用是否正常 * - 最多重试 3 次,每次间隔 5 秒 */ stage('健康检查') { steps { script { def maxRetries = 3 def retryCount = 0 def healthy = false while (retryCount < maxRetries && !healthy) { try { sh "${SSH_CMD} 'docker exec ${CONTAINER_PREFIX}-${env.DEPLOY_TARGET} curl -f ${HEALTH_URL}'" healthy = true } catch (Exception e) { retryCount++ if (retryCount < maxRetries) { echo "健康检查失败,5秒后重试 (${retryCount}/${maxRetries})" sleep 5 } } } if (!healthy) { error "健康检查失败,部署中止" } echo "✅ ${CONTAINER_PREFIX}-${env.DEPLOY_TARGET} 健康检查通过" } } } /** * 检查 Nginx * - 放在健康检查之后,确保 blue/green 容器已运行,Nginx reload 时能解析主机名 * - 不存在:启动 → docker cp 配置 → reload * - 已存在:确保运行中 */ stage('检查Nginx') { steps { script { def nginxExists = sh(script: "${SSH_CMD} 'docker ps -aq -f name=${CONTAINER_PREFIX}-nginx'", returnStdout: true).trim() if (!nginxExists) { echo "首次部署,初始化 Nginx 容器" sh "${SSH_CMD} 'cd ${REMOTE_WORK_DIR} && docker-compose -f ${COMPOSE_FILE} up -d nginx'" sh "sleep 3" 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: "${SSH_CMD} 'docker ps -q -f name=${CONTAINER_PREFIX}-nginx'", returnStdout: true).trim() if (!nginxRunning) { echo "Nginx 容器已停止,重新启动" sh "${SSH_CMD} 'docker start ${CONTAINER_PREFIX}-nginx'" sh "sleep 2" } echo "Nginx 容器已存在且运行中" } } } } /** * 切换流量 * - sed 修改 nginx proxy_pass 指向新色 * - reload 生效 */ stage('切换流量') { steps { script { 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}" } } } /** * 删除旧版本 */ stage('删除旧版本') { steps { script { def otherExists = sh(script: "${SSH_CMD} 'docker ps -aq -f name=${CONTAINER_PREFIX}-${env.OTHER_TARGET}'", returnStdout: true).trim() if (otherExists) { sh "${SSH_CMD} 'docker rm -f ${CONTAINER_PREFIX}-${env.OTHER_TARGET}'" echo "旧版本 ${env.OTHER_TARGET} 已删除" } } } } } post { success { echo "✅ 蓝绿部署成功!当前运行: ${env.DEPLOY_TARGET}" } failure { echo '❌ 部署失败,请检查日志' } } }