You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
266 lines
14 KiB
266 lines
14 KiB
#!/usr/bin/env bash
|
|
# 生产环境部署:选择「应用 + 区域 + 镜像版本 tag」部署,支持回滚。
|
|
#
|
|
# 镜像由 ./prod-build.sh 预先构建推送;本脚本只做「远端拉取 + 重启」。
|
|
# 所有应用共用同一镜像 starpivot-app 与本目录的模板,仅「远端运行目录」按应用区分(见下方 APPS)。
|
|
# 各区域一套服务器(见下方 region_profile)。
|
|
# 仅做镜像部署:服务器上的 docker-compose.yml / .env / confs/ 全部由你自行维护,本脚本只下发 *.example 模板、绝不覆盖真实文件
|
|
# (首次照同步来的 docker-compose.yml.example / env.example / confs/*.yaml.example 在各应用远端目录复制成真实文件并填值)。
|
|
#
|
|
# 用法(在 deploy/voitrans/ 下):
|
|
# ./prod-deploy.sh # 不带参数 → 交互式引导:逐步选「应用 / 区域 / 版本」,部署前确认
|
|
# ./prod-deploy.sh <应用> <区域> <tag> # 直接部署,如 ./prod-deploy.sh voitrans shanghai 0.1.1
|
|
# ./prod-deploy.sh <应用> <区域> rollback # 回滚到上一次部署版本(与当前版本来回切换)
|
|
# ./prod-deploy.sh <区域> <tag> # 省略应用 → 默认首个应用(兼容旧写法)
|
|
# ./prod-deploy.sh -h # 查看用法
|
|
# 应用: 见 APPS(默认 voitrans) 区域: shanghai | japan | singapore
|
|
set -euo pipefail
|
|
|
|
SVC_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
|
|
REPO_ROOT="$(cd "$SVC_DIR/../.." && pwd)"
|
|
# 各公司镜像仓库地址/账号统一放这里,换仓库只改 deploy/registry-profiles.sh
|
|
source "$REPO_ROOT/deploy/registry-profiles.sh"
|
|
|
|
blue() { printf "\033[34m%s\033[0m\n" "$*"; }
|
|
green() { printf "\033[32m%s\033[0m\n" "$*"; }
|
|
red() { printf "\033[31m%s\033[0m\n" "$*"; }
|
|
|
|
# ── 私有仓库凭据(写死;公开仓库前清空 REGISTRY_PASS)──
|
|
# 镜像仓库按公司选择:COMPANY=lingpu ./prod-deploy.sh …(默认 yunyan)
|
|
# 必须与 prod-build.sh 构建时所选公司一致,否则拉不到镜像。
|
|
COMPANY="${COMPANY:-$DEFAULT_COMPANY}"
|
|
registry_profile "$COMPANY" || exit 1
|
|
# 共用镜像:所有应用都用这一个。名字走 prod_image_name(),与 prod-build.sh 同源,防漂移。
|
|
IMAGE_NAME="$(prod_image_name "$COMPANY" app)" || exit 1
|
|
DOCKER_NETWORK="1panel-network" # compose 外部网络名(与服务器一致)
|
|
|
|
# ╔══════════════════════════════════════════════════════════════════════╗
|
|
# ║ 应用列表 —— 共用镜像 starpivot-app 与本目录模板,仅「远端部署目录」按应用区分。 ║
|
|
# ║ 格式: "key|显示名|远端部署目录" ║
|
|
# ║ 新增应用:在此加一行(远端目录在目标服务器上自行备好 .env/confs/compose)。 ║
|
|
# ╚══════════════════════════════════════════════════════════════════════╝
|
|
APPS=(
|
|
"voitrans|Voitrans|/home/work/voitrans-a11"
|
|
"lumi|Lumi|/home/work/lumi-a11"
|
|
"echomeet|EchoMeet|/home/work/echomeet-a11"
|
|
"deapvoice|DeapVoice|/home/work/deapvoice-a11"
|
|
"deapsound|DeapSound|/home/work/deapsound-a11"
|
|
"allhelp|AllHelp|/home/work/allhelp-a11"
|
|
"deepglass|DeepGlass|/home/work/deepglass-a11"
|
|
)
|
|
app_keys() { local e k; for e in "${APPS[@]}"; do IFS='|' read -r k _ <<<"$e"; printf '%s\n' "$k"; done; }
|
|
app_field() { # $1=key $2=字段序号(2=显示名 3=远端目录)
|
|
local e k n r
|
|
for e in "${APPS[@]}"; do
|
|
IFS='|' read -r k n r <<<"$e"
|
|
[ "$k" = "$1" ] || continue
|
|
case "$2" in 2) printf '%s' "$n" ;; 3) printf '%s' "$r" ;; esac
|
|
return 0
|
|
done
|
|
return 1
|
|
}
|
|
is_app() { app_field "$1" 2 >/dev/null 2>&1; }
|
|
DEFAULT_APP="${APPS[0]%%|*}" # 省略应用时的默认(首个);用参数展开取首行 key,避开 head 截断管道触发 SIGPIPE
|
|
|
|
# ╔══════════════════════════════════════════════════════════════════════╗
|
|
# ║ 各区域部署目标 —— 每区域一整套 HOST/PORT/USER/KEY(IP 与 SSH 私钥各不相同)。║
|
|
# ║ 写死在此(本文件纳入 git)。公开仓库前清空 HOST/KEY。 ║
|
|
# ║ 新增区域:在 region_profile 加一段 case,并把区域名加进 REGIONS 数组。 ║
|
|
# ╚══════════════════════════════════════════════════════════════════════╝
|
|
REGIONS=(shanghai japan singapore) # 可选环境/区域(交互菜单按此顺序展示)
|
|
|
|
region_profile() { # 远端部署目录由「应用」决定(见 APPS),此处只管各区域服务器连接信息
|
|
case "$REGION" in
|
|
shanghai)
|
|
DEPLOY_HOST="139.224.212.237" # ← 上海生产服务器 IP(必填)
|
|
DEPLOY_PORT=22
|
|
DEPLOY_USER=root
|
|
DEPLOY_KEY=/Users/liwei/Documents/key/aliyun_shanghai.pem # ← 上海服务器 SSH 私钥(各区域不同)
|
|
;;
|
|
japan)
|
|
DEPLOY_HOST="8.216.34.101" # ← 日本生产服务器 IP(必填)
|
|
DEPLOY_PORT=22
|
|
DEPLOY_USER=root
|
|
DEPLOY_KEY=/Users/liwei/Documents/key/aliyun_japan.pem # ← 日本服务器 SSH 私钥(各区域不同)
|
|
;;
|
|
singapore)
|
|
DEPLOY_HOST="47.84.198.24" # ← 新加坡生产服务器 IP(必填)
|
|
DEPLOY_PORT=22
|
|
DEPLOY_USER=root
|
|
DEPLOY_KEY=/Users/liwei/Documents/key/alibaba_singapore.pem # ← 新加坡服务器 SSH 私钥(各区域不同)
|
|
;;
|
|
*) red "✗ 未知区域: ${REGION}(应为 ${REGIONS[*]// /|})"; exit 1 ;;
|
|
esac
|
|
}
|
|
|
|
# 取某区域的目标主机(供交互菜单展示),不污染当前环境变量
|
|
region_host_of() { ( REGION="$1" region_profile >/dev/null 2>&1; printf '%s' "${DEPLOY_HOST:-}" ); }
|
|
|
|
# ── 用法 ──
|
|
usage() {
|
|
blue "生产部署(共用镜像 ${IMAGE_NAME},仅远端目录按应用区分)"
|
|
cat >&2 <<EOF
|
|
用法: ./prod-deploy.sh <应用> <环境/区域> <tag|rollback>
|
|
应用 : $(app_keys | paste -sd '/' -)
|
|
环境/区域 : ${REGIONS[*]// /, }
|
|
tag : 由 ./prod-build.sh <tag> 预先构建推送的版本号,如 0.1.1
|
|
rollback : 回滚到上一次部署版本(与当前版本来回切换)
|
|
|
|
示例:
|
|
./prod-deploy.sh voitrans shanghai 0.1.1 # 部署 voitrans 0.1.1 到上海
|
|
./prod-deploy.sh voitrans japan rollback # voitrans 日本回滚到上一版本
|
|
./prod-deploy.sh shanghai 0.1.1 # 省略应用 → 默认首个应用(${DEFAULT_APP:-?})
|
|
|
|
提示: 不带参数(或参数不全)运行将进入交互式引导,逐步选择应用 / 环境 / 版本。
|
|
EOF
|
|
}
|
|
|
|
# ── 参数解析:<应用> <区域> <tag|rollback>;省略应用兼容旧式 <区域> <tag>(默认首个应用)──
|
|
case "${1:-}" in -h|--help|help) usage; exit 0 ;; esac
|
|
|
|
ARGS=("$@")
|
|
APP_KEY=""; REGION=""; ARG2=""; INTERACTIVE=0
|
|
|
|
if [ "${#ARGS[@]}" -gt 0 ] && is_app "${ARGS[0]:-}"; then
|
|
APP_KEY="${ARGS[0]}"; REGION="${ARGS[1]:-}"; ARG2="${ARGS[2]:-}"
|
|
else
|
|
REGION="${ARGS[0]:-}"; ARG2="${ARGS[1]:-}" # 旧式:省略应用
|
|
fi
|
|
|
|
# 缺任一项就进交互(仅当不是「旧式两参数」这种完整非交互调用)
|
|
enter_interactive() { [ "$INTERACTIVE" = "1" ] && return 0; INTERACTIVE=1; usage; echo >&2; }
|
|
|
|
# ① 选择应用
|
|
if [ -z "$APP_KEY" ]; then
|
|
if [ -n "$REGION" ] && [ -n "$ARG2" ]; then
|
|
APP_KEY="$DEFAULT_APP" # 旧式两参数:默认首个应用,不打断
|
|
else
|
|
enter_interactive
|
|
blue "① 选择应用:"
|
|
i=1
|
|
while IFS= read -r k; do
|
|
printf " %d) %-12s → 远端目录 %s\n" "$i" "$k" "$(app_field "$k" 3)" >&2
|
|
i=$((i+1))
|
|
done < <(app_keys)
|
|
napp="$(app_keys | wc -l | tr -d ' ')"
|
|
printf " 请输入序号 [1-%s]: " "$napp" >&2
|
|
read -r sel || { red "✗ 已取消"; exit 1; }
|
|
case "$sel" in ''|*[!0-9]*) red "✗ 无效序号: $sel"; exit 1 ;; esac
|
|
{ [ "$sel" -ge 1 ] && [ "$sel" -le "$napp" ]; } || { red "✗ 序号超出范围"; exit 1; }
|
|
APP_KEY="$(app_keys | sed -n "${sel}p")"
|
|
green " ✓ 应用: $APP_KEY"
|
|
fi
|
|
fi
|
|
is_app "$APP_KEY" || { red "✗ 未知应用: ${APP_KEY}(应为 $(app_keys | paste -sd '/' -))"; exit 1; }
|
|
|
|
# 解析应用 → 远端部署目录(镜像与模板共用 voitrans 这套)
|
|
APP_DISPLAY="$(app_field "$APP_KEY" 2)"
|
|
DEPLOY_DIR="$(app_field "$APP_KEY" 3)"
|
|
|
|
# ② 选择环境 / 区域
|
|
if [ -z "$REGION" ]; then
|
|
enter_interactive
|
|
blue "② 选择部署环境 / 区域:"
|
|
i=1
|
|
for r in "${REGIONS[@]}"; do
|
|
h="$(region_host_of "$r")"
|
|
printf " %d) %-10s → %s\n" "$i" "$r" "${h:-(未配置 IP,需先在脚本 region_profile 填)}" >&2
|
|
i=$((i+1))
|
|
done
|
|
printf " 请输入序号 [1-%d]: " "${#REGIONS[@]}" >&2
|
|
read -r sel || { red "✗ 已取消"; exit 1; }
|
|
case "$sel" in ''|*[!0-9]*) red "✗ 无效序号: $sel"; exit 1 ;; esac
|
|
{ [ "$sel" -ge 1 ] && [ "$sel" -le "${#REGIONS[@]}" ]; } || { red "✗ 序号超出范围"; exit 1; }
|
|
REGION="${REGIONS[$((sel-1))]}"
|
|
green " ✓ 环境: $REGION"
|
|
fi
|
|
|
|
# ③ 选择版本 / 动作
|
|
if [ -z "$ARG2" ]; then
|
|
enter_interactive
|
|
echo >&2
|
|
blue "③ 选择版本(输入由 prod-build.sh 推送的 tag,或输入 rollback 回滚):"
|
|
printf " tag 或 rollback: " >&2
|
|
read -r ARG2 || { red "✗ 已取消"; exit 1; }
|
|
[ -n "$ARG2" ] || { red "✗ 未输入版本"; exit 1; }
|
|
fi
|
|
|
|
if [ "$ARG2" = "rollback" ]; then MODE="rollback"; NEW_TAG=""; else MODE="deploy"; NEW_TAG="$ARG2"; fi
|
|
|
|
# deploy 模式校验 tag:去空白 + 非空 + 合法字符(避免空/空格 tag 让远端 docker 报 invalid reference format)
|
|
if [ "$MODE" = "deploy" ]; then
|
|
NEW_TAG="$(printf '%s' "$NEW_TAG" | tr -d '[:space:]')"
|
|
[ -n "$NEW_TAG" ] || { red "✗ 未指定版本 tag —— deploy 必须带版本号(如 0.1.1,需先 ./prod-build.sh 0.1.1 构建推送);回滚请用 rollback"; exit 1; }
|
|
case "$NEW_TAG" in
|
|
*[!A-Za-z0-9._-]*) red "✗ 非法版本 tag: '$NEW_TAG'(只能含字母数字与 . _ -)"; exit 1 ;;
|
|
esac
|
|
fi
|
|
|
|
region_profile
|
|
: "${DEPLOY_HOST:?请在 prod-deploy.sh 的 region_profile($REGION) 段填该区域 IP}"
|
|
: "${DEPLOY_KEY:?请在 prod-deploy.sh 的 region_profile($REGION) 段填该区域 SSH 私钥}"
|
|
|
|
# 交互模式下,部署前二次确认(带全参数的非交互/CI 调用不打断)
|
|
if [ "$INTERACTIVE" = "1" ]; then
|
|
echo >&2
|
|
blue "即将部署 → 应用: $APP_DISPLAY($APP_KEY) 镜像: $IMAGE_NAME 环境: $REGION 目标: $DEPLOY_USER@$DEPLOY_HOST:$DEPLOY_DIR 动作: $MODE${NEW_TAG:+ $NEW_TAG}"
|
|
printf "确认执行?[y/N]: " >&2
|
|
read -r yn || true
|
|
case "$yn" in [yY]|[yY][eE][sS]) ;; *) red "已取消"; exit 0 ;; esac
|
|
fi
|
|
|
|
_ssh() { ssh -p "$DEPLOY_PORT" -i "${DEPLOY_KEY/#\~/$HOME}" -o StrictHostKeyChecking=accept-new "$DEPLOY_USER@$DEPLOY_HOST" "$@"; }
|
|
_scp() { scp -P "$DEPLOY_PORT" -i "${DEPLOY_KEY/#\~/$HOME}" -o StrictHostKeyChecking=accept-new "$@"; }
|
|
_dest() { echo "$DEPLOY_USER@$DEPLOY_HOST:$1"; }
|
|
|
|
# 同步「模板」(只传 *.example,绝不覆盖你在服务器维护的真实 docker-compose.yml / .env / confs/*.yaml)
|
|
blue ">>> [$APP_KEY/$REGION] 同步模板 → $DEPLOY_USER@$DEPLOY_HOST:$DEPLOY_DIR"
|
|
_ssh "mkdir -p '$DEPLOY_DIR/confs' '$DEPLOY_DIR/log'"
|
|
_scp "$SVC_DIR/docker-compose.yml" "$(_dest "$DEPLOY_DIR/docker-compose.yml.example")"
|
|
_scp "$SVC_DIR"/confs/*.yaml.example "$(_dest "$DEPLOY_DIR/confs/")"
|
|
_scp "$SVC_DIR/env/env.example" "$(_dest "$DEPLOY_DIR/env.example")"
|
|
|
|
# 远端拉取并重启(不上传真实配置):版本历史存独立的 .deploy_tag;部署成功后会把当前 TAG 写回 .env 的 TAG 行(仅这一行)。
|
|
blue ">>> [$APP_KEY/$REGION] 远端部署($MODE${NEW_TAG:+ $NEW_TAG}) → $DEPLOY_USER@$DEPLOY_HOST:$DEPLOY_DIR"
|
|
_ssh bash -s -- "$DEPLOY_DIR" "$MODE" "$NEW_TAG" "$REGISTRY/$IMAGE_NAME" "$REGISTRY" "$REGISTRY_USER" "$REGISTRY_PASS" "$DOCKER_NETWORK" "$IMAGE_NAME" <<'REMOTE'
|
|
set -e
|
|
DIR="$1"; MODE="$2"; NEW_TAG="$3"; IMG_PREFIX="$4"; REG="$5"; REG_USER="$6"; REG_PASS="$7"; NET="$8"; IMG_NAME="${9}"
|
|
|
|
[ -f "$DIR/.env" ] || { echo "✗ 缺少 $DIR/.env(请照 env.example 在服务器填好真实 .env)"; exit 1; }
|
|
[ -f "$DIR/confs/gateway.yaml" ] || { echo "✗ 缺少 $DIR/confs/*.yaml(请照 confs/*.yaml.example 在服务器填好)"; exit 1; }
|
|
[ -f "$DIR/docker-compose.yml" ] || { echo "✗ 缺少 $DIR/docker-compose.yml(请照 docker-compose.yml.example 复制为 docker-compose.yml)"; exit 1; }
|
|
|
|
STATE="$DIR/.deploy_tag" # 当前部署 tag(独立文件,记录版本历史)
|
|
PREVF="$DIR/.deploy_tag.prev" # 上一次部署 tag
|
|
CUR_TAG="$([ -f "$STATE" ] && cat "$STATE" || true)"
|
|
CUR_PREV="$([ -f "$PREVF" ] && cat "$PREVF" || true)"
|
|
|
|
if [ "$MODE" = "rollback" ]; then
|
|
[ -n "$CUR_PREV" ] || { echo "没有上一次版本记录(.deploy_tag.prev),无法回滚"; exit 1; }
|
|
FINAL_TAG="$CUR_PREV"; FINAL_PREV="$CUR_TAG" # 与当前版本来回切换
|
|
else
|
|
FINAL_TAG="$NEW_TAG"
|
|
if [ -n "$CUR_TAG" ] && [ "$CUR_TAG" != "$NEW_TAG" ]; then FINAL_PREV="$CUR_TAG"; else FINAL_PREV="$CUR_PREV"; fi
|
|
fi
|
|
|
|
echo "$REG_PASS" | docker login "$REG" -u "$REG_USER" --password-stdin >/dev/null
|
|
docker network inspect "$NET" >/dev/null 2>&1 || docker network create "$NET"
|
|
docker pull "$IMG_PREFIX:$FINAL_TAG"
|
|
# REGISTRY/IMAGE_NAME/TAG 通过 inline 环境变量传给 compose(优先级高于 --env-file),
|
|
# 确保 compose 用的镜像和上面 docker pull 的完全是同一个——两者曾经漂移过(pull voitrans-a11、compose 找 starpivot-app)
|
|
cd "$DIR" && REGISTRY="$REG" IMAGE_NAME="$IMG_NAME" TAG="$FINAL_TAG" docker compose --env-file .env up -d --force-recreate
|
|
docker image prune -f
|
|
|
|
# 记录当前运行版本:① 写回 .env 的 TAG 行(只改这一行,便于 cat .env 查看)② 维护 .deploy_tag 历史
|
|
if grep -q '^TAG=' "$DIR/.env"; then
|
|
sed -i "s/^TAG=.*/TAG=$FINAL_TAG/" "$DIR/.env"
|
|
else
|
|
printf 'TAG=%s\n' "$FINAL_TAG" >> "$DIR/.env"
|
|
fi
|
|
echo "$FINAL_TAG" > "$STATE"
|
|
[ -n "$FINAL_PREV" ] && echo "$FINAL_PREV" > "$PREVF" || true
|
|
echo "已部署版本 TAG=$FINAL_TAG (上一次=${FINAL_PREV:-无});已写回 .env 的 TAG 行"
|
|
REMOTE
|
|
|
|
green "✓ [$APP_KEY/$REGION] $MODE 完成"
|
|
|