Files
baiyue-firstleg/deploy/scripts/deploy-blue-green-service.sh
T
2026-09-20 20:53:20 +08:00

241 lines
8.0 KiB
Bash
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env sh
set -eu
if [ "$#" -ne 7 ]; then
echo "用法: $0 <环境> <镜像仓库> <镜像标签> <服务> <环境文件> <Compose文件> <项目名>" >&2
exit 2
fi
DEPLOY_ENV="$1"
IMAGE_REPOSITORY="$2"
IMAGE_TAG="$3"
SERVICE="$4"
ENV_FILE="$5"
COMPOSE_FILE="$6"
BASE_PROJECT="$7"
case "$DEPLOY_ENV" in
production) ;;
*) echo "蓝绿执行器仅用于 production,当前: $DEPLOY_ENV" >&2; exit 2 ;;
esac
for file in "$ENV_FILE" "$COMPOSE_FILE"; do
[ -f "$file" ] || { echo "必需文件不存在: $file" >&2; exit 2; }
done
case "$SERVICE" in
*[!A-Za-z0-9_-]*|'') echo "非法服务名: $SERVICE" >&2; exit 2 ;;
esac
env_value() {
sed -n "s/^$1=//p" "$ENV_FILE" | tail -n 1 | tr -d '\r'
}
service_contract() {
case "$SERVICE:$COMPOSE_FILE" in
gateway:*) echo "gateway|9000|9000|gateway" ;;
auth:*) echo "auth|9100|9100|auth-service" ;;
iam:*) echo "iam|9201|9201|iam-service" ;;
firstleg:*) echo "business|9202|9202|firstleg-service" ;;
finance:*) echo "finance|9203|9203|finance-service" ;;
order:*) echo "order|9204|9204|order-service" ;;
warehouse:*) echo "warehouse|9205|9205|warehouse-service" ;;
file:*) echo "file|9206|9206|file-service" ;;
print:*) echo "print|9207|9207|print-service" ;;
integration-service:*) echo "integration|9210|9210|integration-service" ;;
audit-service:*) echo "audit|9700|9700|audit-service" ;;
gateway-overseas:*) echo "gateway-overseas|9300|9300|gateway-overseas" ;;
lastleg-service:*) echo "lastleg|9303|9303|lastleg-service" ;;
warehouse-overseas-service:*) echo "warehouse-overseas|9305|9305|warehouse-overseas-service" ;;
event-bridge-service:*overseas*) echo "event-bridge-overseas|9312|9313|event-bridge-service" ;;
event-bridge-service:*) echo "event-bridge|9312|9312|event-bridge-service" ;;
*) echo "不支持蓝绿部署的服务: $SERVICE" >&2; exit 2 ;;
esac
}
IFS='|' read -r CONTAINER_STEM INTERNAL_PORT BASE_PORT NACOS_SERVICE <<EOF
$(service_contract)
EOF
STATE_ROOT="${BLUE_GREEN_STATE_ROOT:-/data/firstleg/deploy/blue-green-state}"
STATE_KEY="${BASE_PROJECT}-${SERVICE}"
STATE_FILE="$STATE_ROOT/$STATE_KEY.active"
ROUTING_NETWORK="${BLUE_GREEN_ROUTING_NETWORK:-firstleg-${DEPLOY_ENV}-routing}"
HEALTH_TIMEOUT="${DEPLOY_HEALTH_TIMEOUT_SECONDS:-240}"
HEALTH_POLL="${DEPLOY_HEALTH_POLL_SECONDS:-5}"
STABILIZATION_SECONDS="${BLUE_GREEN_STABILIZATION_SECONDS:-10}"
STOP_TIMEOUT="${BLUE_GREEN_STOP_TIMEOUT_SECONDS:-45}"
IMAGE="$IMAGE_REPOSITORY/$SERVICE:$IMAGE_TAG"
for value in "$HEALTH_TIMEOUT" "$HEALTH_POLL" "$STABILIZATION_SECONDS" "$STOP_TIMEOUT"; do
case "$value" in ''|*[!0-9]*) echo "蓝绿时间参数必须为非负整数: $value" >&2; exit 2 ;; esac
done
mkdir -p "$STATE_ROOT"
chmod 750 "$STATE_ROOT"
docker network inspect "$ROUTING_NETWORK" >/dev/null 2>&1 || docker network create "$ROUTING_NETWORK" >/dev/null
legacy_container="firstleg-${DEPLOY_ENV}-${CONTAINER_STEM}"
active_slot=""
active_container=""
active_port=""
if [ -s "$STATE_FILE" ]; then
IFS='|' read -r active_slot active_container active_port active_image < "$STATE_FILE"
elif docker inspect "$legacy_container" >/dev/null 2>&1; then
active_slot="legacy"
active_container="$legacy_container"
active_port="$BASE_PORT"
fi
if [ "$active_slot" = "blue" ]; then
target_slot="green"
else
target_slot="blue"
[ "$active_slot" = "legacy" ] && target_slot="green"
fi
if [ "$target_slot" = "blue" ]; then
target_port="$BASE_PORT"
else
target_port=$((BASE_PORT + 10000))
fi
candidate="firstleg-${DEPLOY_ENV}-${CONTAINER_STEM}-${target_slot}"
if docker inspect "$candidate" >/dev/null 2>&1; then
if [ "$candidate" = "$active_container" ]; then
echo "活动槽状态异常,候选容器与活动容器相同: $candidate" >&2
exit 1
fi
docker rm -f "$candidate" >/dev/null
fi
compose() {
docker compose --project-name "$BASE_PROJECT" --env-file "$ENV_FILE" -f "$COMPOSE_FILE" "$@"
}
candidate_health() {
docker inspect --format '{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}' \
"$candidate" 2>/dev/null || echo missing
}
cleanup_candidate() {
docker rm -f "$candidate" >/dev/null 2>&1 || true
}
wait_for_candidate() {
elapsed=0
while [ "$elapsed" -lt "$HEALTH_TIMEOUT" ]; do
state="$(candidate_health)"
echo "候选槽 $target_slot 健康状态: $state(已等待 ${elapsed}s)"
case "$state" in
healthy) return 0 ;;
unhealthy|exited|dead|missing) return 1 ;;
esac
sleep "$HEALTH_POLL"
elapsed=$((elapsed + HEALTH_POLL))
done
return 1
}
connect_routing_alias() {
alias_name="$1"
container="$2"
docker network inspect "$ROUTING_NETWORK" --format '{{json .Containers}}' | grep -q "\"$container\"" \
&& return 0
docker network connect --alias "$alias_name" "$ROUTING_NETWORK" "$container"
}
bootstrap_legacy_routes() {
for pair in \
gateway:gateway auth:auth iam:iam firstleg:business finance:finance order:order \
warehouse:warehouse file:file print:print integration-service:integration audit-service:audit \
gateway-overseas:gateway-overseas lastleg-service:lastleg \
warehouse-overseas-service:warehouse-overseas event-bridge-cn:event-bridge \
event-bridge-overseas:event-bridge-overseas
do
alias_name="${pair%%:*}"
stem="${pair#*:}"
container="firstleg-${DEPLOY_ENV}-${stem}"
if docker inspect "$container" >/dev/null 2>&1; then
connect_routing_alias "$alias_name" "$container" || true
fi
done
}
switch_gateway_proxy() {
[ "$SERVICE" = "gateway" ] || [ "$SERVICE" = "gateway-overseas" ] || return 0
if [ "$SERVICE" = "gateway" ]; then
config="$(env_value GATEWAY_BLUE_GREEN_NGINX_CONFIG)"
else
config="$(env_value GATEWAY_OVERSEAS_BLUE_GREEN_NGINX_CONFIG)"
fi
if [ -z "$config" ] || [ ! -f "$config" ]; then
echo "$SERVICE 蓝绿切换要求环境文件配置有效的 Nginx 配置路径" >&2
return 1
fi
backup="$config.blue-green-backup"
cp -f "$config" "$backup"
if ! sed -i "s#127\.0\.0\.1:${active_port:-$BASE_PORT}#127.0.0.1:$target_port#g" "$config" \
|| cmp -s "$config" "$backup"; then
cp -f "$backup" "$config"
echo "未能在 Nginx 配置中切换 $SERVICE 上游端口" >&2
return 1
fi
if ! nginx -t || ! nginx -s reload; then
cp -f "$backup" "$config"
nginx -t && nginx -s reload || true
return 1
fi
rm -f "$backup"
}
bootstrap_legacy_routes
export DEPLOY_ENV IMAGE_REPOSITORY IMAGE_TAG
echo "启动候选槽: service=$SERVICE slot=$target_slot port=$target_port image=$IMAGE"
if ! compose run -d --no-deps --name "$candidate" \
-e "BAIYUE_DEPLOY_SLOT=$target_slot" -e "BAIYUE_RELEASE_ID=$IMAGE_TAG" \
-p "${PUBLISHED_BIND_ADDRESS:-0.0.0.0}:$target_port:$INTERNAL_PORT" "$SERVICE" >/dev/null; then
cleanup_candidate
exit 1
fi
if ! wait_for_candidate; then
echo "候选槽未通过健康检查,保留旧实例" >&2
docker logs --tail 200 "$candidate" 2>&1 || true
cleanup_candidate
exit 1
fi
running_image="$(docker inspect --format '{{.Config.Image}}' "$candidate")"
if [ "$running_image" != "$IMAGE" ]; then
echo "候选镜像不一致: expected=$IMAGE actual=$running_image" >&2
cleanup_candidate
exit 1
fi
connect_routing_alias "$SERVICE" "$candidate"
[ "$NACOS_SERVICE" = "$SERVICE" ] || connect_routing_alias "$NACOS_SERVICE" "$candidate"
if [ "$STABILIZATION_SECONDS" -gt 0 ]; then
echo "候选槽稳定观察 ${STABILIZATION_SECONDS}s"
sleep "$STABILIZATION_SECONDS"
[ "$(candidate_health)" = "healthy" ] || { cleanup_candidate; exit 1; }
fi
if ! switch_gateway_proxy; then
cleanup_candidate
exit 1
fi
if [ -n "$active_container" ] && docker inspect "$active_container" >/dev/null 2>&1; then
echo "优雅停止旧实例: $active_container"
docker stop --time "$STOP_TIMEOUT" "$active_container" >/dev/null
docker rm "$active_container" >/dev/null
fi
printf '%s|%s|%s|%s\n' "$target_slot" "$candidate" "$target_port" "$IMAGE" > "$STATE_FILE.tmp"
chmod 640 "$STATE_FILE.tmp"
mv -f "$STATE_FILE.tmp" "$STATE_FILE"
echo "蓝绿切换完成: service=$SERVICE active=$target_slot container=$candidate port=$target_port"