Jenkinsfile 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226
  1. pipeline {
  2. agent any
  3. options {
  4. buildDiscarder(logRotator(numToKeepStr: '10'))
  5. timestamps()
  6. ansiColor('xterm')
  7. }
  8. // 构建时手选环境与分支:一份 Jenkinsfile 服务 dev / sit / uat 三个环境。
  9. // 物理隔离架构下:每台 Jenkins 上设全局环境变量 DEPLOY_ENV=dev/sit/uat,
  10. // Validate Deploy Target stage 会强制 APP_ENV 必须等于 DEPLOY_ENV,
  11. // 避免把错误环境的代码部署到错误的机器上。
  12. parameters {
  13. choice(
  14. name: 'APP_ENV',
  15. choices: ['dev', 'sit', 'uat'],
  16. description: '部署到哪个环境(物理隔离下:必须与本机 DEPLOY_ENV 一致,否则构建失败)'
  17. )
  18. string(
  19. name: 'BRANCH',
  20. defaultValue: 'dev',
  21. description: '要部署的 Git 分支(一般 dev 环境用 dev 分支,sit 用 sit,uat 用 uat;只有一条主干时统一填 main)'
  22. )
  23. }
  24. environment {
  25. // ---------- Dockerfile 路径 ----------
  26. DOCKERFILE_STORE = "alien_store/Dockerfile"
  27. DOCKERFILE_GATEWAY = "alien_gateway/Dockerfile"
  28. DOCKERFILE_CONTRACT = "alien_contract/Dockerfile"
  29. // ---------- 镜像仓库(如不推远端 Registry 就留空) ----------
  30. REGISTRY = ""
  31. REGISTRY_CRED = "registry-cred-id"
  32. // ---------- 三环境共用的命名规则:全部以 ${APP_ENV} 区分 ----------
  33. IMAGE_TAG = "${params.APP_ENV}-${env.BUILD_NUMBER}"
  34. IMAGE_STORE = "${REGISTRY ? REGISTRY + '/' : ''}alien_store:${IMAGE_TAG}"
  35. IMAGE_GATEWAY = "${REGISTRY ? REGISTRY + '/' : ''}alien_gateway:${IMAGE_TAG}"
  36. IMAGE_CONTRACT = "${REGISTRY ? REGISTRY + '/' : ''}alien_contract:${IMAGE_TAG}"
  37. CONTAINER_NAME_STORE = "alien_store_py-${params.APP_ENV}"
  38. CONTAINER_NAME_GATEWAY = "alien_gateway_py-${params.APP_ENV}"
  39. CONTAINER_NAME_CONTRACT = "alien_contract_py-${params.APP_ENV}"
  40. DOCKER_NET = "alien_net_${params.APP_ENV}"
  41. LOG_ROOT = "/docker/python-${params.APP_ENV}/logs"
  42. }
  43. stages {
  44. stage('Validate Deploy Target') {
  45. // 物理隔离守护:本机的 DEPLOY_ENV 必须与请求的 APP_ENV 一致
  46. // 在 dev 服务器误选 sit/uat 时,立即失败,避免把 uat 代码部到 dev 机
  47. // 配置方式:在每台机器的 Jenkins "Global properties → Environment variables"
  48. // 设置 DEPLOY_ENV 为 dev / sit / uat 之一。
  49. // 未配置 DEPLOY_ENV 时(如本地调试),跳过校验。
  50. steps {
  51. script {
  52. def expected = env.DEPLOY_ENV
  53. if (expected && expected != params.APP_ENV) {
  54. error """
  55. ============ 部署环境与本机定位不匹配 ============
  56. 本机被定位为: DEPLOY_ENV=${expected}
  57. 你请求部署的: APP_ENV=${params.APP_ENV}
  58. 本次构建已终止,避免错误地把 ${params.APP_ENV} 部署到 ${expected} 机器上。
  59. 如果你确实想改本机定位,请联系运维修改 Jenkins 全局环境变量 DEPLOY_ENV。
  60. =================================================
  61. """.stripIndent()
  62. }
  63. if (!expected) {
  64. echo "提示: 本机 Jenkins 未设置 DEPLOY_ENV,跳过部署目标校验。"
  65. }
  66. }
  67. }
  68. }
  69. stage('Show Build Info') {
  70. steps {
  71. echo "============================================================"
  72. echo " 部署环境 : ${params.APP_ENV}"
  73. echo " 部署分支 : ${params.BRANCH}"
  74. echo " 镜像 TAG : ${IMAGE_TAG}"
  75. echo " 网络名 : ${DOCKER_NET}"
  76. echo " 日志根目录: ${LOG_ROOT}"
  77. echo "============================================================"
  78. }
  79. }
  80. // 注意:使用 "Pipeline script from SCM" 模式时 Jenkins 已自动 checkout
  81. // 但因为我们参数化了 BRANCH,这里显式 checkout 指定分支
  82. stage('Checkout') {
  83. steps {
  84. checkout([
  85. $class: 'GitSCM',
  86. branches: [[name: "*/${params.BRANCH}"]],
  87. userRemoteConfigs: scm.userRemoteConfigs
  88. ])
  89. }
  90. }
  91. stage('Load Env Port Mapping') {
  92. // 从 .env.${APP_ENV} 解析 GATEWAY_PORT,作为 docker -p 宿主端口映射用
  93. // (容器内监听端口同样靠 Dockerfile 默认 ARG/ENV 与 .env 配合)
  94. steps {
  95. script {
  96. def envFile = ".env.${params.APP_ENV}"
  97. if (!fileExists(envFile)) {
  98. error "缺少环境配置文件: ${envFile}(仓库里应该有这份;请检查 .gitignore 是否误屏蔽)"
  99. }
  100. def gatewayPort = sh(
  101. script: "grep -E '^GATEWAY_PORT=' ${envFile} | head -n1 | cut -d= -f2 | tr -d '\"' | tr -d \"'\"",
  102. returnStdout: true
  103. ).trim()
  104. if (!gatewayPort) {
  105. gatewayPort = "33333"
  106. }
  107. env.GATEWAY_PORT = gatewayPort
  108. echo "从 ${envFile} 解析到 GATEWAY_PORT=${env.GATEWAY_PORT}"
  109. }
  110. }
  111. }
  112. stage('Build Images') {
  113. steps {
  114. script {
  115. def buildArgs = "--build-arg APP_ENV=${params.APP_ENV}"
  116. if (env.REGISTRY?.trim()) {
  117. withDockerRegistry(credentialsId: env.REGISTRY_CRED, url: "") {
  118. sh "docker build ${buildArgs} -f ${DOCKERFILE_STORE} -t ${IMAGE_STORE} ."
  119. sh "docker build ${buildArgs} -f ${DOCKERFILE_GATEWAY} -t ${IMAGE_GATEWAY} ."
  120. sh "docker build ${buildArgs} -f ${DOCKERFILE_CONTRACT} -t ${IMAGE_CONTRACT} ."
  121. }
  122. } else {
  123. sh "docker build ${buildArgs} -f ${DOCKERFILE_STORE} -t ${IMAGE_STORE} ."
  124. sh "docker build ${buildArgs} -f ${DOCKERFILE_GATEWAY} -t ${IMAGE_GATEWAY} ."
  125. sh "docker build ${buildArgs} -f ${DOCKERFILE_CONTRACT} -t ${IMAGE_CONTRACT} ."
  126. }
  127. }
  128. }
  129. }
  130. stage('Push Images') {
  131. when { expression { return env.REGISTRY?.trim() as boolean } }
  132. steps {
  133. withDockerRegistry(credentialsId: env.REGISTRY_CRED, url: "") {
  134. sh "docker push ${IMAGE_STORE}"
  135. sh "docker push ${IMAGE_GATEWAY}"
  136. sh "docker push ${IMAGE_CONTRACT}"
  137. }
  138. }
  139. }
  140. stage('Deploy') {
  141. steps {
  142. script {
  143. echo ">>> [${params.APP_ENV}] 部署镜像: ${IMAGE_STORE} / ${IMAGE_GATEWAY} / ${IMAGE_CONTRACT}"
  144. sh """
  145. set -e
  146. docker network create ${DOCKER_NET} 2>/dev/null || true
  147. mkdir -p ${LOG_ROOT}/store ${LOG_ROOT}/gateway ${LOG_ROOT}/contract
  148. docker rm -f ${CONTAINER_NAME_STORE} ${CONTAINER_NAME_GATEWAY} ${CONTAINER_NAME_CONTRACT} 2>/dev/null || true
  149. # 1) 下游:store
  150. # APP_ENV=${params.APP_ENV} 让 config.py 加载镜像内的 .env.${params.APP_ENV}
  151. docker run -d --name ${CONTAINER_NAME_STORE} \\
  152. --network ${DOCKER_NET} \\
  153. -e APP_ENV=${params.APP_ENV} \\
  154. -v ${LOG_ROOT}/store:/app/common/logs/alien_store \\
  155. --restart unless-stopped \\
  156. ${IMAGE_STORE}
  157. # 2) 下游:contract
  158. docker run -d --name ${CONTAINER_NAME_CONTRACT} \\
  159. --network ${DOCKER_NET} \\
  160. -e APP_ENV=${params.APP_ENV} \\
  161. -v ${LOG_ROOT}/contract:/app/common/logs/alien_contract \\
  162. --restart unless-stopped \\
  163. ${IMAGE_CONTRACT}
  164. # 3) 网关:gateway(唯一对外端口,并用 -e 把下游地址覆盖为容器名)
  165. docker run -d --name ${CONTAINER_NAME_GATEWAY} \\
  166. --network ${DOCKER_NET} \\
  167. -p ${env.GATEWAY_PORT}:${env.GATEWAY_PORT} \\
  168. -e APP_ENV=${params.APP_ENV} \\
  169. -e STORE_BASE_URL=http://${CONTAINER_NAME_STORE}:8001 \\
  170. -e CONTRACT_BASE_URL=http://${CONTAINER_NAME_CONTRACT}:8002 \\
  171. -v ${LOG_ROOT}/gateway:/app/common/logs/alien_gateway \\
  172. --restart unless-stopped \\
  173. ${IMAGE_GATEWAY}
  174. """
  175. }
  176. }
  177. }
  178. stage('Smoke Test') {
  179. steps {
  180. script {
  181. sh """
  182. sleep 3
  183. echo '--- /health ---'
  184. curl -sf http://127.0.0.1:${env.GATEWAY_PORT}/health || (echo 'gateway /health 失败' && exit 1)
  185. echo
  186. echo '--- /health/redis ---'
  187. curl -sf http://127.0.0.1:${env.GATEWAY_PORT}/health/redis || echo 'redis 健康检查未通过(非阻塞,部署继续)'
  188. """
  189. }
  190. }
  191. }
  192. }
  193. post {
  194. success {
  195. echo "[${params.APP_ENV}] 环境部署成功!gateway: http://<host>:${env.GATEWAY_PORT}"
  196. }
  197. failure {
  198. echo "[${params.APP_ENV}] 环境部署失败,请检查上面日志。"
  199. }
  200. always {
  201. cleanWs()
  202. }
  203. }
  204. }