pipeline { agent any options { buildDiscarder(logRotator(numToKeepStr: '10')) timestamps() ansiColor('xterm') } // 构建时手选环境与分支:一份 Jenkinsfile 服务 dev / sit / uat 三个环境。 // 物理隔离架构下:每台 Jenkins 上设全局环境变量 DEPLOY_ENV=dev/sit/uat, // Validate Deploy Target stage 会强制 APP_ENV 必须等于 DEPLOY_ENV, // 避免把错误环境的代码部署到错误的机器上。 parameters { choice( name: 'APP_ENV', choices: ['dev', 'sit', 'uat'], description: '部署到哪个环境(物理隔离下:必须与本机 DEPLOY_ENV 一致,否则构建失败)' ) string( name: 'BRANCH', defaultValue: 'dev', description: '要部署的 Git 分支(一般 dev 环境用 dev 分支,sit 用 sit,uat 用 uat;只有一条主干时统一填 main)' ) } environment { // ---------- Dockerfile 路径 ---------- DOCKERFILE_STORE = "alien_store/Dockerfile" DOCKERFILE_GATEWAY = "alien_gateway/Dockerfile" DOCKERFILE_CONTRACT = "alien_contract/Dockerfile" // ---------- 镜像仓库(如不推远端 Registry 就留空) ---------- REGISTRY = "" REGISTRY_CRED = "registry-cred-id" // ---------- 三环境共用的命名规则:全部以 ${APP_ENV} 区分 ---------- IMAGE_TAG = "${params.APP_ENV}-${env.BUILD_NUMBER}" IMAGE_STORE = "${REGISTRY ? REGISTRY + '/' : ''}alien_store:${IMAGE_TAG}" IMAGE_GATEWAY = "${REGISTRY ? REGISTRY + '/' : ''}alien_gateway:${IMAGE_TAG}" IMAGE_CONTRACT = "${REGISTRY ? REGISTRY + '/' : ''}alien_contract:${IMAGE_TAG}" CONTAINER_NAME_STORE = "alien_store_py-${params.APP_ENV}" CONTAINER_NAME_GATEWAY = "alien_gateway_py-${params.APP_ENV}" CONTAINER_NAME_CONTRACT = "alien_contract_py-${params.APP_ENV}" DOCKER_NET = "alien_net_${params.APP_ENV}" LOG_ROOT = "/docker/python-${params.APP_ENV}/logs" } stages { stage('Validate Deploy Target') { // 物理隔离守护:本机的 DEPLOY_ENV 必须与请求的 APP_ENV 一致 // 在 dev 服务器误选 sit/uat 时,立即失败,避免把 uat 代码部到 dev 机 // 配置方式:在每台机器的 Jenkins "Global properties → Environment variables" // 设置 DEPLOY_ENV 为 dev / sit / uat 之一。 // 未配置 DEPLOY_ENV 时(如本地调试),跳过校验。 steps { script { def expected = env.DEPLOY_ENV if (expected && expected != params.APP_ENV) { error """ ============ 部署环境与本机定位不匹配 ============ 本机被定位为: DEPLOY_ENV=${expected} 你请求部署的: APP_ENV=${params.APP_ENV} 本次构建已终止,避免错误地把 ${params.APP_ENV} 部署到 ${expected} 机器上。 如果你确实想改本机定位,请联系运维修改 Jenkins 全局环境变量 DEPLOY_ENV。 ================================================= """.stripIndent() } if (!expected) { echo "提示: 本机 Jenkins 未设置 DEPLOY_ENV,跳过部署目标校验。" } } } } stage('Show Build Info') { steps { echo "============================================================" echo " 部署环境 : ${params.APP_ENV}" echo " 部署分支 : ${params.BRANCH}" echo " 镜像 TAG : ${IMAGE_TAG}" echo " 网络名 : ${DOCKER_NET}" echo " 日志根目录: ${LOG_ROOT}" echo "============================================================" } } // 注意:使用 "Pipeline script from SCM" 模式时 Jenkins 已自动 checkout // 但因为我们参数化了 BRANCH,这里显式 checkout 指定分支 stage('Checkout') { steps { checkout([ $class: 'GitSCM', branches: [[name: "*/${params.BRANCH}"]], userRemoteConfigs: scm.userRemoteConfigs ]) } } stage('Load Env Port Mapping') { // 从 .env.${APP_ENV} 解析 GATEWAY_PORT,作为 docker -p 宿主端口映射用 // (容器内监听端口同样靠 Dockerfile 默认 ARG/ENV 与 .env 配合) steps { script { def envFile = ".env.${params.APP_ENV}" if (!fileExists(envFile)) { error "缺少环境配置文件: ${envFile}(仓库里应该有这份;请检查 .gitignore 是否误屏蔽)" } def gatewayPort = sh( script: "grep -E '^GATEWAY_PORT=' ${envFile} | head -n1 | cut -d= -f2 | tr -d '\"' | tr -d \"'\"", returnStdout: true ).trim() if (!gatewayPort) { gatewayPort = "33333" } env.GATEWAY_PORT = gatewayPort echo "从 ${envFile} 解析到 GATEWAY_PORT=${env.GATEWAY_PORT}" } } } stage('Build Images') { steps { script { def buildArgs = "--build-arg APP_ENV=${params.APP_ENV}" if (env.REGISTRY?.trim()) { withDockerRegistry(credentialsId: env.REGISTRY_CRED, url: "") { sh "docker build ${buildArgs} -f ${DOCKERFILE_STORE} -t ${IMAGE_STORE} ." sh "docker build ${buildArgs} -f ${DOCKERFILE_GATEWAY} -t ${IMAGE_GATEWAY} ." sh "docker build ${buildArgs} -f ${DOCKERFILE_CONTRACT} -t ${IMAGE_CONTRACT} ." } } else { sh "docker build ${buildArgs} -f ${DOCKERFILE_STORE} -t ${IMAGE_STORE} ." sh "docker build ${buildArgs} -f ${DOCKERFILE_GATEWAY} -t ${IMAGE_GATEWAY} ." sh "docker build ${buildArgs} -f ${DOCKERFILE_CONTRACT} -t ${IMAGE_CONTRACT} ." } } } } stage('Push Images') { when { expression { return env.REGISTRY?.trim() as boolean } } steps { withDockerRegistry(credentialsId: env.REGISTRY_CRED, url: "") { sh "docker push ${IMAGE_STORE}" sh "docker push ${IMAGE_GATEWAY}" sh "docker push ${IMAGE_CONTRACT}" } } } stage('Deploy') { steps { script { echo ">>> [${params.APP_ENV}] 部署镜像: ${IMAGE_STORE} / ${IMAGE_GATEWAY} / ${IMAGE_CONTRACT}" sh """ set -e docker network create ${DOCKER_NET} 2>/dev/null || true mkdir -p ${LOG_ROOT}/store ${LOG_ROOT}/gateway ${LOG_ROOT}/contract docker rm -f ${CONTAINER_NAME_STORE} ${CONTAINER_NAME_GATEWAY} ${CONTAINER_NAME_CONTRACT} 2>/dev/null || true # 1) 下游:store # APP_ENV=${params.APP_ENV} 让 config.py 加载镜像内的 .env.${params.APP_ENV} docker run -d --name ${CONTAINER_NAME_STORE} \\ --network ${DOCKER_NET} \\ -e APP_ENV=${params.APP_ENV} \\ -v ${LOG_ROOT}/store:/app/common/logs/alien_store \\ --restart unless-stopped \\ ${IMAGE_STORE} # 2) 下游:contract docker run -d --name ${CONTAINER_NAME_CONTRACT} \\ --network ${DOCKER_NET} \\ -e APP_ENV=${params.APP_ENV} \\ -v ${LOG_ROOT}/contract:/app/common/logs/alien_contract \\ --restart unless-stopped \\ ${IMAGE_CONTRACT} # 3) 网关:gateway(唯一对外端口,并用 -e 把下游地址覆盖为容器名) docker run -d --name ${CONTAINER_NAME_GATEWAY} \\ --network ${DOCKER_NET} \\ -p ${env.GATEWAY_PORT}:${env.GATEWAY_PORT} \\ -e APP_ENV=${params.APP_ENV} \\ -e STORE_BASE_URL=http://${CONTAINER_NAME_STORE}:8001 \\ -e CONTRACT_BASE_URL=http://${CONTAINER_NAME_CONTRACT}:8002 \\ -v ${LOG_ROOT}/gateway:/app/common/logs/alien_gateway \\ --restart unless-stopped \\ ${IMAGE_GATEWAY} """ } } } stage('Smoke Test') { steps { script { sh """ sleep 3 echo '--- /health ---' curl -sf http://127.0.0.1:${env.GATEWAY_PORT}/health || (echo 'gateway /health 失败' && exit 1) echo echo '--- /health/redis ---' curl -sf http://127.0.0.1:${env.GATEWAY_PORT}/health/redis || echo 'redis 健康检查未通过(非阻塞,部署继续)' """ } } } } post { success { echo "[${params.APP_ENV}] 环境部署成功!gateway: http://:${env.GATEWAY_PORT}" } failure { echo "[${params.APP_ENV}] 环境部署失败,请检查上面日志。" } always { cleanWs() } } }