diff --git a/isolated/.cos.env.example b/isolated/.cos.env.example new file mode 100644 index 0000000..7cfa774 --- /dev/null +++ b/isolated/.cos.env.example @@ -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 diff --git a/isolated/README.md b/isolated/README.md index 9a952c6..00cc7b9 100644 --- a/isolated/README.md +++ b/isolated/README.md @@ -17,6 +17,7 @@ apply-update.sh 甲方服务器增量更新脚本 profiles/docker-compose.yml emp-test / emp-uat 共用运行模板 profiles/emp-test/.env.example profiles/emp-uat/.env.example +profiles/emp-shunfeng/README.md 顺丰无源码主从增量发布说明 docker-compose.runtime.yml 旧默认运行模板,保留兼容 ``` @@ -33,7 +34,7 @@ profiles//docker-compose.yml ## COS 配置 -打包服务器启用 `COS_UPLOAD=1` 前,需要设置: +打包服务器启用 `COS_UPLOAD=1` 前,可以设置环境变量,或把 `.cos.env.example` 复制为仅保存在受控构建机上的 `.cos.env`: ```bash 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`。 +## 顺丰无源码增量发布 + +顺丰环境只发布 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 全量打包并上传 ```bash diff --git a/isolated/build-update.sh b/isolated/build-update.sh index 5866666..1b7a895 100644 --- a/isolated/build-update.sh +++ b/isolated/build-update.sh @@ -21,6 +21,15 @@ NPM_REGISTRY="${NPM_REGISTRY:-https://registry.npmjs.org}" SKIP_BUILD="${SKIP_BUILD:-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() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" } @@ -35,7 +44,16 @@ need_cmd() { } 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() { @@ -64,7 +82,7 @@ resolve_root_dir() { fi 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() { @@ -85,7 +103,7 @@ compose_service_name() { } image_name() { - echo "$IMAGE_NAMESPACE/emp-$1" + echo "${IMAGE_NAME_PREFIX}emp-$1" } java_module_name() { @@ -193,6 +211,12 @@ build_ws_image() { build_admin_image() { local admin_dir="$ROOT_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" run_step "$admin_dir" pnpm install --frozen-lockfile @@ -202,7 +226,7 @@ build_admin_image() { rm -rf "$context_dir" mkdir -p "$context_dir" 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" run_step "$context_dir" docker build \ @@ -214,6 +238,7 @@ build_admin_image() { prepare_package() { 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_DIR="$DIST_DIR/$PACKAGE_NAME" PACKAGE_ARCHIVE="$DIST_DIR/${PACKAGE_NAME}.tar.gz" @@ -222,9 +247,13 @@ prepare_package() { mkdir -p "$PACKAGE_DIR" 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 cp "$profile_dir/update-env.defaults" "$PACKAGE_DIR/update-env.defaults" fi @@ -237,6 +266,7 @@ prepare_package() { { echo "deploy_env=$DEPLOY_ENV" echo "image_namespace=$IMAGE_NAMESPACE" + echo "image_name_prefix=$IMAGE_NAME_PREFIX" echo "image_tag=$IMAGE_TAG" echo "services=${REQUESTED_SERVICES[*]}" echo "compose_services=$(tr '\n' ' ' < "$PACKAGE_DIR/services.txt")" @@ -278,6 +308,14 @@ need_cmd tar mapfile -t REQUESTED_SERVICES < <(read_requested_services "$@") [[ "${#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)" SERVICES_SLUG="$(IFS=-; echo "${REQUESTED_SERVICES[*]}")" @@ -331,6 +369,21 @@ COS upload finished. Use the "Target deploy command" above on the target server. EOF else + if [[ "$DEPLOY_ENV" == "emp-shunfeng" ]]; then + cat < \ +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 下载命令;主服务器会自动协调从服务器,无需在从服务器手工重复发布。 diff --git a/isolated/profiles/emp-shunfeng/build-runtime-package.sh b/isolated/profiles/emp-shunfeng/build-runtime-package.sh new file mode 100644 index 0000000..69ec789 --- /dev/null +++ b/isolated/profiles/emp-shunfeng/build-runtime-package.sh @@ -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 diff --git a/isolated/profiles/emp-shunfeng/docker-compose.yml b/isolated/profiles/emp-shunfeng/docker-compose.yml new file mode 100644 index 0000000..bf6b631 --- /dev/null +++ b/isolated/profiles/emp-shunfeng/docker-compose.yml @@ -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 diff --git a/isolated/profiles/emp-shunfeng/install-runtime.sh b/isolated/profiles/emp-shunfeng/install-runtime.sh new file mode 100644 index 0000000..1c121e8 --- /dev/null +++ b/isolated/profiles/emp-shunfeng/install-runtime.sh @@ -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" diff --git a/isolated/profiles/emp-shunfeng/package-files/apply-node-update.sh b/isolated/profiles/emp-shunfeng/package-files/apply-node-update.sh new file mode 100644 index 0000000..116ee7d --- /dev/null +++ b/isolated/profiles/emp-shunfeng/package-files/apply-node-update.sh @@ -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 diff --git a/isolated/profiles/emp-shunfeng/package-files/apply-update.sh b/isolated/profiles/emp-shunfeng/package-files/apply-update.sh new file mode 100644 index 0000000..a9f458e --- /dev/null +++ b/isolated/profiles/emp-shunfeng/package-files/apply-update.sh @@ -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}}' diff --git a/isolated/profiles/emp-shunfeng/release.env.example b/isolated/profiles/emp-shunfeng/release.env.example new file mode 100644 index 0000000..25ad102 --- /dev/null +++ b/isolated/profiles/emp-shunfeng/release.env.example @@ -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 diff --git a/isolated/publish-cos.sh b/isolated/publish-cos.sh index 29b58fc..2a311e7 100644 --- a/isolated/publish-cos.sh +++ b/isolated/publish-cos.sh @@ -16,13 +16,23 @@ set -Eeuo pipefail 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}" PACKAGE_KIND="${PACKAGE_KIND:-}" COSCLI_BIN="${COSCLI_BIN:-coscli}" COS_SIGN_EXPIRE="${COS_SIGN_EXPIRE:-604800}" 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_BUCKET="${COS_BUCKET:-emp-1258090764}" @@ -63,7 +73,11 @@ infer_package_kind() { print_target_deploy_command() { 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 target_script_dir="${TARGET_SCRIPT_DIR:-$target_deploy_home/runtime}" diff --git a/isolated/test/env.txt b/isolated/test/env.txt index 779e0b2..44abfff 100644 --- a/isolated/test/env.txt +++ b/isolated/test/env.txt @@ -142,8 +142,8 @@ PDF_FRONTEND_BASE_URL=http://1.14.103.234:37361 # 第三方配置,按需填写 # ----------------------------------------------------------------------------- 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_BUCKET=emp-1258090764