| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341 |
- /**
- * alien_py_cloud 统一部署流水线(dev / sit / uat / produ 共用)
- *
- * 设计原则:
- * - 无 parameters,Build Now 直接跑
- * - APP_ENV 由 Jenkins SCM 的 GIT_BRANCH 自动推断(分支 dev / sit / uat / produ)
- * - 端口、SSH 目标等环境差异全部来自 .env.${APP_ENV},代码与 Jenkinsfile 四分支一致
- * - dev/sit/uat:Jenkins 本机 docker build + run
- * - produ:SSH 到 DEPLOY_SSH_TARGET 远程 build + run(目标见 .env.produ)
- */
- def PROD_SSH_CREDENTIALS_ID = 'e611a045-2fdc-4613-babd-a72d69bf9814'
- def readEnvVar(String envFile, String key, String defaultValue = '') {
- def val = sh(
- script: "grep -E '^${key}=' ${envFile} | head -n1 | cut -d= -f2- | tr -d '\"' | tr -d \"'\" | xargs || true",
- returnStdout: true
- ).trim()
- return val ?: defaultValue
- }
- pipeline {
- agent any
- options {
- buildDiscarder(logRotator(numToKeepStr: '10'))
- timestamps()
- disableConcurrentBuilds()
- }
- stages {
- stage('Resolve Environment') {
- 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', 'produ'])) {
- error """
- ============ 无法识别当前部署环境 ============
- Jenkins 注入的 GIT_BRANCH = '${env.GIT_BRANCH}'
- 解析后的分支名 = '${branch}'
- 期望分支必须是 dev / sit / uat / produ 之一。
- 请检查 Job 配置中的 Branch Specifier:
- DEV → origin/dev 或 dev
- SIT → origin/sit 或 sit
- UAT → origin/uat 或 uat
- PRODU → origin/produ 或 produ
- =============================================
- """.stripIndent()
- }
- env.APP_ENV = branch
- env.BRANCH = branch
- def envFile = ".env.${env.APP_ENV}"
- if (!fileExists(envFile)) {
- error "缺少环境配置文件: ${envFile}"
- }
- env.STORE_PORT = readEnvVar(envFile, 'STORE_PORT', '8001')
- env.GATEWAY_PORT = readEnvVar(envFile, 'GATEWAY_PORT', '33333')
- env.CONTRACT_PORT = readEnvVar(envFile, 'CONTRACT_PORT', '8002')
- env.GATEWAY_HOST_PORT = readEnvVar(envFile, 'GATEWAY_HOST_PORT', env.GATEWAY_PORT)
- env.CONTRACT_HOST_PORT = readEnvVar(envFile, 'CONTRACT_HOST_PORT', '')
- env.DEPLOY_MODE = (env.APP_ENV == 'produ') ? 'ssh' : 'local'
- 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}"
- if (env.DEPLOY_MODE == 'ssh') {
- env.SSH_TARGET = readEnvVar(envFile, 'DEPLOY_SSH_TARGET')
- env.CODE_DIR_REMOTE = readEnvVar(envFile, 'DEPLOY_CODE_DIR')
- env.LOG_ROOT = readEnvVar(envFile, 'DEPLOY_LOG_ROOT')
- env.ENV_FILE_REMOTE = "${env.CODE_DIR_REMOTE}/.env.${env.APP_ENV}"
- if (!env.SSH_TARGET || !env.CODE_DIR_REMOTE) {
- error ".env.produ 缺少 DEPLOY_SSH_TARGET 或 DEPLOY_CODE_DIR"
- }
- if (!env.LOG_ROOT) {
- env.LOG_ROOT = "${env.CODE_DIR_REMOTE}/logs"
- }
- } else {
- env.LOG_ROOT = "/docker/python-${env.APP_ENV}/logs"
- }
- echo "APP_ENV=${env.APP_ENV} DEPLOY_MODE=${env.DEPLOY_MODE}"
- echo "端口 store=${env.STORE_PORT} gateway=${env.GATEWAY_PORT} host=${env.GATEWAY_HOST_PORT} contract=${env.CONTRACT_PORT}"
- }
- }
- }
- stage('Show Build Info') {
- steps {
- script {
- echo "============================================================"
- echo " 部署环境 : ${env.APP_ENV} (GIT_BRANCH=${env.GIT_BRANCH})"
- echo " 部署模式 : ${env.DEPLOY_MODE}"
- echo " 镜像 TAG : ${env.IMAGE_TAG}"
- echo " 容器网络 : ${env.DOCKER_NET}"
- echo " 日志目录 : ${env.LOG_ROOT}"
- if (env.DEPLOY_MODE == 'ssh') {
- echo " SSH 目标 : ${env.SSH_TARGET}"
- echo " 远程目录 : ${env.CODE_DIR_REMOTE}"
- }
- echo "============================================================"
- }
- }
- }
- stage('Verify SSH') {
- when {
- expression { env.DEPLOY_MODE == 'ssh' }
- }
- steps {
- sshagent(credentials: [PROD_SSH_CREDENTIALS_ID]) {
- sh """
- set -e
- ssh -o BatchMode=yes -o StrictHostKeyChecking=accept-new '${env.SSH_TARGET}' \\
- 'test -f ${env.ENV_FILE_REMOTE} && sudo docker info >/dev/null'
- echo ">>> SSH OK: ${env.SSH_TARGET}"
- """
- }
- }
- }
- stage('Sync Code') {
- when {
- expression { env.DEPLOY_MODE == 'ssh' }
- }
- steps {
- sshagent(credentials: [PROD_SSH_CREDENTIALS_ID]) {
- sh """
- set -e
- ssh -o BatchMode=yes -o StrictHostKeyChecking=accept-new '${env.SSH_TARGET}' bash -s <<'REMOTE_EOF'
- set -e
- cd '${env.CODE_DIR_REMOTE}'
- if [ ! -d .git ]; then
- echo "ERROR: ${env.CODE_DIR_REMOTE} is not a git repo"
- exit 1
- fi
- git fetch origin
- git checkout '${env.BRANCH}'
- git reset --hard origin/'${env.BRANCH}'
- echo ">>> git at \$(git rev-parse --short HEAD) on \$(hostname)"
- REMOTE_EOF
- """
- }
- }
- }
- stage('Build Images') {
- steps {
- script {
- def buildCmds = """
- sudo docker build --build-arg APP_ENV=${env.APP_ENV} --build-arg STORE_PORT=${env.STORE_PORT} \\
- -f alien_store/Dockerfile -t ${env.IMAGE_STORE} .
- sudo docker build --build-arg APP_ENV=${env.APP_ENV} --build-arg GATEWAY_PORT=${env.GATEWAY_PORT} \\
- -f alien_gateway/Dockerfile -t ${env.IMAGE_GATEWAY} .
- sudo docker build --build-arg APP_ENV=${env.APP_ENV} --build-arg CONTRACT_PORT=${env.CONTRACT_PORT} \\
- -f alien_contract/Dockerfile -t ${env.IMAGE_CONTRACT} .
- """.stripIndent()
- if (env.DEPLOY_MODE == 'ssh') {
- sshagent(credentials: [PROD_SSH_CREDENTIALS_ID]) {
- sh """
- set -e
- ssh -o BatchMode=yes -o StrictHostKeyChecking=accept-new '${env.SSH_TARGET}' bash -s <<'REMOTE_EOF'
- set -e
- cd '${env.CODE_DIR_REMOTE}'
- ${buildCmds}
- echo ">>> images built on \$(hostname)"
- REMOTE_EOF
- """
- }
- } else {
- sh """
- set -e
- docker build --build-arg APP_ENV=${env.APP_ENV} --build-arg STORE_PORT=${env.STORE_PORT} \\
- -f alien_store/Dockerfile -t ${env.IMAGE_STORE} .
- docker build --build-arg APP_ENV=${env.APP_ENV} --build-arg GATEWAY_PORT=${env.GATEWAY_PORT} \\
- -f alien_gateway/Dockerfile -t ${env.IMAGE_GATEWAY} .
- docker build --build-arg APP_ENV=${env.APP_ENV} --build-arg CONTRACT_PORT=${env.CONTRACT_PORT} \\
- -f alien_contract/Dockerfile -t ${env.IMAGE_CONTRACT} .
- """
- }
- }
- }
- }
- stage('Deploy') {
- steps {
- script {
- def dockerBin = (env.DEPLOY_MODE == 'ssh') ? 'sudo docker' : 'docker'
- def sshEnvLines = (env.DEPLOY_MODE == 'ssh') ?
- " --env-file ${env.ENV_FILE_REMOTE} \\\n -v ${env.ENV_FILE_REMOTE}:/app/.env.${env.APP_ENV}:ro \\\n" : ''
- def contractPublishLine = env.CONTRACT_HOST_PORT?.trim() ?
- " -p ${env.CONTRACT_HOST_PORT}:${env.CONTRACT_PORT} \\\n" : ''
- def legacyCleanup = (env.APP_ENV == 'produ') ? """
- ${dockerBin} rm -f py_esign_produ py_gateway_produ py_contract_produ esign alien_gateway_py alien_contract_py \\
- alien_store_produ alien_gateway_produ alien_contract_produ 2>/dev/null || true
- """ : ''
- def deployScript = """
- set -e
- ${dockerBin} network create ${env.DOCKER_NET} 2>/dev/null || true
- mkdir -p ${env.LOG_ROOT}/store ${env.LOG_ROOT}/gateway ${env.LOG_ROOT}/contract
- ${dockerBin} rm -f ${env.CONTAINER_NAME_STORE} ${env.CONTAINER_NAME_GATEWAY} ${env.CONTAINER_NAME_CONTRACT} 2>/dev/null || true
- ${legacyCleanup}
- ${dockerBin} run -d --name ${env.CONTAINER_NAME_STORE} \\
- --network ${env.DOCKER_NET} \\
- ${sshEnvLines} -e APP_ENV=${env.APP_ENV} \\
- -e STORE_PORT=${env.STORE_PORT} \\
- -v ${env.LOG_ROOT}/store:/app/common/logs/alien_store \\
- --restart unless-stopped \\
- ${env.IMAGE_STORE}
- ${dockerBin} run -d --name ${env.CONTAINER_NAME_CONTRACT} \\
- --network ${env.DOCKER_NET} \\
- ${contractPublishLine}${sshEnvLines} -e APP_ENV=${env.APP_ENV} \\
- -e CONTRACT_PORT=${env.CONTRACT_PORT} \\
- -v ${env.LOG_ROOT}/contract:/app/common/logs/alien_contract \\
- --restart unless-stopped \\
- ${env.IMAGE_CONTRACT}
- ${dockerBin} run -d --name ${env.CONTAINER_NAME_GATEWAY} \\
- --network ${env.DOCKER_NET} \\
- -p ${env.GATEWAY_HOST_PORT}:${env.GATEWAY_PORT} \\
- ${sshEnvLines} -e APP_ENV=${env.APP_ENV} \\
- -e GATEWAY_PORT=${env.GATEWAY_PORT} \\
- -e STORE_BASE_URL=http://${env.CONTAINER_NAME_STORE}:${env.STORE_PORT} \\
- -e CONTRACT_BASE_URL=http://${env.CONTAINER_NAME_CONTRACT}:${env.CONTRACT_PORT} \\
- -v ${env.LOG_ROOT}/gateway:/app/common/logs/alien_gateway \\
- --restart unless-stopped \\
- ${env.IMAGE_GATEWAY}
- """
- if (env.DEPLOY_MODE == 'ssh') {
- sshagent(credentials: [PROD_SSH_CREDENTIALS_ID]) {
- sh """
- set -e
- ssh -o BatchMode=yes -o StrictHostKeyChecking=accept-new '${env.SSH_TARGET}' bash -s <<'REMOTE_EOF'
- ${deployScript}
- REMOTE_EOF
- """
- }
- } else {
- sh deployScript
- }
- }
- }
- }
- stage('Smoke Test') {
- steps {
- script {
- sleep(time: 5, unit: 'SECONDS')
- def containers = [
- env.CONTAINER_NAME_STORE,
- env.CONTAINER_NAME_CONTRACT,
- env.CONTAINER_NAME_GATEWAY
- ]
- if (env.DEPLOY_MODE == 'ssh') {
- sshagent(credentials: [PROD_SSH_CREDENTIALS_ID]) {
- def allRunning = true
- containers.each { c ->
- def status = sh(
- returnStdout: true,
- script: """
- ssh -o BatchMode=yes -o StrictHostKeyChecking=accept-new '${env.SSH_TARGET}' \\
- "sudo docker inspect -f '{{.State.Status}}' ${c} 2>/dev/null || echo missing"
- """
- ).trim()
- echo " ${c}: ${status}"
- if (status != 'running') { allRunning = false }
- }
- if (!allRunning) {
- error "存在容器未处于 running 状态,部署失败"
- }
- def rc = sh(returnStatus: true, script: """
- ssh -o BatchMode=yes -o StrictHostKeyChecking=accept-new '${env.SSH_TARGET}' \\
- "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)\\""
- """)
- if (rc == 0) {
- echo "✓ gateway /health 通过"
- } else {
- echo "⚠️ gateway /health 未通过,请手动验证:curl http://<host>:${env.GATEWAY_HOST_PORT}/health"
- }
- }
- } else {
- 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 }
- }
- if (!allRunning) {
- error "存在容器未处于 running 状态,部署失败"
- }
- echo "✓ 3 个业务容器均在 running 状态"
- def rc = sh(returnStatus: true, script: """
- 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())'
- """)
- if (rc == 0) {
- echo "✓ gateway /health 通过"
- } else {
- echo "⚠️ gateway /health 未通过,请手动验证:curl http://<host>:${env.GATEWAY_HOST_PORT}/health"
- }
- }
- }
- }
- }
- }
- post {
- success {
- echo "[${env.APP_ENV}] 部署成功!访问 http://<host>:${env.GATEWAY_HOST_PORT}/health"
- }
- failure {
- echo "[${env.APP_ENV}] 部署失败,请检查日志。"
- }
- always {
- cleanWs()
- }
- }
- }
|