Jenkinsfile 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345
  1. /**
  2. * alien_py_cloud 统一部署流水线(dev / sit / uat / produ 共用)
  3. *
  4. * 设计原则:
  5. * - 无 parameters,Build Now 直接跑
  6. * - APP_ENV 由 Jenkins SCM 的 GIT_BRANCH 自动推断(分支 dev / sit / uat / produ)
  7. * - 端口、SSH 目标等环境差异全部来自 .env.${APP_ENV},代码与 Jenkinsfile 四分支一致
  8. * - dev/sit/uat:Jenkins 本机 docker build + run
  9. * - produ:SSH 到 DEPLOY_SSH_TARGET 远程 build + run(目标见 .env.produ)
  10. */
  11. def PROD_SSH_CREDENTIALS_ID = 'e611a045-2fdc-4613-babd-a72d69bf9814'
  12. def readEnvVar(String envFile, String key, String defaultValue = '') {
  13. def val = sh(
  14. script: "grep -E '^${key}=' ${envFile} | head -n1 | cut -d= -f2- | tr -d '\"' | tr -d \"'\" | xargs || true",
  15. returnStdout: true
  16. ).trim()
  17. return val ?: defaultValue
  18. }
  19. pipeline {
  20. agent any
  21. options {
  22. buildDiscarder(logRotator(numToKeepStr: '10'))
  23. timestamps()
  24. disableConcurrentBuilds()
  25. }
  26. stages {
  27. stage('Resolve Environment') {
  28. steps {
  29. script {
  30. def raw = env.GIT_BRANCH ?: env.BRANCH_NAME ?: ''
  31. def branch = raw.replaceFirst('^origin/', '').replaceFirst('^refs/heads/', '').trim()
  32. if (!(branch in ['dev', 'sit', 'uat', 'produ'])) {
  33. error """
  34. ============ 无法识别当前部署环境 ============
  35. Jenkins 注入的 GIT_BRANCH = '${env.GIT_BRANCH}'
  36. 解析后的分支名 = '${branch}'
  37. 期望分支必须是 dev / sit / uat / produ 之一。
  38. 请检查 Job 配置中的 Branch Specifier:
  39. DEV → origin/dev 或 dev
  40. SIT → origin/sit 或 sit
  41. UAT → origin/uat 或 uat
  42. PRODU → origin/produ 或 produ
  43. =============================================
  44. """.stripIndent()
  45. }
  46. env.APP_ENV = branch
  47. env.BRANCH = branch
  48. def envFile = ".env.${env.APP_ENV}"
  49. if (!fileExists(envFile)) {
  50. error "缺少环境配置文件: ${envFile}"
  51. }
  52. env.STORE_PORT = readEnvVar(envFile, 'STORE_PORT', '8001')
  53. env.GATEWAY_PORT = readEnvVar(envFile, 'GATEWAY_PORT', '33333')
  54. env.CONTRACT_PORT = readEnvVar(envFile, 'CONTRACT_PORT', '8002')
  55. env.GATEWAY_HOST_PORT = readEnvVar(envFile, 'GATEWAY_HOST_PORT', env.GATEWAY_PORT)
  56. env.CONTRACT_HOST_PORT = readEnvVar(envFile, 'CONTRACT_HOST_PORT', '')
  57. env.DEPLOY_MODE = (env.APP_ENV == 'produ') ? 'ssh' : 'local'
  58. env.IMAGE_TAG = "${env.APP_ENV}-${env.BUILD_NUMBER}"
  59. env.IMAGE_STORE = "alien_store:${env.IMAGE_TAG}"
  60. env.IMAGE_GATEWAY = "alien_gateway:${env.IMAGE_TAG}"
  61. env.IMAGE_CONTRACT = "alien_contract:${env.IMAGE_TAG}"
  62. env.CONTAINER_NAME_STORE = "alien_store_py-${env.APP_ENV}"
  63. env.CONTAINER_NAME_GATEWAY = "alien_gateway_py-${env.APP_ENV}"
  64. env.CONTAINER_NAME_CONTRACT = "alien_contract_py-${env.APP_ENV}"
  65. env.DOCKER_NET = "alien_net_${env.APP_ENV}"
  66. if (env.DEPLOY_MODE == 'ssh') {
  67. env.SSH_TARGET = readEnvVar(envFile, 'DEPLOY_SSH_TARGET')
  68. env.CODE_DIR_REMOTE = readEnvVar(envFile, 'DEPLOY_CODE_DIR')
  69. env.LOG_ROOT = readEnvVar(envFile, 'DEPLOY_LOG_ROOT')
  70. env.ENV_FILE_REMOTE = "${env.CODE_DIR_REMOTE}/.env.${env.APP_ENV}"
  71. if (!env.SSH_TARGET || !env.CODE_DIR_REMOTE) {
  72. error ".env.produ 缺少 DEPLOY_SSH_TARGET 或 DEPLOY_CODE_DIR"
  73. }
  74. if (!env.LOG_ROOT) {
  75. env.LOG_ROOT = "${env.CODE_DIR_REMOTE}/logs"
  76. }
  77. } else {
  78. env.LOG_ROOT = "/docker/python-${env.APP_ENV}/logs"
  79. }
  80. echo "APP_ENV=${env.APP_ENV} DEPLOY_MODE=${env.DEPLOY_MODE}"
  81. echo "端口 store=${env.STORE_PORT} gateway=${env.GATEWAY_PORT} host=${env.GATEWAY_HOST_PORT} contract=${env.CONTRACT_PORT}"
  82. }
  83. }
  84. }
  85. stage('Show Build Info') {
  86. steps {
  87. echo "============================================================"
  88. echo " 部署环境 : ${env.APP_ENV} (GIT_BRANCH=${env.GIT_BRANCH})"
  89. echo " 部署模式 : ${env.DEPLOY_MODE}"
  90. echo " 镜像 TAG : ${env.IMAGE_TAG}"
  91. echo " 容器网络 : ${env.DOCKER_NET}"
  92. echo " 日志目录 : ${env.LOG_ROOT}"
  93. if (env.DEPLOY_MODE == 'ssh') {
  94. echo " SSH 目标 : ${env.SSH_TARGET}"
  95. echo " 远程目录 : ${env.CODE_DIR_REMOTE}"
  96. }
  97. echo "============================================================"
  98. }
  99. }
  100. stage('Verify SSH') {
  101. when {
  102. expression { env.DEPLOY_MODE == 'ssh' }
  103. }
  104. steps {
  105. sshagent(credentials: [PROD_SSH_CREDENTIALS_ID]) {
  106. sh """
  107. set -e
  108. ssh -o BatchMode=yes -o StrictHostKeyChecking=accept-new '${env.SSH_TARGET}' \\
  109. 'test -f ${env.ENV_FILE_REMOTE} && sudo docker info >/dev/null'
  110. echo ">>> SSH OK: ${env.SSH_TARGET}"
  111. """
  112. }
  113. }
  114. }
  115. stage('Sync Code') {
  116. when {
  117. expression { env.DEPLOY_MODE == 'ssh' }
  118. }
  119. steps {
  120. sshagent(credentials: [PROD_SSH_CREDENTIALS_ID]) {
  121. sh """
  122. set -e
  123. ssh -o BatchMode=yes -o StrictHostKeyChecking=accept-new '${env.SSH_TARGET}' bash -s <<'REMOTE_EOF'
  124. set -e
  125. cd '${env.CODE_DIR_REMOTE}'
  126. if [ ! -d .git ]; then
  127. echo "ERROR: ${env.CODE_DIR_REMOTE} is not a git repo"
  128. exit 1
  129. fi
  130. git fetch origin
  131. git checkout '${env.BRANCH}'
  132. git pull origin '${env.BRANCH}'
  133. echo ">>> git at \$(git rev-parse --short HEAD) on \$(hostname)"
  134. REMOTE_EOF
  135. """
  136. }
  137. }
  138. }
  139. stage('Build Images') {
  140. steps {
  141. script {
  142. def buildCmds = """
  143. sudo docker build --build-arg APP_ENV=${env.APP_ENV} --build-arg STORE_PORT=${env.STORE_PORT} \\
  144. -f alien_store/Dockerfile -t ${env.IMAGE_STORE} .
  145. sudo docker build --build-arg APP_ENV=${env.APP_ENV} --build-arg GATEWAY_PORT=${env.GATEWAY_PORT} \\
  146. -f alien_gateway/Dockerfile -t ${env.IMAGE_GATEWAY} .
  147. sudo docker build --build-arg APP_ENV=${env.APP_ENV} --build-arg CONTRACT_PORT=${env.CONTRACT_PORT} \\
  148. -f alien_contract/Dockerfile -t ${env.IMAGE_CONTRACT} .
  149. """.stripIndent()
  150. if (env.DEPLOY_MODE == 'ssh') {
  151. sshagent(credentials: [PROD_SSH_CREDENTIALS_ID]) {
  152. sh """
  153. set -e
  154. ssh -o BatchMode=yes -o StrictHostKeyChecking=accept-new '${env.SSH_TARGET}' bash -s <<'REMOTE_EOF'
  155. set -e
  156. cd '${env.CODE_DIR_REMOTE}'
  157. ${buildCmds}
  158. echo ">>> images built on \$(hostname)"
  159. REMOTE_EOF
  160. """
  161. }
  162. } else {
  163. sh """
  164. set -e
  165. docker build --build-arg APP_ENV=${env.APP_ENV} --build-arg STORE_PORT=${env.STORE_PORT} \\
  166. -f alien_store/Dockerfile -t ${env.IMAGE_STORE} .
  167. docker build --build-arg APP_ENV=${env.APP_ENV} --build-arg GATEWAY_PORT=${env.GATEWAY_PORT} \\
  168. -f alien_gateway/Dockerfile -t ${env.IMAGE_GATEWAY} .
  169. docker build --build-arg APP_ENV=${env.APP_ENV} --build-arg CONTRACT_PORT=${env.CONTRACT_PORT} \\
  170. -f alien_contract/Dockerfile -t ${env.IMAGE_CONTRACT} .
  171. """
  172. }
  173. }
  174. }
  175. }
  176. stage('Deploy') {
  177. steps {
  178. script {
  179. def dockerBin = (env.DEPLOY_MODE == 'ssh') ? 'sudo docker' : 'docker'
  180. def envFileArgs = (env.DEPLOY_MODE == 'ssh') ? """
  181. --env-file ${env.ENV_FILE_REMOTE} \\
  182. -v ${env.ENV_FILE_REMOTE}:/app/.env.${env.APP_ENV}:ro \\""" : ''
  183. def contractPortArgs = env.CONTRACT_HOST_PORT?.trim() ?
  184. "-p ${env.CONTRACT_HOST_PORT}:${env.CONTRACT_PORT} \\" : ''
  185. def legacyCleanup = (env.APP_ENV == 'produ') ? """
  186. ${dockerBin} rm -f py_esign_produ py_gateway_produ py_contract_produ esign alien_gateway_py alien_contract_py \\
  187. alien_store_produ alien_gateway_produ alien_contract_produ 2>/dev/null || true
  188. """ : ''
  189. def deployScript = """
  190. set -e
  191. ${dockerBin} network create ${env.DOCKER_NET} 2>/dev/null || true
  192. mkdir -p ${env.LOG_ROOT}/store ${env.LOG_ROOT}/gateway ${env.LOG_ROOT}/contract
  193. ${dockerBin} rm -f ${env.CONTAINER_NAME_STORE} ${env.CONTAINER_NAME_GATEWAY} ${env.CONTAINER_NAME_CONTRACT} 2>/dev/null || true
  194. ${legacyCleanup}
  195. ${dockerBin} run -d --name ${env.CONTAINER_NAME_STORE} \\
  196. --network ${env.DOCKER_NET} \\
  197. ${envFileArgs}
  198. -e APP_ENV=${env.APP_ENV} \\
  199. -e STORE_PORT=${env.STORE_PORT} \\
  200. -v ${env.LOG_ROOT}/store:/app/common/logs/alien_store \\
  201. --restart unless-stopped \\
  202. ${env.IMAGE_STORE}
  203. ${dockerBin} run -d --name ${env.CONTAINER_NAME_CONTRACT} \\
  204. --network ${env.DOCKER_NET} \\
  205. ${contractPortArgs}
  206. ${envFileArgs}
  207. -e APP_ENV=${env.APP_ENV} \\
  208. -e CONTRACT_PORT=${env.CONTRACT_PORT} \\
  209. -v ${env.LOG_ROOT}/contract:/app/common/logs/alien_contract \\
  210. --restart unless-stopped \\
  211. ${env.IMAGE_CONTRACT}
  212. ${dockerBin} run -d --name ${env.CONTAINER_NAME_GATEWAY} \\
  213. --network ${env.DOCKER_NET} \\
  214. -p ${env.GATEWAY_HOST_PORT}:${env.GATEWAY_PORT} \\
  215. ${envFileArgs}
  216. -e APP_ENV=${env.APP_ENV} \\
  217. -e GATEWAY_PORT=${env.GATEWAY_PORT} \\
  218. -e STORE_BASE_URL=http://${env.CONTAINER_NAME_STORE}:${env.STORE_PORT} \\
  219. -e CONTRACT_BASE_URL=http://${env.CONTAINER_NAME_CONTRACT}:${env.CONTRACT_PORT} \\
  220. -v ${env.LOG_ROOT}/gateway:/app/common/logs/alien_gateway \\
  221. --restart unless-stopped \\
  222. ${env.IMAGE_GATEWAY}
  223. """
  224. if (env.DEPLOY_MODE == 'ssh') {
  225. sshagent(credentials: [PROD_SSH_CREDENTIALS_ID]) {
  226. sh """
  227. set -e
  228. ssh -o BatchMode=yes -o StrictHostKeyChecking=accept-new '${env.SSH_TARGET}' bash -s <<'REMOTE_EOF'
  229. ${deployScript}
  230. REMOTE_EOF
  231. """
  232. }
  233. } else {
  234. sh deployScript
  235. }
  236. }
  237. }
  238. }
  239. stage('Smoke Test') {
  240. steps {
  241. script {
  242. sleep(time: 5, unit: 'SECONDS')
  243. def containers = [
  244. env.CONTAINER_NAME_STORE,
  245. env.CONTAINER_NAME_CONTRACT,
  246. env.CONTAINER_NAME_GATEWAY
  247. ]
  248. if (env.DEPLOY_MODE == 'ssh') {
  249. sshagent(credentials: [PROD_SSH_CREDENTIALS_ID]) {
  250. def allRunning = true
  251. containers.each { c ->
  252. def status = sh(
  253. returnStdout: true,
  254. script: """
  255. ssh -o BatchMode=yes -o StrictHostKeyChecking=accept-new '${env.SSH_TARGET}' \\
  256. "sudo docker inspect -f '{{.State.Status}}' ${c} 2>/dev/null || echo missing"
  257. """
  258. ).trim()
  259. echo " ${c}: ${status}"
  260. if (status != 'running') { allRunning = false }
  261. }
  262. if (!allRunning) {
  263. error "存在容器未处于 running 状态,部署失败"
  264. }
  265. def rc = sh(returnStatus: true, script: """
  266. ssh -o BatchMode=yes -o StrictHostKeyChecking=accept-new '${env.SSH_TARGET}' \\
  267. "sudo 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)\\""
  268. """)
  269. if (rc == 0) {
  270. echo "✓ gateway /health 通过"
  271. } else {
  272. echo "⚠️ gateway /health 未通过,请手动验证:curl http://<host>:${env.GATEWAY_HOST_PORT}/health"
  273. }
  274. }
  275. } else {
  276. def allRunning = true
  277. containers.each { c ->
  278. def status = sh(
  279. returnStdout: true,
  280. script: "docker inspect -f '{{.State.Status}}' ${c} 2>/dev/null || echo missing"
  281. ).trim()
  282. echo " ${c}: ${status}"
  283. if (status != 'running') { allRunning = false }
  284. }
  285. if (!allRunning) {
  286. error "存在容器未处于 running 状态,部署失败"
  287. }
  288. echo "✓ 3 个业务容器均在 running 状态"
  289. def rc = sh(returnStatus: true, script: """
  290. 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())'
  291. """)
  292. if (rc == 0) {
  293. echo "✓ gateway /health 通过"
  294. } else {
  295. echo "⚠️ gateway /health 未通过,请手动验证:curl http://<host>:${env.GATEWAY_HOST_PORT}/health"
  296. }
  297. }
  298. }
  299. }
  300. }
  301. }
  302. post {
  303. success {
  304. echo "[${env.APP_ENV}] 部署成功!访问 http://<host>:${env.GATEWAY_HOST_PORT}/health"
  305. }
  306. failure {
  307. echo "[${env.APP_ENV}] 部署失败,请检查日志。"
  308. }
  309. always {
  310. cleanWs()
  311. }
  312. }
  313. }