소스 검색

shunfeng发布脚本

master
leiyun 1 개월 전
부모
커밋
3c2e585538
12개의 변경된 파일1060개의 추가작업 그리고 12개의 파일을 삭제
  1. +11
    -0
      isolated/.cos.env.example
  2. +13
    -1
      isolated/README.md
  3. +59
    -6
      isolated/build-update.sh
  4. +141
    -0
      isolated/profiles/emp-shunfeng/README.md
  5. +39
    -0
      isolated/profiles/emp-shunfeng/build-runtime-package.sh
  6. +83
    -0
      isolated/profiles/emp-shunfeng/docker-compose.yml
  7. +95
    -0
      isolated/profiles/emp-shunfeng/install-runtime.sh
  8. +286
    -0
      isolated/profiles/emp-shunfeng/package-files/apply-node-update.sh
  9. +278
    -0
      isolated/profiles/emp-shunfeng/package-files/apply-update.sh
  10. +36
    -0
      isolated/profiles/emp-shunfeng/release.env.example
  11. +17
    -3
      isolated/publish-cos.sh
  12. +2
    -2
      isolated/test/env.txt

+ 11
- 0
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

+ 13
- 1
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/<DEPLOY_ENV>/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


+ 59
- 6
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 <<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

Next steps on target server:


+ 141
- 0
isolated/profiles/emp-shunfeng/README.md 파일 보기

@@ -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 下载命令;主服务器会自动协调从服务器,无需在从服务器手工重复发布。

+ 39
- 0
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

+ 83
- 0
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

+ 95
- 0
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"

+ 286
- 0
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

+ 278
- 0
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}}'

+ 36
- 0
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

+ 17
- 3
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}"


+ 2
- 2
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



불러오는 중...
취소
저장