Jenkinsfile 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341
  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. script {
  88. echo "============================================================"
  89. echo " 部署环境 : ${env.APP_ENV} (GIT_BRANCH=${env.GIT_BRANCH})"
  90. echo " 部署模式 : ${env.DEPLOY_MODE}"
  91. echo " 镜像 TAG : ${env.IMAGE_TAG}"
  92. echo " 容器网络 : ${env.DOCKER_NET}"
  93. echo " 日志目录 : ${env.LOG_ROOT}"
  94. if (env.DEPLOY_MODE == 'ssh') {
  95. echo " SSH 目标 : ${env.SSH_TARGET}"
  96. echo " 远程目录 : ${env.CODE_DIR_REMOTE}"
  97. }
  98. echo "============================================================"
  99. }
  100. }
  101. }
  102. stage('Verify SSH') {
  103. when {
  104. expression { env.DEPLOY_MODE == 'ssh' }
  105. }
  106. steps {
  107. sshagent(credentials: [PROD_SSH_CREDENTIALS_ID]) {
  108. sh """
  109. set -e
  110. ssh -o BatchMode=yes -o StrictHostKeyChecking=accept-new '${env.SSH_TARGET}' \\
  111. 'test -f ${env.ENV_FILE_REMOTE} && sudo docker info >/dev/null'
  112. echo ">>> SSH OK: ${env.SSH_TARGET}"
  113. """
  114. }
  115. }
  116. }
  117. stage('Sync Code') {
  118. when {
  119. expression { env.DEPLOY_MODE == 'ssh' }
  120. }
  121. steps {
  122. sshagent(credentials: [PROD_SSH_CREDENTIALS_ID]) {
  123. sh """
  124. set -e
  125. ssh -o BatchMode=yes -o StrictHostKeyChecking=accept-new '${env.SSH_TARGET}' bash -s <<'REMOTE_EOF'
  126. set -e
  127. cd '${env.CODE_DIR_REMOTE}'
  128. if [ ! -d .git ]; then
  129. echo "ERROR: ${env.CODE_DIR_REMOTE} is not a git repo"
  130. exit 1
  131. fi
  132. git fetch origin
  133. git checkout '${env.BRANCH}'
  134. git reset --hard origin/'${env.BRANCH}'
  135. echo ">>> git at \$(git rev-parse --short HEAD) on \$(hostname)"
  136. REMOTE_EOF
  137. """
  138. }
  139. }
  140. }
  141. stage('Build Images') {
  142. steps {
  143. script {
  144. def buildCmds = """
  145. sudo docker build --build-arg APP_ENV=${env.APP_ENV} --build-arg STORE_PORT=${env.STORE_PORT} \\
  146. -f alien_store/Dockerfile -t ${env.IMAGE_STORE} .
  147. sudo docker build --build-arg APP_ENV=${env.APP_ENV} --build-arg GATEWAY_PORT=${env.GATEWAY_PORT} \\
  148. -f alien_gateway/Dockerfile -t ${env.IMAGE_GATEWAY} .
  149. sudo docker build --build-arg APP_ENV=${env.APP_ENV} --build-arg CONTRACT_PORT=${env.CONTRACT_PORT} \\
  150. -f alien_contract/Dockerfile -t ${env.IMAGE_CONTRACT} .
  151. """.stripIndent()
  152. if (env.DEPLOY_MODE == 'ssh') {
  153. sshagent(credentials: [PROD_SSH_CREDENTIALS_ID]) {
  154. sh """
  155. set -e
  156. ssh -o BatchMode=yes -o StrictHostKeyChecking=accept-new '${env.SSH_TARGET}' bash -s <<'REMOTE_EOF'
  157. set -e
  158. cd '${env.CODE_DIR_REMOTE}'
  159. ${buildCmds}
  160. echo ">>> images built on \$(hostname)"
  161. REMOTE_EOF
  162. """
  163. }
  164. } else {
  165. sh """
  166. set -e
  167. docker build --build-arg APP_ENV=${env.APP_ENV} --build-arg STORE_PORT=${env.STORE_PORT} \\
  168. -f alien_store/Dockerfile -t ${env.IMAGE_STORE} .
  169. docker build --build-arg APP_ENV=${env.APP_ENV} --build-arg GATEWAY_PORT=${env.GATEWAY_PORT} \\
  170. -f alien_gateway/Dockerfile -t ${env.IMAGE_GATEWAY} .
  171. docker build --build-arg APP_ENV=${env.APP_ENV} --build-arg CONTRACT_PORT=${env.CONTRACT_PORT} \\
  172. -f alien_contract/Dockerfile -t ${env.IMAGE_CONTRACT} .
  173. """
  174. }
  175. }
  176. }
  177. }
  178. stage('Deploy') {
  179. steps {
  180. script {
  181. def dockerBin = (env.DEPLOY_MODE == 'ssh') ? 'sudo docker' : 'docker'
  182. def sshEnvLines = (env.DEPLOY_MODE == 'ssh') ?
  183. " --env-file ${env.ENV_FILE_REMOTE} \\\n -v ${env.ENV_FILE_REMOTE}:/app/.env.${env.APP_ENV}:ro \\\n" : ''
  184. def contractPublishLine = env.CONTRACT_HOST_PORT?.trim() ?
  185. " -p ${env.CONTRACT_HOST_PORT}:${env.CONTRACT_PORT} \\\n" : ''
  186. def legacyCleanup = (env.APP_ENV == 'produ') ? """
  187. ${dockerBin} rm -f py_esign_produ py_gateway_produ py_contract_produ esign alien_gateway_py alien_contract_py \\
  188. alien_store_produ alien_gateway_produ alien_contract_produ 2>/dev/null || true
  189. """ : ''
  190. def deployScript = """
  191. set -e
  192. ${dockerBin} network create ${env.DOCKER_NET} 2>/dev/null || true
  193. mkdir -p ${env.LOG_ROOT}/store ${env.LOG_ROOT}/gateway ${env.LOG_ROOT}/contract
  194. ${dockerBin} rm -f ${env.CONTAINER_NAME_STORE} ${env.CONTAINER_NAME_GATEWAY} ${env.CONTAINER_NAME_CONTRACT} 2>/dev/null || true
  195. ${legacyCleanup}
  196. ${dockerBin} run -d --name ${env.CONTAINER_NAME_STORE} \\
  197. --network ${env.DOCKER_NET} \\
  198. ${sshEnvLines} -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. ${contractPublishLine}${sshEnvLines} -e APP_ENV=${env.APP_ENV} \\
  206. -e CONTRACT_PORT=${env.CONTRACT_PORT} \\
  207. -v ${env.LOG_ROOT}/contract:/app/common/logs/alien_contract \\
  208. --restart unless-stopped \\
  209. ${env.IMAGE_CONTRACT}
  210. ${dockerBin} run -d --name ${env.CONTAINER_NAME_GATEWAY} \\
  211. --network ${env.DOCKER_NET} \\
  212. -p ${env.GATEWAY_HOST_PORT}:${env.GATEWAY_PORT} \\
  213. ${sshEnvLines} -e APP_ENV=${env.APP_ENV} \\
  214. -e GATEWAY_PORT=${env.GATEWAY_PORT} \\
  215. -e STORE_BASE_URL=http://${env.CONTAINER_NAME_STORE}:${env.STORE_PORT} \\
  216. -e CONTRACT_BASE_URL=http://${env.CONTAINER_NAME_CONTRACT}:${env.CONTRACT_PORT} \\
  217. -v ${env.LOG_ROOT}/gateway:/app/common/logs/alien_gateway \\
  218. --restart unless-stopped \\
  219. ${env.IMAGE_GATEWAY}
  220. """
  221. if (env.DEPLOY_MODE == 'ssh') {
  222. sshagent(credentials: [PROD_SSH_CREDENTIALS_ID]) {
  223. sh """
  224. set -e
  225. ssh -o BatchMode=yes -o StrictHostKeyChecking=accept-new '${env.SSH_TARGET}' bash -s <<'REMOTE_EOF'
  226. ${deployScript}
  227. REMOTE_EOF
  228. """
  229. }
  230. } else {
  231. sh deployScript
  232. }
  233. }
  234. }
  235. }
  236. stage('Smoke Test') {
  237. steps {
  238. script {
  239. sleep(time: 5, unit: 'SECONDS')
  240. def containers = [
  241. env.CONTAINER_NAME_STORE,
  242. env.CONTAINER_NAME_CONTRACT,
  243. env.CONTAINER_NAME_GATEWAY
  244. ]
  245. if (env.DEPLOY_MODE == 'ssh') {
  246. sshagent(credentials: [PROD_SSH_CREDENTIALS_ID]) {
  247. def allRunning = true
  248. containers.each { c ->
  249. def status = sh(
  250. returnStdout: true,
  251. script: """
  252. ssh -o BatchMode=yes -o StrictHostKeyChecking=accept-new '${env.SSH_TARGET}' \\
  253. "sudo docker inspect -f '{{.State.Status}}' ${c} 2>/dev/null || echo missing"
  254. """
  255. ).trim()
  256. echo " ${c}: ${status}"
  257. if (status != 'running') { allRunning = false }
  258. }
  259. if (!allRunning) {
  260. error "存在容器未处于 running 状态,部署失败"
  261. }
  262. def rc = sh(returnStatus: true, script: """
  263. ssh -o BatchMode=yes -o StrictHostKeyChecking=accept-new '${env.SSH_TARGET}' \\
  264. "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)\\""
  265. """)
  266. if (rc == 0) {
  267. echo "✓ gateway /health 通过"
  268. } else {
  269. echo "⚠️ gateway /health 未通过,请手动验证:curl http://<host>:${env.GATEWAY_HOST_PORT}/health"
  270. }
  271. }
  272. } else {
  273. def allRunning = true
  274. containers.each { c ->
  275. def status = sh(
  276. returnStdout: true,
  277. script: "docker inspect -f '{{.State.Status}}' ${c} 2>/dev/null || echo missing"
  278. ).trim()
  279. echo " ${c}: ${status}"
  280. if (status != 'running') { allRunning = false }
  281. }
  282. if (!allRunning) {
  283. error "存在容器未处于 running 状态,部署失败"
  284. }
  285. echo "✓ 3 个业务容器均在 running 状态"
  286. def rc = sh(returnStatus: true, script: """
  287. 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())'
  288. """)
  289. if (rc == 0) {
  290. echo "✓ gateway /health 通过"
  291. } else {
  292. echo "⚠️ gateway /health 未通过,请手动验证:curl http://<host>:${env.GATEWAY_HOST_PORT}/health"
  293. }
  294. }
  295. }
  296. }
  297. }
  298. }
  299. post {
  300. success {
  301. echo "[${env.APP_ENV}] 部署成功!访问 http://<host>:${env.GATEWAY_HOST_PORT}/health"
  302. }
  303. failure {
  304. echo "[${env.APP_ENV}] 部署失败,请检查日志。"
  305. }
  306. always {
  307. cleanWs()
  308. }
  309. }
  310. }