| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207 |
- pipeline {
- agent any
- options {
- buildDiscarder(logRotator(numToKeepStr: '10'))
- timestamps()
- disableConcurrentBuilds()
- }
- // 设计原则:
- // - 无 parameters 块,所以"Build Now"直接跑、不弹窗
- // - 环境(APP_ENV) 与 分支(BRANCH) 都从 Jenkins 自动注入的 GIT_BRANCH 推断
- // GIT_BRANCH 来自每个 Job 的 SCM 配置(Branch Specifier */dev、*/sit、*/uat 之一)
- // - 因此每台机器上 Job 唯一要配的就是 Branch Specifier,三台机器的 Jenkinsfile 内容完全一致
- stages {
- stage('Resolve Environment') {
- // 从 Jenkins 注入的 GIT_BRANCH 推断 APP_ENV
- // GIT_BRANCH 形如 "origin/uat" / "uat" / "refs/heads/uat",统一去前缀
- steps {
- script {
- def raw = env.GIT_BRANCH ?: env.BRANCH_NAME ?: ''
- def branch = raw.replaceFirst('^origin/', '').replaceFirst('^refs/heads/', '').trim()
- if (!(branch in ['dev', 'sit', 'uat'])) {
- error """
- ============ 无法识别当前部署环境 ============
- Jenkins 注入的 GIT_BRANCH = '${env.GIT_BRANCH}'
- 解析后的分支名 = '${branch}'
- 期望分支必须是 dev / sit / uat 之一。
- 请检查 Job 配置中的 Branch Specifier 是否正确:
- DEV 服务器 → */dev
- SIT 服务器 → */sit
- UAT 服务器 → */uat
- =============================================
- """.stripIndent()
- }
- // 把推断结果写回 env,后续 stage 用 env.APP_ENV 而不是 params.APP_ENV
- env.APP_ENV = branch
- env.BRANCH = branch
- // 派生命名规则(容器名/网络/日志/镜像 TAG 全部以 APP_ENV 区分)
- env.IMAGE_TAG = "${env.APP_ENV}-${env.BUILD_NUMBER}"
- env.IMAGE_STORE = "alien_store:${env.IMAGE_TAG}"
- env.IMAGE_GATEWAY = "alien_gateway:${env.IMAGE_TAG}"
- env.IMAGE_CONTRACT = "alien_contract:${env.IMAGE_TAG}"
- env.CONTAINER_NAME_STORE = "alien_store_py-${env.APP_ENV}"
- env.CONTAINER_NAME_GATEWAY = "alien_gateway_py-${env.APP_ENV}"
- env.CONTAINER_NAME_CONTRACT = "alien_contract_py-${env.APP_ENV}"
- env.DOCKER_NET = "alien_net_${env.APP_ENV}"
- env.LOG_ROOT = "/docker/python-${env.APP_ENV}/logs"
- }
- }
- }
- stage('Show Build Info') {
- steps {
- echo "============================================================"
- echo " 部署环境 : ${env.APP_ENV} (来自 GIT_BRANCH=${env.GIT_BRANCH})"
- echo " 部署分支 : ${env.BRANCH}"
- echo " 镜像 TAG : ${env.IMAGE_TAG}"
- echo " 容器网络 : ${env.DOCKER_NET}"
- echo " 日志根目录: ${env.LOG_ROOT}"
- echo "============================================================"
- }
- }
- stage('Load Env Port Mapping') {
- // 从 .env.${APP_ENV} 解析 GATEWAY_PORT,作为 docker -p 宿主端口映射用
- steps {
- script {
- def envFile = ".env.${env.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=${env.APP_ENV}"
- sh "docker build ${buildArgs} -f alien_store/Dockerfile -t ${env.IMAGE_STORE} ."
- sh "docker build ${buildArgs} -f alien_gateway/Dockerfile -t ${env.IMAGE_GATEWAY} ."
- sh "docker build ${buildArgs} -f alien_contract/Dockerfile -t ${env.IMAGE_CONTRACT} ."
- }
- }
- }
- stage('Deploy') {
- steps {
- script {
- echo ">>> [${env.APP_ENV}] 部署镜像: ${env.IMAGE_STORE} / ${env.IMAGE_GATEWAY} / ${env.IMAGE_CONTRACT}"
- sh """
- set -e
- docker network create ${env.DOCKER_NET} 2>/dev/null || true
- mkdir -p ${env.LOG_ROOT}/store ${env.LOG_ROOT}/gateway ${env.LOG_ROOT}/contract
- docker rm -f ${env.CONTAINER_NAME_STORE} ${env.CONTAINER_NAME_GATEWAY} ${env.CONTAINER_NAME_CONTRACT} 2>/dev/null || true
- # 1) 下游:store
- # APP_ENV=${env.APP_ENV} 让 config.py 加载镜像内的 .env.${env.APP_ENV}
- docker run -d --name ${env.CONTAINER_NAME_STORE} \\
- --network ${env.DOCKER_NET} \\
- -e APP_ENV=${env.APP_ENV} \\
- -v ${env.LOG_ROOT}/store:/app/common/logs/alien_store \\
- --restart unless-stopped \\
- ${env.IMAGE_STORE}
- # 2) 下游:contract
- docker run -d --name ${env.CONTAINER_NAME_CONTRACT} \\
- --network ${env.DOCKER_NET} \\
- -e APP_ENV=${env.APP_ENV} \\
- -v ${env.LOG_ROOT}/contract:/app/common/logs/alien_contract \\
- --restart unless-stopped \\
- ${env.IMAGE_CONTRACT}
- # 3) 网关:gateway(唯一对外端口)
- # -e GATEWAY_PORT=... 是关键:必须覆盖 Dockerfile 的默认 33333,
- # 否则容器内 uvicorn 会监听默认端口,导致与宿主机映射端口对不上
- # pydantic-settings 也是环境变量优先于 .env 文件
- docker run -d --name ${env.CONTAINER_NAME_GATEWAY} \\
- --network ${env.DOCKER_NET} \\
- -p ${env.GATEWAY_PORT}:${env.GATEWAY_PORT} \\
- -e APP_ENV=${env.APP_ENV} \\
- -e GATEWAY_PORT=${env.GATEWAY_PORT} \\
- -e STORE_BASE_URL=http://${env.CONTAINER_NAME_STORE}:8001 \\
- -e CONTRACT_BASE_URL=http://${env.CONTAINER_NAME_CONTRACT}:8002 \\
- -v ${env.LOG_ROOT}/gateway:/app/common/logs/alien_gateway \\
- --restart unless-stopped \\
- ${env.IMAGE_GATEWAY}
- """
- }
- }
- }
- stage('Smoke Test') {
- // 注意:Jenkins 本身可能跑在 Docker 容器里,无法直接 curl 宿主机端口。
- // 这里采取两层验证:
- // 1) 硬验证:3 个业务容器必须都在 running 状态(不通过则 fail)
- // 2) 软验证:通过 docker exec 进入 gateway 容器内部跑 /health 自检
- // (HTTP 失败只 warn,不让 Pipeline 失败,因为这只代表 Jenkins 的网络位置看不到,
- // 不代表服务对外不可用,需要人工从其他位置验证)
- steps {
- script {
- sleep(time: 5, unit: 'SECONDS')
- def containers = [
- env.CONTAINER_NAME_STORE,
- env.CONTAINER_NAME_CONTRACT,
- env.CONTAINER_NAME_GATEWAY
- ]
- def allRunning = true
- containers.each { c ->
- def status = sh(
- returnStdout: true,
- script: "docker inspect -f '{{.State.Status}}' ${c} 2>/dev/null || echo missing"
- ).trim()
- echo " ${c}: ${status}"
- if (status != 'running') {
- allRunning = false
- sh "docker logs --tail 50 ${c} || true"
- }
- }
- if (!allRunning) {
- error "存在容器未处于 running 状态,部署失败"
- }
- echo "✓ 3 个业务容器均在 running 状态"
- def healthCmd = """docker exec ${env.CONTAINER_NAME_GATEWAY} python -c 'import urllib.request as u; r=u.urlopen("http://localhost:${env.GATEWAY_PORT}/health", timeout=3); print("HTTP", r.status, r.read(200).decode())'"""
- def rc = sh(returnStatus: true, script: healthCmd)
- if (rc == 0) {
- echo "✓ gateway /health 通过"
- } else {
- echo "⚠️ gateway /health 未通过(容器在跑但 HTTP 自检失败,请从外部手动验证:curl http://<host>:${env.GATEWAY_PORT}/health)"
- }
- }
- }
- }
- }
- post {
- success {
- echo "[${env.APP_ENV}] 环境部署成功!请从外部访问 http://<host>:${env.GATEWAY_PORT}/health 验证"
- }
- failure {
- echo "[${env.APP_ENV}] 环境部署失败,请检查上面日志。"
- }
- always {
- cleanWs()
- }
- }
- }
|