|
|
@@ -1,101 +1,75 @@
|
|
|
pipeline {
|
|
|
agent any
|
|
|
-
|
|
|
options {
|
|
|
- buildDiscarder(logRotator(numToKeepStr: '10'))
|
|
|
+ buildDiscarder(logRotator(numToKeepStr: '5'))
|
|
|
timestamps()
|
|
|
- disableConcurrentBuilds()
|
|
|
+ }
|
|
|
+ parameters {
|
|
|
+ string(name: 'BRANCH', defaultValue: 'uat', description: '要部署到UAT的分支')
|
|
|
}
|
|
|
|
|
|
- // 设计原则:
|
|
|
- // - 无 parameters 块,所以"Build Now"直接跑、不弹窗
|
|
|
- // - 环境(APP_ENV) 与 分支(BRANCH) 都从 Jenkins 自动注入的 GIT_BRANCH 推断
|
|
|
- // GIT_BRANCH 来自每个 Job 的 SCM 配置(Branch Specifier */dev、*/sit、*/uat 之一)
|
|
|
- // - 因此每台机器上 Job 唯一要配的就是 Branch Specifier,三台机器的 Jenkinsfile 内容完全一致
|
|
|
+ environment {
|
|
|
+ // Dockerfile 路径(相对于代码根目录,即 Jenkins workspace)
|
|
|
+ DOCKERFILE_STORE = "alien_store/Dockerfile"
|
|
|
+ DOCKERFILE_GATEWAY = "alien_gateway/Dockerfile"
|
|
|
+ DOCKERFILE_CONTRACT = "alien_contract/Dockerfile"
|
|
|
|
|
|
- stages {
|
|
|
+ // 如需推送远端 Registry,填写地址,否则留空
|
|
|
+ REGISTRY = ""
|
|
|
+ REGISTRY_CRED = "registry-cred-id"
|
|
|
|
|
|
- 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()
|
|
|
- }
|
|
|
+ // 镜像标签统一使用 uat
|
|
|
+ IMAGE_STORE = "${REGISTRY ? REGISTRY + '/alien_store:uat-' + env.BUILD_NUMBER : 'alien_store:uat'}"
|
|
|
+ IMAGE_GATEWAY = "${REGISTRY ? REGISTRY + '/alien_gateway:uat-' + env.BUILD_NUMBER : 'alien_gateway:uat'}"
|
|
|
+ IMAGE_CONTRACT = "${REGISTRY ? REGISTRY + '/alien_contract:uat-' + env.BUILD_NUMBER : 'alien_contract:uat'}"
|
|
|
|
|
|
- // 把推断结果写回 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"
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
+ // 容器名(store 不要再用 esign-uat,会和真正的 e签宝容器撞名)
|
|
|
+ CONTAINER_NAME_STORE = "alien_store_py-uat"
|
|
|
+ CONTAINER_NAME_GATEWAY = "alien_gateway_py-uat"
|
|
|
+ CONTAINER_NAME_CONTRACT = "alien_contract_py-uat"
|
|
|
|
|
|
- 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 "============================================================"
|
|
|
- }
|
|
|
- }
|
|
|
+ // 容器内端口(必须与各服务 Dockerfile 中 uvicorn 监听端口一致)
|
|
|
+ PORT_STORE = "8001"
|
|
|
+ PORT_GATEWAY = "43333"
|
|
|
+ PORT_CONTRACT = "8002"
|
|
|
+
|
|
|
+ // 使用独立的 UAT 网络
|
|
|
+ DOCKER_NET = "alien_net_uat"
|
|
|
+
|
|
|
+ // UAT 日志目录
|
|
|
+ LOG_ROOT = "/docker/python-uat/logs"
|
|
|
+ }
|
|
|
|
|
|
- stage('Load Env Port Mapping') {
|
|
|
- // 从 .env.${APP_ENV} 解析 GATEWAY_PORT,作为 docker -p 宿主端口映射用
|
|
|
+ stages {
|
|
|
+ // 注意:使用 "Pipeline script from SCM" 模式时,
|
|
|
+ // Jenkins 会自动 checkout 代码到 workspace,不需要在 Jenkinsfile 里再写 Checkout stage。
|
|
|
+
|
|
|
+ stage('Build Images') {
|
|
|
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"
|
|
|
+ // 直接在当前 workspace(Jenkins 已 checkout 的代码根)里构建
|
|
|
+ if (env.REGISTRY?.trim()) {
|
|
|
+ withDockerRegistry(credentialsId: env.REGISTRY_CRED, url: "") {
|
|
|
+ sh "docker build -f ${DOCKERFILE_STORE} -t ${IMAGE_STORE} ."
|
|
|
+ sh "docker build -f ${DOCKERFILE_GATEWAY} -t ${IMAGE_GATEWAY} ."
|
|
|
+ sh "docker build -f ${DOCKERFILE_CONTRACT} -t ${IMAGE_CONTRACT} ."
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ sh "docker build -f ${DOCKERFILE_STORE} -t ${IMAGE_STORE} ."
|
|
|
+ sh "docker build -f ${DOCKERFILE_GATEWAY} -t ${IMAGE_GATEWAY} ."
|
|
|
+ sh "docker build -f ${DOCKERFILE_CONTRACT} -t ${IMAGE_CONTRACT} ."
|
|
|
}
|
|
|
- env.GATEWAY_PORT = gatewayPort
|
|
|
- echo "从 ${envFile} 解析到 GATEWAY_PORT=${env.GATEWAY_PORT}"
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- stage('Build Images') {
|
|
|
+ stage('Push Images') {
|
|
|
+ when { expression { return env.REGISTRY?.trim() as boolean } }
|
|
|
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} ."
|
|
|
+ withDockerRegistry(credentialsId: env.REGISTRY_CRED, url: "") {
|
|
|
+ sh "docker push ${IMAGE_STORE}"
|
|
|
+ sh "docker push ${IMAGE_GATEWAY}"
|
|
|
+ sh "docker push ${IMAGE_CONTRACT}"
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
@@ -103,102 +77,66 @@ pipeline {
|
|
|
stage('Deploy') {
|
|
|
steps {
|
|
|
script {
|
|
|
- echo ">>> [${env.APP_ENV}] 部署镜像: ${env.IMAGE_STORE} / ${env.IMAGE_GATEWAY} / ${env.IMAGE_CONTRACT}"
|
|
|
+ echo ">>> UAT 部署镜像: ${IMAGE_STORE} / ${IMAGE_GATEWAY} / ${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 \\
|
|
|
+ # 创建 UAT 专用网络
|
|
|
+ docker network create ${DOCKER_NET} 2>/dev/null || true
|
|
|
+
|
|
|
+ # 创建日志目录
|
|
|
+ mkdir -p ${LOG_ROOT}/store ${LOG_ROOT}/gateway ${LOG_ROOT}/contract
|
|
|
+
|
|
|
+ # 把当前 workspace 的 .env.uat 复制到宿主机持久目录,供容器挂载使用
|
|
|
+ # 注意:先把可能被 docker 自动创建为目录的同名路径清理掉,确保它是一个普通文件
|
|
|
+ mkdir -p /docker/python-uat
|
|
|
+ rm -rf /docker/python-uat/.env.uat
|
|
|
+ cp .env.uat /docker/python-uat/.env.uat
|
|
|
+ chmod 644 /docker/python-uat/.env.uat
|
|
|
+
|
|
|
+ # 停止旧容器(不会误删 java 的 gateway-uat / store-uat / 真正的 esign-uat 等)
|
|
|
+ docker rm -f ${CONTAINER_NAME_STORE} ${CONTAINER_NAME_GATEWAY} ${CONTAINER_NAME_CONTRACT} 2>/dev/null || true
|
|
|
+
|
|
|
+ # 1) 先启动下游:store
|
|
|
+ docker run -d --name ${CONTAINER_NAME_STORE} \\
|
|
|
+ --network ${DOCKER_NET} \\
|
|
|
+ -e APP_ENV=uat \\
|
|
|
+ -v /docker/python-uat/.env.uat:/app/.env.uat:ro \\
|
|
|
+ -v ${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 \\
|
|
|
+ ${IMAGE_STORE}
|
|
|
+
|
|
|
+ # 2) 再启动下游:contract
|
|
|
+ docker run -d --name ${CONTAINER_NAME_CONTRACT} \\
|
|
|
+ --network ${DOCKER_NET} \\
|
|
|
+ -e APP_ENV=uat \\
|
|
|
+ -v /docker/python-uat/.env.uat:/app/.env.uat:ro \\
|
|
|
+ -v ${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 \\
|
|
|
+ ${IMAGE_CONTRACT}
|
|
|
+
|
|
|
+ # 3) 最后启动网关:gateway(依赖上面两个下游;唯一对外暴露的入口)
|
|
|
+ # STORE_BASE_URL / CONTRACT_BASE_URL 通过 -e 覆盖 .env.uat 中的本地默认值
|
|
|
+ docker run -d --name ${CONTAINER_NAME_GATEWAY} \\
|
|
|
+ --network ${DOCKER_NET} \\
|
|
|
+ -p ${PORT_GATEWAY}:${PORT_GATEWAY} \\
|
|
|
+ -e APP_ENV=uat \\
|
|
|
+ -e STORE_BASE_URL=http://${CONTAINER_NAME_STORE}:${PORT_STORE} \\
|
|
|
+ -e CONTRACT_BASE_URL=http://${CONTAINER_NAME_CONTRACT}:${PORT_CONTRACT} \\
|
|
|
+ -v /docker/python-uat/.env.uat:/app/.env.uat:ro \\
|
|
|
+ -v ${LOG_ROOT}/gateway:/app/common/logs/alien_gateway \\
|
|
|
--restart unless-stopped \\
|
|
|
- ${env.IMAGE_GATEWAY}
|
|
|
+ ${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 验证"
|
|
|
+ echo "UAT 环境部署成功!"
|
|
|
}
|
|
|
failure {
|
|
|
- echo "[${env.APP_ENV}] 环境部署失败,请检查上面日志。"
|
|
|
+ echo "UAT 环境部署失败,请检查日志。"
|
|
|
}
|
|
|
always {
|
|
|
cleanWs()
|