| @@ -0,0 +1,11 @@ | |||||
| # Copy this file to .cos.env on the controlled build server. .cos.env is ignored by Git. | |||||
| COS_SECRET_ID=change-me | |||||
| COS_SECRET_KEY=change-me | |||||
| COS_REGION=ap-chengdu | |||||
| COS_BUCKET=emp-example-bucket | |||||
| # Or leave the two credentials above empty and use an existing coscli config. | |||||
| # COS_CONFIG_PATH=/home/git/.config/coscli.yaml | |||||
| # Signed update URLs are valid for seven days by default. | |||||
| COS_SIGN_EXPIRE=604800 | |||||
| @@ -17,6 +17,7 @@ apply-update.sh 甲方服务器增量更新脚本 | |||||
| profiles/docker-compose.yml emp-test / emp-uat 共用运行模板 | profiles/docker-compose.yml emp-test / emp-uat 共用运行模板 | ||||
| profiles/emp-test/.env.example | profiles/emp-test/.env.example | ||||
| profiles/emp-uat/.env.example | profiles/emp-uat/.env.example | ||||
| profiles/emp-shunfeng/README.md 顺丰无源码主从增量发布说明 | |||||
| docker-compose.runtime.yml 旧默认运行模板,保留兼容 | docker-compose.runtime.yml 旧默认运行模板,保留兼容 | ||||
| ``` | ``` | ||||
| @@ -33,7 +34,7 @@ profiles/<DEPLOY_ENV>/docker-compose.yml | |||||
| ## COS 配置 | ## COS 配置 | ||||
| 打包服务器启用 `COS_UPLOAD=1` 前,需要设置: | |||||
| 打包服务器启用 `COS_UPLOAD=1` 前,可以设置环境变量,或把 `.cos.env.example` 复制为仅保存在受控构建机上的 `.cos.env`: | |||||
| ```bash | ```bash | ||||
| export COS_SECRET_ID=change-me | export COS_SECRET_ID=change-me | ||||
| @@ -52,6 +53,17 @@ export COS_CONFIG_PATH=/path/to/.cos.yaml | |||||
| `publish-cos.sh` 使用腾讯云 `coscli cp` 上传包,并用 `coscli signurl` 生成临时下载 URL。打包服务器需要提前安装 `coscli`。 | `publish-cos.sh` 使用腾讯云 `coscli cp` 上传包,并用 `coscli signurl` 生成临时下载 URL。打包服务器需要提前安装 `coscli`。 | ||||
| ## 顺丰无源码增量发布 | |||||
| 顺丰环境只发布 Admin 与 Java 服务,不接管 PDF、WS 和中间件。首次迁移、主从顺序、自动回滚和源码删除前检查见 [profiles/emp-shunfeng/README.md](profiles/emp-shunfeng/README.md)。日常构建命令: | |||||
| ```bash | |||||
| DEPLOY_ENV=emp-shunfeng \ | |||||
| COS_UPLOAD=1 \ | |||||
| EMP_ROOT=/home/git \ | |||||
| bash ./build-update.sh admin gateway auth | |||||
| ``` | |||||
| ## UAT 全量打包并上传 | ## UAT 全量打包并上传 | ||||
| ```bash | ```bash | ||||
| @@ -21,6 +21,15 @@ NPM_REGISTRY="${NPM_REGISTRY:-https://registry.npmjs.org}" | |||||
| SKIP_BUILD="${SKIP_BUILD:-0}" | SKIP_BUILD="${SKIP_BUILD:-0}" | ||||
| COS_UPLOAD="${COS_UPLOAD:-0}" | COS_UPLOAD="${COS_UPLOAD:-0}" | ||||
| # emp-test / emp-uat 继续使用带环境命名空间的镜像名;shunfeng 为兼容 | |||||
| # 现网 emp-gateway:latest / emp-auth:latest 等平铺名称,不增加命名空间。 | |||||
| if [[ "$DEPLOY_ENV" == "emp-shunfeng" ]]; then | |||||
| DEFAULT_IMAGE_NAME_PREFIX="" | |||||
| else | |||||
| DEFAULT_IMAGE_NAME_PREFIX="$IMAGE_NAMESPACE/" | |||||
| fi | |||||
| IMAGE_NAME_PREFIX="${IMAGE_NAME_PREFIX-$DEFAULT_IMAGE_NAME_PREFIX}" | |||||
| log() { | log() { | ||||
| echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | ||||
| } | } | ||||
| @@ -35,7 +44,16 @@ need_cmd() { | |||||
| } | } | ||||
| has_emp_root() { | has_emp_root() { | ||||
| [[ -d "$1/emp_server" && -d "$1/emp_admin" && -d "$1/emp_ws" ]] | |||||
| local root="$1" | |||||
| local service | |||||
| [[ -d "$root/emp_server" ]] || return 1 | |||||
| for service in "${REQUESTED_SERVICES[@]}"; do | |||||
| case "$service" in | |||||
| admin) [[ -d "$root/emp_admin" ]] || return 1 ;; | |||||
| ws) [[ -d "$root/emp_ws" ]] || return 1 ;; | |||||
| esac | |||||
| done | |||||
| } | } | ||||
| resolve_root_dir() { | resolve_root_dir() { | ||||
| @@ -64,7 +82,7 @@ resolve_root_dir() { | |||||
| fi | fi | ||||
| done | done | ||||
| die "Cannot find EMP root. Set EMP_ROOT=/path/to/emp where emp_server, emp_admin and emp_ws exist." | |||||
| die "Cannot find EMP root for the selected services. Set EMP_ROOT=/path/to/emp." | |||||
| } | } | ||||
| normalize_service() { | normalize_service() { | ||||
| @@ -85,7 +103,7 @@ compose_service_name() { | |||||
| } | } | ||||
| image_name() { | image_name() { | ||||
| echo "$IMAGE_NAMESPACE/emp-$1" | |||||
| echo "${IMAGE_NAME_PREFIX}emp-$1" | |||||
| } | } | ||||
| java_module_name() { | java_module_name() { | ||||
| @@ -193,6 +211,12 @@ build_ws_image() { | |||||
| build_admin_image() { | build_admin_image() { | ||||
| local admin_dir="$ROOT_DIR/emp_admin" | local admin_dir="$ROOT_DIR/emp_admin" | ||||
| local context_dir="$BUILD_CONTEXT_DIR/emp-admin" | local context_dir="$BUILD_CONTEXT_DIR/emp-admin" | ||||
| local nginx_conf="${ADMIN_NGINX_CONF:-$SCRIPT_DIR/nginx/admin.conf}" | |||||
| if [[ "$DEPLOY_ENV" == "emp-shunfeng" && -z "${ADMIN_NGINX_CONF:-}" ]]; then | |||||
| nginx_conf="$admin_dir/nginx.conf" | |||||
| fi | |||||
| [[ -f "$nginx_conf" ]] || die "Admin Nginx config not found: $nginx_conf" | |||||
| log "Build frontend dist" | log "Build frontend dist" | ||||
| run_step "$admin_dir" pnpm install --frozen-lockfile | run_step "$admin_dir" pnpm install --frozen-lockfile | ||||
| @@ -202,7 +226,7 @@ build_admin_image() { | |||||
| rm -rf "$context_dir" | rm -rf "$context_dir" | ||||
| mkdir -p "$context_dir" | mkdir -p "$context_dir" | ||||
| cp -R "$admin_dir/dist" "$context_dir/dist" | cp -R "$admin_dir/dist" "$context_dir/dist" | ||||
| cp "$SCRIPT_DIR/nginx/admin.conf" "$context_dir/admin.conf" | |||||
| cp "$nginx_conf" "$context_dir/admin.conf" | |||||
| log "Build image: $(image_name admin):$IMAGE_TAG" | log "Build image: $(image_name admin):$IMAGE_TAG" | ||||
| run_step "$context_dir" docker build \ | run_step "$context_dir" docker build \ | ||||
| @@ -214,6 +238,7 @@ build_admin_image() { | |||||
| prepare_package() { | prepare_package() { | ||||
| local services_slug="$1" | local services_slug="$1" | ||||
| local profile_dir="${PROFILE_DIR:-$SCRIPT_DIR/profiles/$DEPLOY_ENV}" | |||||
| PACKAGE_NAME="${PACKAGE_NAME:-${DEPLOY_ENV}-update-${IMAGE_TAG}-${services_slug}}" | PACKAGE_NAME="${PACKAGE_NAME:-${DEPLOY_ENV}-update-${IMAGE_TAG}-${services_slug}}" | ||||
| PACKAGE_DIR="$DIST_DIR/$PACKAGE_NAME" | PACKAGE_DIR="$DIST_DIR/$PACKAGE_NAME" | ||||
| PACKAGE_ARCHIVE="$DIST_DIR/${PACKAGE_NAME}.tar.gz" | PACKAGE_ARCHIVE="$DIST_DIR/${PACKAGE_NAME}.tar.gz" | ||||
| @@ -222,9 +247,13 @@ prepare_package() { | |||||
| mkdir -p "$PACKAGE_DIR" | mkdir -p "$PACKAGE_DIR" | ||||
| cp "$SCRIPT_DIR/apply-update.sh" "$PACKAGE_DIR/apply-update.sh" | cp "$SCRIPT_DIR/apply-update.sh" "$PACKAGE_DIR/apply-update.sh" | ||||
| chmod +x "$PACKAGE_DIR/apply-update.sh" | |||||
| local profile_dir="${PROFILE_DIR:-$SCRIPT_DIR/profiles/$DEPLOY_ENV}" | |||||
| if [[ -d "$profile_dir/package-files" ]]; then | |||||
| cp -R "$profile_dir/package-files/." "$PACKAGE_DIR/" | |||||
| fi | |||||
| find "$PACKAGE_DIR" -maxdepth 1 -type f -name '*.sh' -exec chmod +x {} + | |||||
| if [[ -f "$profile_dir/update-env.defaults" ]]; then | if [[ -f "$profile_dir/update-env.defaults" ]]; then | ||||
| cp "$profile_dir/update-env.defaults" "$PACKAGE_DIR/update-env.defaults" | cp "$profile_dir/update-env.defaults" "$PACKAGE_DIR/update-env.defaults" | ||||
| fi | fi | ||||
| @@ -237,6 +266,7 @@ prepare_package() { | |||||
| { | { | ||||
| echo "deploy_env=$DEPLOY_ENV" | echo "deploy_env=$DEPLOY_ENV" | ||||
| echo "image_namespace=$IMAGE_NAMESPACE" | echo "image_namespace=$IMAGE_NAMESPACE" | ||||
| echo "image_name_prefix=$IMAGE_NAME_PREFIX" | |||||
| echo "image_tag=$IMAGE_TAG" | echo "image_tag=$IMAGE_TAG" | ||||
| echo "services=${REQUESTED_SERVICES[*]}" | echo "services=${REQUESTED_SERVICES[*]}" | ||||
| echo "compose_services=$(tr '\n' ' ' < "$PACKAGE_DIR/services.txt")" | echo "compose_services=$(tr '\n' ' ' < "$PACKAGE_DIR/services.txt")" | ||||
| @@ -278,6 +308,14 @@ need_cmd tar | |||||
| mapfile -t REQUESTED_SERVICES < <(read_requested_services "$@") | mapfile -t REQUESTED_SERVICES < <(read_requested_services "$@") | ||||
| [[ "${#REQUESTED_SERVICES[@]}" -gt 0 ]] || die "No services to update." | [[ "${#REQUESTED_SERVICES[@]}" -gt 0 ]] || die "No services to update." | ||||
| if [[ "$DEPLOY_ENV" == "emp-shunfeng" ]]; then | |||||
| for service in "${REQUESTED_SERVICES[@]}"; do | |||||
| case "$service" in | |||||
| pdf|ws) die "emp-shunfeng only supports admin, gateway, auth, monitor and data" ;; | |||||
| esac | |||||
| done | |||||
| fi | |||||
| ROOT_DIR="$(resolve_root_dir)" | ROOT_DIR="$(resolve_root_dir)" | ||||
| SERVICES_SLUG="$(IFS=-; echo "${REQUESTED_SERVICES[*]}")" | SERVICES_SLUG="$(IFS=-; echo "${REQUESTED_SERVICES[*]}")" | ||||
| @@ -331,6 +369,21 @@ COS upload finished. Use the "Target deploy command" above on the target server. | |||||
| EOF | EOF | ||||
| else | else | ||||
| if [[ "$DEPLOY_ENV" == "emp-shunfeng" ]]; then | |||||
| cat <<EOF | |||||
| Next steps on the shunfeng master server: | |||||
| Copy $(basename "$PACKAGE_ARCHIVE") to the master server, then run: | |||||
| mkdir -p /tmp/emp-shunfeng-update | |||||
| tar -xzf $(basename "$PACKAGE_ARCHIVE") -C /tmp/emp-shunfeng-update --strip-components=1 | |||||
| cd /tmp/emp-shunfeng-update | |||||
| DEPLOY_ENV=emp-shunfeng DEPLOY_HOME=/home/emp-shunfeng bash apply-update.sh | |||||
| EOF | |||||
| exit 0 | |||||
| fi | |||||
| cat <<EOF | cat <<EOF | ||||
| Next steps on target server: | Next steps on target server: | ||||
| @@ -0,0 +1,141 @@ | |||||
| # 顺丰无源码增量发布 | |||||
| 这套配置用于把顺丰环境从“在甲方服务器拉源码并现场构建”迁移为“我方构建镜像、甲方服务器只接收镜像包”。它只更新以下服务: | |||||
| - `admin`:主、从服务器 | |||||
| - `gateway`、`auth`、`monitor`:主、从服务器 | |||||
| - `data`:仅主服务器 | |||||
| 现有 `emp-pdf`、`emp-ws`、`emp-nacos`、`emp-tdengine` 不在新发布链路中,不会被停止、重建或删除。 | |||||
| ## 一、迁移前核对 | |||||
| 先在主、从服务器分别记录现状,尤其要核对 Compose 项目名: | |||||
| ```bash | |||||
| docker inspect emp-gateway \ | |||||
| --format '{{ index .Config.Labels "com.docker.compose.project" }}' | |||||
| docker inspect emp-gateway \ | |||||
| --format '{{ index .Config.Labels "com.docker.compose.project.config_files" }}' | |||||
| docker inspect emp-gateway --format '{{.Config.Image}} {{.Image}}' | |||||
| docker inspect emp-admin --format '{{.Config.Image}} {{.Image}}' | |||||
| docker ps -a | |||||
| pm2 list | |||||
| ``` | |||||
| 如果第一个命令不是 `emp_server`,需要把两台服务器 `release.env` 中的 `BACKEND_PROJECT_NAME` 改成实际值。这样更新时会复用原有 `emp_server_emp-net` 网络,不会额外创建另一套网络。 | |||||
| 顺丰环境默认地址为:主服务器 `192.168.0.3`、从服务器 `192.168.0.4`、SSH 端口 `22`。如果 SSH 端口不是 `22`,需要同步修改两台服务器的 `release.env`。 | |||||
| ## 二、一次性安装无源码运行目录 | |||||
| 在我方受控构建机生成引导包: | |||||
| ```bash | |||||
| cd /home/git/deploy/isolated/profiles/emp-shunfeng | |||||
| EMP_ROOT=/home/git bash ./build-runtime-package.sh | |||||
| ``` | |||||
| 把生成的 `dist/emp-shunfeng-runtime-bootstrap-*.tar.gz` 传到主、从服务器。两台服务器分别执行: | |||||
| ```bash | |||||
| mkdir -p /tmp/emp-shunfeng-bootstrap | |||||
| tar -xzf emp-shunfeng-runtime-bootstrap-*.tar.gz \ | |||||
| -C /tmp/emp-shunfeng-bootstrap --strip-components=1 | |||||
| cd /tmp/emp-shunfeng-bootstrap | |||||
| DEPLOY_HOME=/home/emp-shunfeng \ | |||||
| EXISTING_ENV_FILE=<现有emp_server目录下.env的绝对路径> \ | |||||
| bash ./install-runtime.sh | |||||
| ``` | |||||
| 安装只复制运行配置,不加载镜像,也不重启任何容器。随后在两台机器核对: | |||||
| ```bash | |||||
| vi /home/emp-shunfeng/runtime/.env | |||||
| vi /home/emp-shunfeng/runtime/release.env | |||||
| cd /home/emp-shunfeng/runtime | |||||
| docker compose --env-file .env -f docker-compose.yml \ | |||||
| -p emp_server config >/tmp/emp-shunfeng-compose-config.txt | |||||
| ``` | |||||
| `REMOTE_DEPLOY_HOME` 是从服务器上的部署根目录,不是单独的配置文件目录。主、从服务器现在统一使用 `/home/emp-shunfeng`,真正需要的运行文件位于 `/home/emp-shunfeng/runtime`: | |||||
| | 文件 | 主服务器 | 从服务器 | 说明 | | |||||
| | --- | --- | --- | --- | | |||||
| | `runtime/.env` | 必须 | 必须 | 分别复制各自服务器原 `emp_server/.env`,不要用主机文件覆盖从机文件,因为 `HOST_IP` 等配置可能不同 | | |||||
| | `runtime/release.env` | 必须 | 必须 | 主从地址、路径、Compose 项目名;两台可以使用同一份模板 | | |||||
| | `runtime/docker-compose.yml` | 必须 | 必须 | 只定义 Gateway、Auth、Monitor、Data;从机发布时自动排除 Data | | |||||
| | `runtime/deploy-from-url.sh` | 必须 | 可保留 | COS 下载命令只在主服务器执行,从机由主服务器通过 SSH 更新 | | |||||
| 上述文件由 `install-runtime.sh` 自动放到正确位置。根目录 `/home/emp-shunfeng` 下不需要再额外创建 `.env`;`.env` 必须放在 `runtime` 子目录中。 | |||||
| 推荐配置主服务器到从服务器的 SSH 密钥登录。若暂时只能使用密码,可在主服务器的 `release.env` 中增加 `DEPLOY_PASSWORD=...`,文件权限应保持 `600`。 | |||||
| ## 三、构建并发布更新 | |||||
| 在我方受控构建机执行: | |||||
| ```bash | |||||
| cd /home/git/deploy/isolated | |||||
| DEPLOY_ENV=emp-shunfeng \ | |||||
| COS_UPLOAD=1 \ | |||||
| EMP_ROOT=/home/git \ | |||||
| bash ./build-update.sh admin gateway auth | |||||
| ``` | |||||
| 也可以按需发布: | |||||
| ```bash | |||||
| # 两台服务器都更新 admin | |||||
| DEPLOY_ENV=emp-shunfeng COS_UPLOAD=1 EMP_ROOT=/home/git \ | |||||
| bash ./build-update.sh admin | |||||
| # 两台服务器更新 monitor,data 只更新主服务器 | |||||
| DEPLOY_ENV=emp-shunfeng COS_UPLOAD=1 EMP_ROOT=/home/git \ | |||||
| bash ./build-update.sh monitor data | |||||
| ``` | |||||
| 顺丰环境会继续使用平铺镜像名,例如 `emp-gateway:<版本>` 和 `emp-gateway:latest`,不会改为 `emp-shunfeng/emp-gateway`。`pdf` 和 `ws` 在该环境会被构建脚本直接拒绝。 | |||||
| 上传完成后脚本会输出可直接在主服务器执行的命令,目标目录默认为: | |||||
| ```bash | |||||
| cd /home/emp-shunfeng/runtime | |||||
| ``` | |||||
| 发布顺序固定为: | |||||
| 1. 保存主服务器当前镜像作为回滚镜像,但暂不重启主服务。 | |||||
| 2. 把共享服务镜像传到从服务器,先更新从服务器并逐项健康检查。 | |||||
| 3. 从服务器正常后再更新主服务器并逐项健康检查。 | |||||
| 4. 任一节点失败时,自动恢复本次更新前的镜像并重建本次涉及的容器。 | |||||
| 健康检查地址与现网端口一致:Admin `/`、Gateway `/api/health`、Auth `/auth/health`、Monitor `/health`、Data `/health`。 | |||||
| ## 四、COS 凭据 | |||||
| COS 密钥不再写入 Git。在我方构建机一次性创建 `deploy/isolated/.cos.env`: | |||||
| ```bash | |||||
| cd /home/git/deploy/isolated | |||||
| cp .cos.env.example .cos.env | |||||
| chmod 600 .cos.env | |||||
| vi .cos.env | |||||
| ``` | |||||
| `.cos.env` 已被 Git 忽略。配置完成后,日常仍可使用上面的简短构建命令。 | |||||
| ## 五、删除甲方服务器源码 | |||||
| 不要在首次迁移前直接删除源码。建议先完成以下闭环: | |||||
| 1. 两台服务器安装并核对 `/home/emp-shunfeng/runtime`。 | |||||
| 2. 用新链路发布一个低风险服务,确认主从更新、健康检查和自动回滚均正常。 | |||||
| 3. 备份两台服务器现有 `.env`、旧 Compose 文件和 PM2 配置。 | |||||
| 4. 确认 `emp-pdf`、`emp-ws` 的 PM2 启动目录不在准备删除的目录内;它们仍按原方式运行和维护。 | |||||
| 5. 再删除甲方服务器上的 `emp_server`、`emp_admin` Git 工作区,仅保留运行配置、日志和 PM2 所需代码。 | |||||
| 源码删除后,Java 和 Admin 的后续更新只需要在主服务器执行 COS 下载命令;主服务器会自动协调从服务器,无需在从服务器手工重复发布。 | |||||
| @@ -0,0 +1,39 @@ | |||||
| #!/usr/bin/env bash | |||||
| set -Eeuo pipefail | |||||
| # 在我方受控构建机生成一次性的 shunfeng runtime 引导包,不包含业务镜像和源码。 | |||||
| SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" | |||||
| ISOLATED_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)" | |||||
| EMP_ROOT="${EMP_ROOT:-}" | |||||
| DIST_DIR="${DIST_DIR:-$ISOLATED_DIR/dist}" | |||||
| BUILD_TIME="${BUILD_TIME:-$(date '+%Y%m%d%H%M%S')}" | |||||
| PACKAGE_NAME="${PACKAGE_NAME:-emp-shunfeng-runtime-bootstrap-$BUILD_TIME}" | |||||
| PACKAGE_DIR="$DIST_DIR/$PACKAGE_NAME" | |||||
| PACKAGE_ARCHIVE="$DIST_DIR/$PACKAGE_NAME.tar.gz" | |||||
| die() { | |||||
| echo "ERROR: $*" >&2 | |||||
| exit 1 | |||||
| } | |||||
| [[ -n "$EMP_ROOT" ]] || die "set EMP_ROOT to the directory containing emp_server" | |||||
| [[ -f "$EMP_ROOT/emp_server/.env.example" ]] || die "missing $EMP_ROOT/emp_server/.env.example" | |||||
| [[ ! -e "$PACKAGE_DIR" && ! -e "$PACKAGE_ARCHIVE" ]] || die "package output already exists: $PACKAGE_NAME" | |||||
| mkdir -p "$PACKAGE_DIR" | |||||
| cp "$SCRIPT_DIR/docker-compose.yml" "$PACKAGE_DIR/docker-compose.yml" | |||||
| cp "$SCRIPT_DIR/release.env.example" "$PACKAGE_DIR/release.env.example" | |||||
| cp "$SCRIPT_DIR/install-runtime.sh" "$PACKAGE_DIR/install-runtime.sh" | |||||
| cp "$SCRIPT_DIR/README.md" "$PACKAGE_DIR/README.md" | |||||
| cp "$ISOLATED_DIR/deploy-from-url.sh" "$PACKAGE_DIR/deploy-from-url.sh" | |||||
| cp "$EMP_ROOT/emp_server/.env.example" "$PACKAGE_DIR/.env.example" | |||||
| chmod +x "$PACKAGE_DIR/install-runtime.sh" "$PACKAGE_DIR/deploy-from-url.sh" | |||||
| mkdir -p "$DIST_DIR" | |||||
| tar -czf "$PACKAGE_ARCHIVE" -C "$DIST_DIR" "$PACKAGE_NAME" | |||||
| echo "Runtime bootstrap package: $PACKAGE_ARCHIVE" | |||||
| if command -v sha256sum >/dev/null 2>&1; then | |||||
| sha256sum "$PACKAGE_ARCHIVE" | |||||
| fi | |||||
| @@ -0,0 +1,83 @@ | |||||
| x-common-env: &common-env | |||||
| NACOS_ADDR: ${NACOS_ADDR:-localhost:8848} | |||||
| NACOS_USER: ${NACOS_USER:-nacos} | |||||
| NACOS_PWD: ${NACOS_PWD:-nacos} | |||||
| SPRING_PROFILES_ACTIVE: ${SPRING_PROFILES_ACTIVE:-prod} | |||||
| KAFKA_GROUP_ID: ${KAFKA_GROUP_ID:-ecmp-data-group-v2} | |||||
| LOGGING_LOGBACK_ROLLINGPOLICY_MAX_FILE_SIZE: ${SPRING_LOG_MAX_FILE_SIZE:-100MB} | |||||
| LOGGING_LOGBACK_ROLLINGPOLICY_MAX_HISTORY: ${SPRING_LOG_MAX_HISTORY:-30} | |||||
| LOGGING_LOGBACK_ROLLINGPOLICY_TOTAL_SIZE_CAP: ${SPRING_LOG_TOTAL_SIZE_CAP:-5GB} | |||||
| SPRING_CLOUD_NACOS_DISCOVERY_IP: ${HOST_IP:-} | |||||
| x-common: &common | |||||
| restart: unless-stopped | |||||
| env_file: ${BACKEND_ENV_FILE:-.env} | |||||
| environment: | |||||
| <<: *common-env | |||||
| networks: | |||||
| - emp-net | |||||
| logging: | |||||
| driver: json-file | |||||
| options: | |||||
| max-size: ${DOCKER_LOG_MAX_SIZE:-200m} | |||||
| max-file: ${DOCKER_LOG_MAX_FILE:-5} | |||||
| services: | |||||
| emp-gateway: | |||||
| <<: *common | |||||
| image: emp-gateway:${IMAGE_TAG:-latest} | |||||
| container_name: emp-gateway | |||||
| ports: | |||||
| - "9000:9000" | |||||
| volumes: | |||||
| - ${EMP_GATEWAY_LOG_DIR:-/data/emp/logs/emp-gateway}:/logs | |||||
| environment: | |||||
| <<: *common-env | |||||
| LOGGING_FILE_NAME: /logs/emp-gateway.log | |||||
| LOGGING_LOGBACK_ROLLINGPOLICY_FILE_NAME_PATTERN: /logs/emp-gateway-%d{yyyy-MM-dd}.%i.log | |||||
| emp-auth: | |||||
| <<: *common | |||||
| image: emp-auth:${IMAGE_TAG:-latest} | |||||
| container_name: emp-auth | |||||
| ports: | |||||
| - "9001:9001" | |||||
| volumes: | |||||
| - ${EMP_AUTH_LOG_DIR:-/data/emp/logs/emp-auth}:/logs | |||||
| environment: | |||||
| <<: *common-env | |||||
| LOGGING_FILE_NAME: /logs/emp-auth.log | |||||
| LOGGING_LOGBACK_ROLLINGPOLICY_FILE_NAME_PATTERN: /logs/emp-auth-%d{yyyy-MM-dd}.%i.log | |||||
| emp-monitor: | |||||
| <<: *common | |||||
| image: emp-monitor:${IMAGE_TAG:-latest} | |||||
| container_name: emp-monitor | |||||
| ports: | |||||
| - "9002:9002" | |||||
| volumes: | |||||
| - ${EMP_MONITOR_LOG_DIR:-/data/emp/logs/emp-monitor}:/logs | |||||
| environment: | |||||
| <<: *common-env | |||||
| PDF_SERVICE_URL: http://host.docker.internal:3100 | |||||
| LOGGING_FILE_NAME: /logs/emp-monitor.log | |||||
| LOGGING_LOGBACK_ROLLINGPOLICY_FILE_NAME_PATTERN: /logs/emp-monitor-%d{yyyy-MM-dd}.%i.log | |||||
| extra_hosts: | |||||
| - "host.docker.internal:host-gateway" | |||||
| emp-data: | |||||
| <<: *common | |||||
| image: emp-data:${IMAGE_TAG:-latest} | |||||
| container_name: emp-data | |||||
| ports: | |||||
| - "9003:9003" | |||||
| volumes: | |||||
| - ${EMP_DATA_LOG_DIR:-/data/emp/logs/emp-data}:/logs | |||||
| environment: | |||||
| <<: *common-env | |||||
| LOGGING_FILE_NAME: /logs/emp-data.log | |||||
| LOGGING_LOGBACK_ROLLINGPOLICY_FILE_NAME_PATTERN: /logs/emp-data-%d{yyyy-MM-dd}.%i.log | |||||
| networks: | |||||
| emp-net: | |||||
| driver: bridge | |||||
| @@ -0,0 +1,95 @@ | |||||
| #!/usr/bin/env bash | |||||
| set -Eeuo pipefail | |||||
| # 一次性安装 shunfeng 无源码运行目录。本脚本只复制运行配置,不重启服务。 | |||||
| SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" | |||||
| DEPLOY_HOME="${DEPLOY_HOME:-/home/emp-shunfeng}" | |||||
| RUNTIME_DIR="${RUNTIME_DIR:-$DEPLOY_HOME/runtime}" | |||||
| EXISTING_ENV_FILE="${EXISTING_ENV_FILE:-}" | |||||
| log() { | |||||
| echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | |||||
| } | |||||
| die() { | |||||
| echo "ERROR: $*" >&2 | |||||
| exit 1 | |||||
| } | |||||
| validate_runtime() { | |||||
| local compose_cmd=() | |||||
| if docker compose version >/dev/null 2>&1; then | |||||
| compose_cmd=(docker compose) | |||||
| elif command -v docker-compose >/dev/null 2>&1; then | |||||
| compose_cmd=(docker-compose) | |||||
| else | |||||
| die "missing docker compose" | |||||
| fi | |||||
| ( | |||||
| set -a | |||||
| # shellcheck disable=SC1090 | |||||
| . "$RUNTIME_DIR/release.env" | |||||
| set +a | |||||
| [[ "${RUNTIME_DIR:-}" == "/home/emp-shunfeng/runtime" ]] \ | |||||
| || die "RUNTIME_DIR must be /home/emp-shunfeng/runtime" | |||||
| [[ "${BACKEND_COMPOSE_FILE:-}" == "$RUNTIME_DIR/docker-compose.yml" ]] \ | |||||
| || die "BACKEND_COMPOSE_FILE does not match RUNTIME_DIR" | |||||
| [[ "${BACKEND_ENV_FILE:-}" == "$RUNTIME_DIR/.env" ]] \ | |||||
| || die "BACKEND_ENV_FILE does not match RUNTIME_DIR" | |||||
| [[ "${REMOTE_DEPLOY_HOME:-}" == "/home/emp-shunfeng" ]] \ | |||||
| || die "REMOTE_DEPLOY_HOME must be /home/emp-shunfeng" | |||||
| [[ "${REMOTE_RUNTIME_DIR:-}" == "/home/emp-shunfeng/runtime" ]] \ | |||||
| || die "REMOTE_RUNTIME_DIR must be /home/emp-shunfeng/runtime" | |||||
| [[ -n "${PRIMARY_HOST:-}" && -n "${REMOTE_HOST:-}" ]] \ | |||||
| || die "PRIMARY_HOST and REMOTE_HOST are required" | |||||
| BACKEND_ENV_FILE="$RUNTIME_DIR/.env" IMAGE_TAG=latest "${compose_cmd[@]}" \ | |||||
| --env-file "$RUNTIME_DIR/.env" \ | |||||
| -f "$RUNTIME_DIR/docker-compose.yml" \ | |||||
| -p "${BACKEND_PROJECT_NAME:-emp_server}" \ | |||||
| config >/dev/null | |||||
| ) | |||||
| log "Runtime configuration validation passed" | |||||
| } | |||||
| [[ -f "$SCRIPT_DIR/docker-compose.yml" ]] || die "missing docker-compose.yml" | |||||
| [[ -f "$SCRIPT_DIR/deploy-from-url.sh" ]] || die "missing deploy-from-url.sh" | |||||
| [[ -f "$SCRIPT_DIR/release.env.example" ]] || die "missing release.env.example" | |||||
| mkdir -p "$RUNTIME_DIR" | |||||
| cp "$SCRIPT_DIR/docker-compose.yml" "$RUNTIME_DIR/docker-compose.yml" | |||||
| cp "$SCRIPT_DIR/deploy-from-url.sh" "$RUNTIME_DIR/deploy-from-url.sh" | |||||
| chmod +x "$RUNTIME_DIR/deploy-from-url.sh" | |||||
| if [[ ! -f "$RUNTIME_DIR/.env" ]]; then | |||||
| if [[ -n "$EXISTING_ENV_FILE" ]]; then | |||||
| [[ -f "$EXISTING_ENV_FILE" ]] || die "existing env file not found: $EXISTING_ENV_FILE" | |||||
| cp "$EXISTING_ENV_FILE" "$RUNTIME_DIR/.env" | |||||
| chmod 600 "$RUNTIME_DIR/.env" | |||||
| log "Existing backend env copied to $RUNTIME_DIR/.env" | |||||
| elif [[ -f "$SCRIPT_DIR/.env.example" ]]; then | |||||
| cp "$SCRIPT_DIR/.env.example" "$RUNTIME_DIR/.env" | |||||
| chmod 600 "$RUNTIME_DIR/.env" | |||||
| log "Created $RUNTIME_DIR/.env from example; replace all placeholders before deployment" | |||||
| else | |||||
| die "no existing env file supplied and .env.example is missing" | |||||
| fi | |||||
| else | |||||
| log "Keep existing runtime env: $RUNTIME_DIR/.env" | |||||
| fi | |||||
| if [[ ! -f "$RUNTIME_DIR/release.env" ]]; then | |||||
| cp "$SCRIPT_DIR/release.env.example" "$RUNTIME_DIR/release.env" | |||||
| chmod 600 "$RUNTIME_DIR/release.env" | |||||
| log "Created $RUNTIME_DIR/release.env; verify Compose project name and master/sub addresses" | |||||
| else | |||||
| log "Keep existing release config: $RUNTIME_DIR/release.env" | |||||
| fi | |||||
| validate_runtime | |||||
| log "Runtime files installed without restarting containers: $RUNTIME_DIR" | |||||
| log "Next: deploy one image package and verify the services before deleting source" | |||||
| @@ -0,0 +1,286 @@ | |||||
| #!/usr/bin/env bash | |||||
| set -Eeuo pipefail | |||||
| # 在 shunfeng 单个节点上执行镜像快照、服务更新、健康检查或回滚。 | |||||
| # 本脚本由 apply-update.sh 在主机本地调用,并临时复制到从机调用。 | |||||
| ACTION="${1:-}" | |||||
| DEPLOY_ROLE="${2:-}" | |||||
| RUN_ID="${3:-}" | |||||
| IMAGE_TAG="${4:-}" | |||||
| shift $(( $# >= 4 ? 4 : $# )) | |||||
| REQUESTED_SERVICES=("$@") | |||||
| DEPLOY_HOME="${DEPLOY_HOME:-/home/emp-shunfeng}" | |||||
| RUNTIME_DIR="${RUNTIME_DIR:-$DEPLOY_HOME/runtime}" | |||||
| RELEASE_ENV_FILE="${RELEASE_ENV_FILE:-$RUNTIME_DIR/release.env}" | |||||
| log() { | |||||
| echo "[$(date '+%Y-%m-%d %H:%M:%S')] [$DEPLOY_ROLE] $*" | |||||
| } | |||||
| die() { | |||||
| echo "ERROR: [$DEPLOY_ROLE] $*" >&2 | |||||
| exit 1 | |||||
| } | |||||
| need_cmd() { | |||||
| command -v "$1" >/dev/null 2>&1 || die "missing command: $1" | |||||
| } | |||||
| normalize_service() { | |||||
| case "$1" in | |||||
| admin|emp-admin) echo "admin" ;; | |||||
| gateway|emp-gateway) echo "gateway" ;; | |||||
| auth|emp-auth) echo "auth" ;; | |||||
| monitor|emp-monitor) echo "monitor" ;; | |||||
| data|emp-data) echo "data" ;; | |||||
| *) die "unsupported service: $1" ;; | |||||
| esac | |||||
| } | |||||
| service_allowed_on_role() { | |||||
| local service="$1" | |||||
| if [[ "$DEPLOY_ROLE" == "master" ]]; then | |||||
| return 0 | |||||
| fi | |||||
| [[ "$DEPLOY_ROLE" == "sub" && "$service" != "data" ]] | |||||
| } | |||||
| image_name() { | |||||
| echo "emp-$1" | |||||
| } | |||||
| container_name() { | |||||
| echo "emp-$1" | |||||
| } | |||||
| if [[ -f "$RELEASE_ENV_FILE" ]]; then | |||||
| set -a | |||||
| # shellcheck disable=SC1090 | |||||
| . "$RELEASE_ENV_FILE" | |||||
| set +a | |||||
| fi | |||||
| BACKEND_PROJECT_NAME="${BACKEND_PROJECT_NAME:-emp_server}" | |||||
| BACKEND_COMPOSE_FILE="${BACKEND_COMPOSE_FILE:-$RUNTIME_DIR/docker-compose.yml}" | |||||
| BACKEND_ENV_FILE="${BACKEND_ENV_FILE:-$RUNTIME_DIR/.env}" | |||||
| ADMIN_HOST_PORT="${ADMIN_HOST_PORT:-4080}" | |||||
| ADMIN_CONTAINER_PORT="${ADMIN_CONTAINER_PORT:-80}" | |||||
| EMP_ADMIN_LOG_DIR="${EMP_ADMIN_LOG_DIR:-/data/emp/logs/emp-admin}" | |||||
| DOCKER_LOG_MAX_SIZE="${DOCKER_LOG_MAX_SIZE:-200m}" | |||||
| DOCKER_LOG_MAX_FILE="${DOCKER_LOG_MAX_FILE:-5}" | |||||
| HEALTH_RETRIES="${HEALTH_RETRIES:-30}" | |||||
| HEALTH_INTERVAL_SECONDS="${HEALTH_INTERVAL_SECONDS:-2}" | |||||
| NACOS_CONTAINER_NAME="${NACOS_CONTAINER_NAME:-emp-nacos}" | |||||
| REQUIRE_EXISTING_CONTAINERS="${REQUIRE_EXISTING_CONTAINERS:-1}" | |||||
| IMAGE_TAR="${IMAGE_TAR:-}" | |||||
| [[ "$ACTION" =~ ^(snapshot|restore-tags|deploy|apply|rollback)$ ]] || die "invalid action: $ACTION" | |||||
| [[ "$DEPLOY_ROLE" == "master" || "$DEPLOY_ROLE" == "sub" ]] || die "invalid role: $DEPLOY_ROLE" | |||||
| [[ "$RUN_ID" =~ ^[a-zA-Z0-9_.-]+$ ]] || die "invalid run id: $RUN_ID" | |||||
| [[ "$IMAGE_TAG" =~ ^[a-zA-Z0-9_.-]+$ ]] || die "invalid image tag: $IMAGE_TAG" | |||||
| [[ "${#REQUESTED_SERVICES[@]}" -gt 0 ]] || die "no services specified" | |||||
| ACTIVE_SERVICES=() | |||||
| declare -A SEEN_SERVICES=() | |||||
| for raw_service in "${REQUESTED_SERVICES[@]}"; do | |||||
| service="$(normalize_service "$raw_service")" | |||||
| if service_allowed_on_role "$service" && [[ -z "${SEEN_SERVICES[$service]:-}" ]]; then | |||||
| ACTIVE_SERVICES+=("$service") | |||||
| SEEN_SERVICES[$service]=1 | |||||
| fi | |||||
| done | |||||
| [[ "${#ACTIVE_SERVICES[@]}" -gt 0 ]] || die "no services apply to role $DEPLOY_ROLE" | |||||
| need_cmd docker | |||||
| if docker compose version >/dev/null 2>&1; then | |||||
| COMPOSE_CMD=(docker compose) | |||||
| elif command -v docker-compose >/dev/null 2>&1; then | |||||
| COMPOSE_CMD=(docker-compose) | |||||
| else | |||||
| die "missing docker compose" | |||||
| fi | |||||
| snapshot_images() { | |||||
| local service container image image_id rollback_image | |||||
| for service in "${ACTIVE_SERVICES[@]}"; do | |||||
| container="$(container_name "$service")" | |||||
| image="$(image_name "$service")" | |||||
| if ! docker container inspect "$container" >/dev/null 2>&1; then | |||||
| if [[ "$REQUIRE_EXISTING_CONTAINERS" == "1" ]]; then | |||||
| die "existing container is required for rollback: $container" | |||||
| fi | |||||
| log "No existing container to snapshot: $container" | |||||
| continue | |||||
| fi | |||||
| image_id="$(docker container inspect --format '{{.Image}}' "$container")" | |||||
| rollback_image="$image:rollback-$RUN_ID" | |||||
| docker image tag "$image_id" "$rollback_image" | |||||
| log "Rollback image saved: $rollback_image" | |||||
| done | |||||
| } | |||||
| restore_latest_tags() { | |||||
| local service image rollback_image | |||||
| for service in "${ACTIVE_SERVICES[@]}"; do | |||||
| image="$(image_name "$service")" | |||||
| rollback_image="$image:rollback-$RUN_ID" | |||||
| if docker image inspect "$rollback_image" >/dev/null 2>&1; then | |||||
| docker image tag "$rollback_image" "$image:latest" | |||||
| log "Latest tag restored: $image:latest" | |||||
| fi | |||||
| done | |||||
| } | |||||
| ensure_nacos() { | |||||
| local needs_backend=0 service | |||||
| for service in "${ACTIVE_SERVICES[@]}"; do | |||||
| [[ "$service" == "admin" ]] || needs_backend=1 | |||||
| done | |||||
| [[ "$needs_backend" == "1" ]] || return 0 | |||||
| if [[ "$(docker container inspect --format '{{.State.Running}}' "$NACOS_CONTAINER_NAME" 2>/dev/null || true)" != "true" ]]; then | |||||
| die "Nacos container is not running: $NACOS_CONTAINER_NAME" | |||||
| fi | |||||
| } | |||||
| run_admin() { | |||||
| local tag="$1" | |||||
| local image="emp-admin:$tag" | |||||
| docker image inspect "$image" >/dev/null 2>&1 || die "image not found: $image" | |||||
| docker stop emp-admin >/dev/null 2>&1 || true | |||||
| docker rm emp-admin >/dev/null 2>&1 || true | |||||
| docker run -d \ | |||||
| --name emp-admin \ | |||||
| -p "$ADMIN_HOST_PORT:$ADMIN_CONTAINER_PORT" \ | |||||
| -v "$EMP_ADMIN_LOG_DIR:/var/log/nginx" \ | |||||
| --log-opt "max-size=$DOCKER_LOG_MAX_SIZE" \ | |||||
| --log-opt "max-file=$DOCKER_LOG_MAX_FILE" \ | |||||
| --restart unless-stopped \ | |||||
| "$image" >/dev/null | |||||
| log "Admin started: $image" | |||||
| } | |||||
| run_backend_services() { | |||||
| local tag="$1" | |||||
| local compose_services=() | |||||
| local service image | |||||
| for service in "${ACTIVE_SERVICES[@]}"; do | |||||
| [[ "$service" == "admin" ]] && continue | |||||
| image="$(image_name "$service"):$tag" | |||||
| docker image inspect "$image" >/dev/null 2>&1 || die "image not found: $image" | |||||
| compose_services+=("emp-$service") | |||||
| done | |||||
| [[ "${#compose_services[@]}" -gt 0 ]] || return 0 | |||||
| [[ -f "$BACKEND_COMPOSE_FILE" ]] || die "compose file not found: $BACKEND_COMPOSE_FILE" | |||||
| [[ -f "$BACKEND_ENV_FILE" ]] || die "env file not found: $BACKEND_ENV_FILE" | |||||
| IMAGE_TAG="$tag" "${COMPOSE_CMD[@]}" \ | |||||
| --env-file "$BACKEND_ENV_FILE" \ | |||||
| -f "$BACKEND_COMPOSE_FILE" \ | |||||
| -p "$BACKEND_PROJECT_NAME" \ | |||||
| up -d --no-deps --no-build --force-recreate "${compose_services[@]}" | |||||
| } | |||||
| http_ok() { | |||||
| local url="$1" | |||||
| if command -v curl >/dev/null 2>&1; then | |||||
| curl -fsS --connect-timeout 2 --max-time 5 "$url" >/dev/null | |||||
| elif command -v wget >/dev/null 2>&1; then | |||||
| wget -q -T 5 -O /dev/null "$url" | |||||
| else | |||||
| die "curl or wget is required for health checks" | |||||
| fi | |||||
| } | |||||
| health_url() { | |||||
| case "$1" in | |||||
| admin) echo "http://127.0.0.1:$ADMIN_HOST_PORT/" ;; | |||||
| gateway) echo "http://127.0.0.1:9000/api/health" ;; | |||||
| auth) echo "http://127.0.0.1:9001/auth/health" ;; | |||||
| monitor) echo "http://127.0.0.1:9002/health" ;; | |||||
| data) echo "http://127.0.0.1:9003/health" ;; | |||||
| esac | |||||
| } | |||||
| wait_for_service() { | |||||
| local service="$1" | |||||
| local container url attempt | |||||
| container="$(container_name "$service")" | |||||
| url="$(health_url "$service")" | |||||
| for ((attempt = 1; attempt <= HEALTH_RETRIES; attempt += 1)); do | |||||
| if [[ "$(docker container inspect --format '{{.State.Running}}' "$container" 2>/dev/null || true)" == "true" ]] \ | |||||
| && http_ok "$url"; then | |||||
| log "Health check passed: $service ($url)" | |||||
| return 0 | |||||
| fi | |||||
| sleep "$HEALTH_INTERVAL_SECONDS" | |||||
| done | |||||
| log "Health check failed: $service ($url)" | |||||
| docker logs --tail=80 "$container" 2>/dev/null || true | |||||
| return 1 | |||||
| } | |||||
| health_check() { | |||||
| local service | |||||
| for service in "${ACTIVE_SERVICES[@]}"; do | |||||
| wait_for_service "$service" || return 1 | |||||
| done | |||||
| } | |||||
| deploy_services() { | |||||
| local tag="$1" | |||||
| local service | |||||
| ensure_nacos | |||||
| for service in "${ACTIVE_SERVICES[@]}"; do | |||||
| if [[ "$service" == "admin" ]]; then | |||||
| run_admin "$tag" || return 1 | |||||
| fi | |||||
| done | |||||
| run_backend_services "$tag" || return 1 | |||||
| health_check | |||||
| } | |||||
| rollback_services() { | |||||
| log "Rolling back services: ${ACTIVE_SERVICES[*]}" | |||||
| restore_latest_tags | |||||
| deploy_services latest | |||||
| } | |||||
| case "$ACTION" in | |||||
| snapshot) | |||||
| snapshot_images | |||||
| ;; | |||||
| restore-tags) | |||||
| restore_latest_tags | |||||
| ;; | |||||
| deploy) | |||||
| deploy_services "$IMAGE_TAG" | |||||
| ;; | |||||
| rollback) | |||||
| rollback_services | |||||
| ;; | |||||
| apply) | |||||
| [[ -n "$IMAGE_TAR" && -f "$IMAGE_TAR" ]] || die "image tar not found: $IMAGE_TAR" | |||||
| snapshot_images | |||||
| log "Load images: $IMAGE_TAR" | |||||
| docker load -i "$IMAGE_TAR" | |||||
| if ! deploy_services "$IMAGE_TAG"; then | |||||
| log "Deployment failed, start rollback" | |||||
| rollback_services || true | |||||
| exit 1 | |||||
| fi | |||||
| if [[ "$DEPLOY_ROLE" == "sub" ]]; then | |||||
| docker stop emp-data >/dev/null 2>&1 || true | |||||
| docker rm emp-data >/dev/null 2>&1 || true | |||||
| fi | |||||
| ;; | |||||
| esac | |||||
| @@ -0,0 +1,278 @@ | |||||
| #!/usr/bin/env bash | |||||
| set -Eeuo pipefail | |||||
| # shunfeng 主从增量发布编排器。 | |||||
| # 只处理 admin / gateway / auth / monitor / data;不处理 PDF、WS、中间件。 | |||||
| SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" | |||||
| cd "$SCRIPT_DIR" | |||||
| DEPLOY_ENV="${DEPLOY_ENV:-emp-shunfeng}" | |||||
| DEPLOY_HOME="${DEPLOY_HOME:-/home/emp-shunfeng}" | |||||
| RUNTIME_DIR="${RUNTIME_DIR:-$DEPLOY_HOME/runtime}" | |||||
| RELEASE_ENV_FILE="${RELEASE_ENV_FILE:-$RUNTIME_DIR/release.env}" | |||||
| IMAGE_TAR="${IMAGE_TAR:-images.tar}" | |||||
| SERVICE_FILE="${SERVICE_FILE:-services.txt}" | |||||
| MANIFEST_FILE="${MANIFEST_FILE:-manifest.txt}" | |||||
| NODE_HELPER="$SCRIPT_DIR/apply-node-update.sh" | |||||
| log() { | |||||
| echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | |||||
| } | |||||
| die() { | |||||
| echo "ERROR: $*" >&2 | |||||
| exit 1 | |||||
| } | |||||
| need_cmd() { | |||||
| command -v "$1" >/dev/null 2>&1 || die "missing command: $1" | |||||
| } | |||||
| normalize_service() { | |||||
| case "$1" in | |||||
| admin|emp-admin) echo "admin" ;; | |||||
| gateway|emp-gateway) echo "gateway" ;; | |||||
| auth|emp-auth) echo "auth" ;; | |||||
| monitor|emp-monitor) echo "monitor" ;; | |||||
| data|emp-data) echo "data" ;; | |||||
| *) die "unsupported service: $1" ;; | |||||
| esac | |||||
| } | |||||
| manifest_value() { | |||||
| local key="$1" | |||||
| sed -n "s/^${key}=//p" "$MANIFEST_FILE" | head -1 | |||||
| } | |||||
| contains_service() { | |||||
| local target="$1" | |||||
| shift | |||||
| local service | |||||
| for service in "$@"; do | |||||
| [[ "$service" == "$target" ]] && return 0 | |||||
| done | |||||
| return 1 | |||||
| } | |||||
| if [[ -f "$RELEASE_ENV_FILE" ]]; then | |||||
| set -a | |||||
| # shellcheck disable=SC1090 | |||||
| . "$RELEASE_ENV_FILE" | |||||
| set +a | |||||
| fi | |||||
| PRIMARY_HOST="${PRIMARY_HOST:-192.168.0.3}" | |||||
| REMOTE_HOST="${REMOTE_HOST:-192.168.0.4}" | |||||
| REMOTE_USER="${REMOTE_USER:-root}" | |||||
| SSH_PORT="${SSH_PORT:-22}" | |||||
| REMOTE_DEPLOY_HOME="${REMOTE_DEPLOY_HOME:-/home/emp-shunfeng}" | |||||
| REMOTE_RUNTIME_DIR="${REMOTE_RUNTIME_DIR:-$REMOTE_DEPLOY_HOME/runtime}" | |||||
| SKIP_PRIMARY_HOST_CHECK="${SKIP_PRIMARY_HOST_CHECK:-0}" | |||||
| SKIP_REMOTE_SYNC="${SKIP_REMOTE_SYNC:-0}" | |||||
| BACKEND_COMPOSE_FILE="${BACKEND_COMPOSE_FILE:-$RUNTIME_DIR/docker-compose.yml}" | |||||
| BACKEND_ENV_FILE="${BACKEND_ENV_FILE:-$RUNTIME_DIR/.env}" | |||||
| SSH_OPTS=( | |||||
| -o StrictHostKeyChecking=accept-new | |||||
| -o ServerAliveInterval=15 | |||||
| -o ServerAliveCountMax=3 | |||||
| ) | |||||
| ssh_cmd() { | |||||
| if [[ -n "${DEPLOY_PASSWORD:-}" ]]; then | |||||
| need_cmd sshpass | |||||
| SSHPASS="$DEPLOY_PASSWORD" sshpass -e ssh -p "$SSH_PORT" "${SSH_OPTS[@]}" "$REMOTE_USER@$REMOTE_HOST" "$@" | |||||
| else | |||||
| ssh -p "$SSH_PORT" "${SSH_OPTS[@]}" "$REMOTE_USER@$REMOTE_HOST" "$@" | |||||
| fi | |||||
| } | |||||
| scp_file() { | |||||
| local source_file="$1" | |||||
| local target_file="$2" | |||||
| if [[ -n "${DEPLOY_PASSWORD:-}" ]]; then | |||||
| need_cmd sshpass | |||||
| SSHPASS="$DEPLOY_PASSWORD" sshpass -e scp -P "$SSH_PORT" "${SSH_OPTS[@]}" "$source_file" "$REMOTE_USER@$REMOTE_HOST:$target_file" | |||||
| else | |||||
| scp -P "$SSH_PORT" "${SSH_OPTS[@]}" "$source_file" "$REMOTE_USER@$REMOTE_HOST:$target_file" | |||||
| fi | |||||
| } | |||||
| ensure_primary_host() { | |||||
| [[ "$SKIP_PRIMARY_HOST_CHECK" == "1" ]] && return 0 | |||||
| local ips ip | |||||
| ips="$(hostname -I 2>/dev/null || true)" | |||||
| [[ -n "$ips" ]] || return 0 | |||||
| for ip in $ips; do | |||||
| [[ "$ip" == "$PRIMARY_HOST" ]] && return 0 | |||||
| done | |||||
| die "current host IP does not include $PRIMARY_HOST. Current IP: $ips" | |||||
| } | |||||
| node_action() { | |||||
| local action="$1" | |||||
| local role="$2" | |||||
| shift 2 | |||||
| DEPLOY_HOME="$DEPLOY_HOME" \ | |||||
| RUNTIME_DIR="$RUNTIME_DIR" \ | |||||
| bash "$NODE_HELPER" "$action" "$role" "$RUN_ID" "$IMAGE_TAG" "$@" | |||||
| } | |||||
| remote_action() { | |||||
| local action="$1" | |||||
| local image_tar="$2" | |||||
| shift 2 | |||||
| local service_args command_text | |||||
| printf -v service_args '%q ' "$@" | |||||
| command_text="DEPLOY_HOME=$(printf '%q' "$REMOTE_DEPLOY_HOME") RUNTIME_DIR=$(printf '%q' "$REMOTE_RUNTIME_DIR")" | |||||
| if [[ -n "$image_tar" ]]; then | |||||
| command_text+=" IMAGE_TAR=$(printf '%q' "$image_tar")" | |||||
| fi | |||||
| command_text+=" bash $(printf '%q' "$REMOTE_HELPER") $(printf '%q' "$action") sub $(printf '%q' "$RUN_ID") $(printf '%q' "$IMAGE_TAG") $service_args" | |||||
| ssh_cmd "bash -lc $(printf '%q' "$command_text")" | |||||
| } | |||||
| cleanup_remote_files() { | |||||
| [[ -n "${REMOTE_ARCHIVE:-}" && -n "${REMOTE_HELPER:-}" ]] || return 0 | |||||
| local cleanup_command | |||||
| cleanup_command="rm -f -- $(printf '%q' "$REMOTE_ARCHIVE") $(printf '%q' "$REMOTE_HELPER")" | |||||
| ssh_cmd "bash -lc $(printf '%q' "$cleanup_command")" >/dev/null 2>&1 || true | |||||
| } | |||||
| cleanup() { | |||||
| if [[ -n "${SHARED_IMAGE_ARCHIVE:-}" && -f "${SHARED_IMAGE_ARCHIVE:-}" ]]; then | |||||
| rm -f -- "$SHARED_IMAGE_ARCHIVE" | |||||
| fi | |||||
| } | |||||
| trap cleanup EXIT | |||||
| need_cmd docker | |||||
| need_cmd sed | |||||
| need_cmd hostname | |||||
| need_cmd mktemp | |||||
| [[ "$DEPLOY_ENV" == "emp-shunfeng" ]] || die "this package must use DEPLOY_ENV=emp-shunfeng" | |||||
| [[ -f "$IMAGE_TAR" ]] || die "image tar not found: $IMAGE_TAR" | |||||
| [[ -f "$SERVICE_FILE" ]] || die "service file not found: $SERVICE_FILE" | |||||
| [[ -f "$MANIFEST_FILE" ]] || die "manifest not found: $MANIFEST_FILE" | |||||
| [[ -f "$NODE_HELPER" ]] || die "node helper not found: $NODE_HELPER" | |||||
| [[ -f "$RELEASE_ENV_FILE" ]] || die "release config not found: $RELEASE_ENV_FILE" | |||||
| [[ -f "$BACKEND_COMPOSE_FILE" ]] || die "compose file not found: $BACKEND_COMPOSE_FILE" | |||||
| [[ -f "$BACKEND_ENV_FILE" ]] || die "env file not found: $BACKEND_ENV_FILE" | |||||
| PACKAGE_ENV="$(manifest_value deploy_env)" | |||||
| IMAGE_TAG="$(manifest_value image_tag)" | |||||
| [[ "$PACKAGE_ENV" == "emp-shunfeng" ]] || die "package env mismatch: $PACKAGE_ENV" | |||||
| [[ "$IMAGE_TAG" =~ ^[a-zA-Z0-9_.-]+$ ]] || die "invalid package image tag: $IMAGE_TAG" | |||||
| RUN_ID="${IMAGE_TAG}-$(date '+%Y%m%d%H%M%S')" | |||||
| REQUESTED_SERVICES=() | |||||
| declare -A SEEN_SERVICES=() | |||||
| while IFS= read -r raw_service || [[ -n "$raw_service" ]]; do | |||||
| [[ -n "$raw_service" ]] || continue | |||||
| service="$(normalize_service "$raw_service")" | |||||
| if [[ -z "${SEEN_SERVICES[$service]:-}" ]]; then | |||||
| REQUESTED_SERVICES+=("$service") | |||||
| SEEN_SERVICES[$service]=1 | |||||
| fi | |||||
| done < "$SERVICE_FILE" | |||||
| [[ "${#REQUESTED_SERVICES[@]}" -gt 0 ]] || die "no services in package" | |||||
| SHARED_SERVICES=() | |||||
| for service in "${REQUESTED_SERVICES[@]}"; do | |||||
| [[ "$service" == "data" ]] || SHARED_SERVICES+=("$service") | |||||
| done | |||||
| ensure_primary_host | |||||
| if [[ "${#SHARED_SERVICES[@]}" -gt 0 && "$SKIP_REMOTE_SYNC" != "1" ]]; then | |||||
| need_cmd ssh | |||||
| need_cmd scp | |||||
| remote_preflight="test -f $(printf '%q' "$REMOTE_RUNTIME_DIR/release.env") -a -f $(printf '%q' "$REMOTE_RUNTIME_DIR/docker-compose.yml") -a -f $(printf '%q' "$REMOTE_RUNTIME_DIR/.env")" | |||||
| ssh_cmd "bash -lc $(printf '%q' "$remote_preflight")" \ | |||||
| || die "sub server runtime is not ready: $REMOTE_RUNTIME_DIR" | |||||
| fi | |||||
| log "=========================================" | |||||
| log "EMP shunfeng image-package deployment" | |||||
| log "Release: $IMAGE_TAG" | |||||
| log "Master services: ${REQUESTED_SERVICES[*]}" | |||||
| if [[ "${#SHARED_SERVICES[@]}" -gt 0 ]]; then | |||||
| log "Sub services: ${SHARED_SERVICES[*]}" | |||||
| else | |||||
| log "Sub services: none" | |||||
| fi | |||||
| log "=========================================" | |||||
| log "[1/6] Snapshot current master images" | |||||
| node_action snapshot master "${REQUESTED_SERVICES[@]}" | |||||
| log "[2/6] Load release images on master" | |||||
| if ! docker load -i "$IMAGE_TAR"; then | |||||
| node_action restore-tags master "${REQUESTED_SERVICES[@]}" || true | |||||
| die "failed to load release images" | |||||
| fi | |||||
| for service in "${REQUESTED_SERVICES[@]}"; do | |||||
| if ! docker image inspect "emp-$service:$IMAGE_TAG" >/dev/null 2>&1; then | |||||
| node_action restore-tags master "${REQUESTED_SERVICES[@]}" || true | |||||
| die "release image not found after docker load: emp-$service:$IMAGE_TAG" | |||||
| fi | |||||
| done | |||||
| REMOTE_UPDATED=0 | |||||
| if [[ "${#SHARED_SERVICES[@]}" -gt 0 && "$SKIP_REMOTE_SYNC" != "1" ]]; then | |||||
| log "[3/6] Export and transfer shared images to sub server" | |||||
| SHARED_IMAGE_ARCHIVE="$(mktemp "/tmp/emp-shunfeng-${RUN_ID}-XXXXXX")" | |||||
| SHARED_IMAGES=() | |||||
| for service in "${SHARED_SERVICES[@]}"; do | |||||
| SHARED_IMAGES+=("emp-$service:$IMAGE_TAG" "emp-$service:latest") | |||||
| done | |||||
| if ! docker save -o "$SHARED_IMAGE_ARCHIVE" "${SHARED_IMAGES[@]}"; then | |||||
| node_action restore-tags master "${REQUESTED_SERVICES[@]}" || true | |||||
| die "failed to export shared images" | |||||
| fi | |||||
| REMOTE_ARCHIVE="/tmp/emp-shunfeng-${RUN_ID}.tar" | |||||
| REMOTE_HELPER="/tmp/emp-shunfeng-node-${RUN_ID}.sh" | |||||
| if ! scp_file "$SHARED_IMAGE_ARCHIVE" "$REMOTE_ARCHIVE" \ | |||||
| || ! scp_file "$NODE_HELPER" "$REMOTE_HELPER" \ | |||||
| || ! ssh_cmd "chmod 700 $(printf '%q' "$REMOTE_HELPER")"; then | |||||
| node_action restore-tags master "${REQUESTED_SERVICES[@]}" || true | |||||
| cleanup_remote_files | |||||
| die "failed to transfer release files to sub server" | |||||
| fi | |||||
| log "[4/6] Update and verify sub server" | |||||
| if ! remote_action apply "$REMOTE_ARCHIVE" "${SHARED_SERVICES[@]}"; then | |||||
| remote_action rollback "" "${SHARED_SERVICES[@]}" || true | |||||
| node_action restore-tags master "${REQUESTED_SERVICES[@]}" || true | |||||
| cleanup_remote_files | |||||
| die "sub server deployment failed; master containers were not changed" | |||||
| fi | |||||
| REMOTE_UPDATED=1 | |||||
| elif [[ "${#SHARED_SERVICES[@]}" -gt 0 ]]; then | |||||
| log "[3/6] Skip sub server by SKIP_REMOTE_SYNC=1" | |||||
| log "[4/6] Sub server not updated" | |||||
| else | |||||
| log "[3/6] No shared image to transfer" | |||||
| log "[4/6] Sub server not required" | |||||
| fi | |||||
| log "[5/6] Update and verify master server" | |||||
| if ! node_action deploy master "${REQUESTED_SERVICES[@]}"; then | |||||
| log "Master deployment failed, roll back master" | |||||
| node_action rollback master "${REQUESTED_SERVICES[@]}" || true | |||||
| if [[ "$REMOTE_UPDATED" == "1" ]]; then | |||||
| log "Roll back sub server to keep both nodes consistent" | |||||
| remote_action rollback "" "${SHARED_SERVICES[@]}" || true | |||||
| fi | |||||
| cleanup_remote_files | |||||
| die "master server deployment failed" | |||||
| fi | |||||
| log "[6/6] Cleanup temporary transfer files" | |||||
| cleanup_remote_files | |||||
| log "Deployment completed: $IMAGE_TAG" | |||||
| docker ps --filter 'name=emp-' --format 'table {{.Names}}\t{{.Image}}\t{{.Status}}' | |||||
| @@ -0,0 +1,36 @@ | |||||
| # shunfeng 主从发布参数。复制为 release.env 后按现场实际值确认。 | |||||
| # 当前旧发布目录名为 emp_server,Compose 默认项目名通常也是 emp_server。 | |||||
| # 正式迁移前请通过 docker inspect emp-gateway 的 Compose label 核实。 | |||||
| BACKEND_PROJECT_NAME=emp_server | |||||
| # 这份 release.env 主、从服务器都要保存一份,下面三个路径表示当前节点的运行文件。 | |||||
| # 主、从服务器统一使用 /home/emp-shunfeng,避免两套路径。 | |||||
| RUNTIME_DIR=/home/emp-shunfeng/runtime | |||||
| BACKEND_COMPOSE_FILE=/home/emp-shunfeng/runtime/docker-compose.yml | |||||
| BACKEND_ENV_FILE=/home/emp-shunfeng/runtime/.env | |||||
| # 主从服务器。发布命令只在主服务器执行;REMOTE_* 表示主服务器访问从服务器时使用的参数。 | |||||
| PRIMARY_HOST=192.168.0.3 | |||||
| REMOTE_HOST=192.168.0.4 | |||||
| REMOTE_USER=root | |||||
| SSH_PORT=22 | |||||
| REMOTE_DEPLOY_HOME=/home/emp-shunfeng | |||||
| REMOTE_RUNTIME_DIR=/home/emp-shunfeng/runtime | |||||
| SKIP_PRIMARY_HOST_CHECK=0 | |||||
| SKIP_REMOTE_SYNC=0 | |||||
| # Admin 保持原 docker run 参数,不加入 Java Compose。 | |||||
| ADMIN_HOST_PORT=4080 | |||||
| ADMIN_CONTAINER_PORT=80 | |||||
| EMP_ADMIN_LOG_DIR=/data/emp/logs/emp-admin | |||||
| DOCKER_LOG_MAX_SIZE=200m | |||||
| DOCKER_LOG_MAX_FILE=5 | |||||
| # 健康检查。 | |||||
| HEALTH_RETRIES=30 | |||||
| HEALTH_INTERVAL_SECONDS=2 | |||||
| NACOS_CONTAINER_NAME=emp-nacos | |||||
| # 现网迁移默认要求本次发布的容器已经存在,以保证失败时一定有旧镜像可回滚。 | |||||
| REQUIRE_EXISTING_CONTAINERS=1 | |||||
| @@ -16,13 +16,23 @@ set -Eeuo pipefail | |||||
| SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" | SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" | ||||
| # Keep COS credentials outside the repository. A local .cos.env file is ignored | |||||
| # by Git and lets operators keep using the short build command. | |||||
| COS_ENV_FILE="${COS_ENV_FILE:-$SCRIPT_DIR/.cos.env}" | |||||
| if [[ -f "$COS_ENV_FILE" ]]; then | |||||
| set -a | |||||
| # shellcheck disable=SC1090 | |||||
| . "$COS_ENV_FILE" | |||||
| set +a | |||||
| fi | |||||
| DEPLOY_ENV="${DEPLOY_ENV:-emp-test}" | DEPLOY_ENV="${DEPLOY_ENV:-emp-test}" | ||||
| PACKAGE_KIND="${PACKAGE_KIND:-}" | PACKAGE_KIND="${PACKAGE_KIND:-}" | ||||
| COSCLI_BIN="${COSCLI_BIN:-coscli}" | COSCLI_BIN="${COSCLI_BIN:-coscli}" | ||||
| COS_SIGN_EXPIRE="${COS_SIGN_EXPIRE:-604800}" | COS_SIGN_EXPIRE="${COS_SIGN_EXPIRE:-604800}" | ||||
| TARGET_BASE_DIR="${TARGET_BASE_DIR:-/home/admin-x99/emp}" | TARGET_BASE_DIR="${TARGET_BASE_DIR:-/home/admin-x99/emp}" | ||||
| COS_SECRET_ID="${COS_SECRET_ID:-AKIDynPmvra6PTIsypSr24uXxFKDtPipkUI0}" | |||||
| COS_SECRET_KEY="${COS_SECRET_KEY:-Zv76sSq21oPq45vY0GJCuQjrnY6II3aF}" | |||||
| COS_SECRET_ID="${COS_SECRET_ID:-}" | |||||
| COS_SECRET_KEY="${COS_SECRET_KEY:-}" | |||||
| COS_REGION="${COS_REGION:-ap-chengdu}" | COS_REGION="${COS_REGION:-ap-chengdu}" | ||||
| COS_BUCKET="${COS_BUCKET:-emp-1258090764}" | COS_BUCKET="${COS_BUCKET:-emp-1258090764}" | ||||
| @@ -63,7 +73,11 @@ infer_package_kind() { | |||||
| print_target_deploy_command() { | print_target_deploy_command() { | ||||
| local target_deploy_home target_script_dir | local target_deploy_home target_script_dir | ||||
| target_deploy_home="${TARGET_DEPLOY_HOME:-$TARGET_BASE_DIR/$DEPLOY_ENV}" | |||||
| if [[ "$DEPLOY_ENV" == "emp-shunfeng" ]]; then | |||||
| target_deploy_home="${TARGET_DEPLOY_HOME:-/home/emp-shunfeng}" | |||||
| else | |||||
| target_deploy_home="${TARGET_DEPLOY_HOME:-$TARGET_BASE_DIR/$DEPLOY_ENV}" | |||||
| fi | |||||
| if [[ "$PACKAGE_KIND" == "update" ]]; then | if [[ "$PACKAGE_KIND" == "update" ]]; then | ||||
| target_script_dir="${TARGET_SCRIPT_DIR:-$target_deploy_home/runtime}" | target_script_dir="${TARGET_SCRIPT_DIR:-$target_deploy_home/runtime}" | ||||
| @@ -142,8 +142,8 @@ PDF_FRONTEND_BASE_URL=http://1.14.103.234:37361 | |||||
| # 第三方配置,按需填写 | # 第三方配置,按需填写 | ||||
| # ----------------------------------------------------------------------------- | # ----------------------------------------------------------------------------- | ||||
| AMAP_KEY= | AMAP_KEY= | ||||
| COS_SECRET_ID=AKIDynPmvra6PTIsypSr24uXxFKDtPipkUI0 | |||||
| COS_SECRET_KEY=Zv76sSq21oPq45vY0GJCuQjrnY6II3aF | |||||
| COS_SECRET_ID=change-me | |||||
| COS_SECRET_KEY=change-me | |||||
| COS_REGION=ap-chengdu | COS_REGION=ap-chengdu | ||||
| COS_BUCKET=emp-1258090764 | COS_BUCKET=emp-1258090764 | ||||