def6ee2363
CI/CD Pipeline / Validate Code Quality And Tests (push) Successful in 46s
CI/CD Pipeline / Unit Tests (push) Successful in 1m40s
CI/CD Pipeline / Frontend Lint (push) Successful in 2m52s
CI/CD Pipeline / Build Production Runtime Images (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Integration Tests (push) Successful in 1m42s
CI/CD Pipeline / Build & Push Staging (Watchtower auto-deploy) (push) Successful in 16m34s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 2m52s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m55s
合并PR #316:新增release.sh / gray_deploy.sh / rollback_gray.sh三个脚本,适配生产环境架构(nginx upstream权重 + canary容器)
228 lines
7.0 KiB
Bash
Executable File
228 lines
7.0 KiB
Bash
Executable File
#!/bin/bash
|
||
# 灰度发布脚本:在生产服务器上启动 canary 版本,通过 Nginx 权重切流
|
||
# 用法: ./scripts/gray_deploy.sh <版本号> <灰度百分比>
|
||
#
|
||
# 前提:
|
||
# - 在生产服务器上执行(或通过 SSH 管道执行)
|
||
# - 当前已有全量运行的 production 容器
|
||
# - Nginx 配置在 /etc/nginx/sites-enabled/00-xiaoxia-saas
|
||
#
|
||
# 灰度范围:API + Web(Worker 暂时全量升级,队列消费无法按比例切流)
|
||
|
||
set -euo pipefail
|
||
|
||
VERSION="${1:-}"
|
||
GRAY_PCT="${2:-10}"
|
||
|
||
if [[ -z "$VERSION" ]]; then
|
||
echo "用法: $0 <版本号> [灰度百分比]"
|
||
echo "示例: $0 v0.1.130 5"
|
||
exit 1
|
||
fi
|
||
|
||
REGISTRY="${REGISTRY:-git.xiaoxiajianji.com/xiaoxia/xiaoxia-saas}"
|
||
NGINX_CONF="${NGINX_CONF:-/etc/nginx/sites-enabled/00-xiaoxia-saas}"
|
||
ENV_FILE="${ENV_FILE:-/var/lib/xiaoxia-saas-production/.env}"
|
||
GENERATED_DIR="${GENERATED_DIR:-/var/lib/xiaoxia-saas-production/generated}"
|
||
LEGACY_ASSETS_DIR="${LEGACY_ASSETS_DIR:-/var/lib/xiaoxia-saas-production/legacy-assets}"
|
||
|
||
# Canary 端口(与 production 错开)
|
||
CANARY_API_PORT=18001
|
||
CANARY_WEB_PORT=13002
|
||
|
||
echo "============================================"
|
||
echo " 灰度发布"
|
||
echo " 新版本: $VERSION"
|
||
echo " 灰度比例: ${GRAY_PCT}%"
|
||
echo " Canary API 端口: $CANARY_API_PORT"
|
||
echo " Canary Web 端口: $CANARY_WEB_PORT"
|
||
echo "============================================"
|
||
|
||
# 1. 检查环境
|
||
if [[ ! -f "$ENV_FILE" ]]; then
|
||
echo "错误: 环境文件不存在: $ENV_FILE"
|
||
exit 1
|
||
fi
|
||
|
||
if [[ ! -f "$NGINX_CONF" ]]; then
|
||
echo "错误: Nginx 配置不存在: $NGINX_CONF"
|
||
exit 1
|
||
fi
|
||
|
||
# 2. 拉取新版本镜像
|
||
echo ""
|
||
echo ">>> 拉取新版本镜像..."
|
||
for component in api web worker; do
|
||
echo " 拉取 $component:$VERSION ..."
|
||
docker pull "${REGISTRY}-${component}:${VERSION}" 2>&1 | tail -1
|
||
done
|
||
echo " ✅ 镜像拉取完成"
|
||
|
||
# 3. 启动 API Canary
|
||
echo ""
|
||
echo ">>> 启动 API Canary 容器..."
|
||
CANARY_API="xiaoxia-api-canary"
|
||
if docker ps -a --format '{{.Names}}' | grep -q "^${CANARY_API}$"; then
|
||
echo " 停止旧 canary..."
|
||
docker rm -f "$CANARY_API" >/dev/null 2>&1
|
||
fi
|
||
|
||
docker run -d \
|
||
--name "$CANARY_API" \
|
||
--env-file "$ENV_FILE" \
|
||
--network xiaoxia-net-production \
|
||
-p "127.0.0.1:${CANARY_API_PORT}:8000" \
|
||
-e APP_ENV=production \
|
||
-e APP_VERSION="$VERSION-canary" \
|
||
-e GENERATED_FILES_DIR=/app/generated \
|
||
-e GENERATED_FILES_URL_PREFIX=/generated-files \
|
||
-e PUBLIC_API_BASE_URL=https://saas-api.xiaoxiajianji.com \
|
||
-v "${GENERATED_DIR}:/app/generated" \
|
||
--restart unless-stopped \
|
||
--cpus 1 \
|
||
--memory 1g \
|
||
--health-cmd "python -c \"import urllib.request; urllib.request.urlopen('http://localhost:8000/health', timeout=5)\"" \
|
||
--health-interval 30s \
|
||
--health-timeout 10s \
|
||
--health-retries 3 \
|
||
--health-start-period 40s \
|
||
--log-driver json-file \
|
||
--log-opt max-size=50m \
|
||
--log-opt max-file=3 \
|
||
"${REGISTRY}-api:${VERSION}" >/dev/null
|
||
|
||
echo " ✅ API Canary 已启动(端口 $CANARY_API_PORT)"
|
||
|
||
# 4. 启动 Web Canary
|
||
echo ""
|
||
echo ">>> 启动 Web Canary 容器..."
|
||
CANARY_WEB="xiaoxia-web-canary"
|
||
if docker ps -a --format '{{.Names}}' | grep -q "^${CANARY_WEB}$"; then
|
||
echo " 停止旧 canary..."
|
||
docker rm -f "$CANARY_WEB" >/dev/null 2>&1
|
||
fi
|
||
|
||
LEGACY_VOLUME=""
|
||
if [[ -d "$LEGACY_ASSETS_DIR" ]] && [[ -n "$(ls -A "$LEGACY_ASSETS_DIR" 2>/dev/null)" ]]; then
|
||
LEGACY_VOLUME="-v ${LEGACY_ASSETS_DIR}:/usr/share/nginx/html/assets-legacy/assets:ro"
|
||
fi
|
||
|
||
docker run -d \
|
||
--name "$CANARY_WEB" \
|
||
--network xiaoxia-net-production \
|
||
-p "127.0.0.1:${CANARY_WEB_PORT}:80" \
|
||
--restart unless-stopped \
|
||
--cpus 0.5 \
|
||
--memory 256m \
|
||
$LEGACY_VOLUME \
|
||
--health-cmd "wget --spider -q http://127.0.0.1:80" \
|
||
--health-interval 30s \
|
||
--health-timeout 5s \
|
||
--health-retries 3 \
|
||
--health-start-period 10s \
|
||
--log-driver json-file \
|
||
--log-opt max-size=50m \
|
||
--log-opt max-file=3 \
|
||
"${REGISTRY}-web:${VERSION}" >/dev/null
|
||
|
||
echo " ✅ Web Canary 已启动(端口 $CANARY_WEB_PORT)"
|
||
|
||
# 5. 等待健康检查
|
||
echo ""
|
||
echo ">>> 等待 Canary 容器健康..."
|
||
for i in $(seq 1 40); do
|
||
api_healthy=$(docker inspect --format='{{.State.Health.Status}}' "$CANARY_API" 2>/dev/null || echo "starting")
|
||
web_healthy=$(docker inspect --format='{{.State.Health.Status}}' "$CANARY_WEB" 2>/dev/null || echo "starting")
|
||
|
||
if [[ "$api_healthy" == "healthy" && "$web_healthy" == "healthy" ]]; then
|
||
echo " ✅ API + Web Canary 均健康(用时 ${i}s)"
|
||
break
|
||
fi
|
||
|
||
if [[ "$api_healthy" == "unhealthy" ]]; then
|
||
echo " ❌ API Canary 健康检查失败"
|
||
docker logs --tail 30 "$CANARY_API"
|
||
exit 1
|
||
fi
|
||
if [[ "$web_healthy" == "unhealthy" ]]; then
|
||
echo " ❌ Web Canary 健康检查失败"
|
||
docker logs --tail 20 "$CANARY_WEB"
|
||
exit 1
|
||
fi
|
||
|
||
sleep 3
|
||
done
|
||
|
||
# 6. 更新 Nginx 配置 - 添加 upstream 权重
|
||
echo ""
|
||
echo ">>> 更新 Nginx 权重(稳定: $((100-GRAY_PCT))% / 灰度: ${GRAY_PCT}%)..."
|
||
|
||
# 备份
|
||
BAK_FILE="${NGINX_CONF}.bak.gray.$(date +%Y%m%d%H%M%S)"
|
||
cp "$NGINX_CONF" "$BAK_FILE"
|
||
echo " 已备份: $BAK_FILE"
|
||
|
||
# 生成 upstream 块
|
||
UPSTREAM_BLOCK="
|
||
# Gray release upstreams(自动生成 - gray_deploy.sh)
|
||
upstream saas_api_backend {
|
||
server 127.0.0.1:8001 weight=$((100-GRAY_PCT));
|
||
server 127.0.0.1:${CANARY_API_PORT} weight=${GRAY_PCT};
|
||
}
|
||
|
||
upstream saas_web_backend {
|
||
server 127.0.0.1:3002 weight=$((100-GRAY_PCT));
|
||
server 127.0.0.1:${CANARY_WEB_PORT} weight=${GRAY_PCT};
|
||
}
|
||
"
|
||
|
||
# 在文件最前面插入 upstream 块
|
||
TMP_CONF=$(mktemp)
|
||
{
|
||
echo "$UPSTREAM_BLOCK"
|
||
cat "$NGINX_CONF"
|
||
} > "$TMP_CONF"
|
||
|
||
# 替换 proxy_pass 指向 upstream
|
||
# API: proxy_pass http://127.0.0.1:8001 -> proxy_pass http://saas_api_backend
|
||
sed -i 's|proxy_pass http://127\.0\.0\.1:8001|proxy_pass http://saas_api_backend|g' "$TMP_CONF"
|
||
# Web: proxy_pass http://127.0.0.1:3002/ -> proxy_pass http://saas_web_backend/
|
||
sed -i 's|proxy_pass http://127\.0\.0\.1:3002/|proxy_pass http://saas_web_backend/|g' "$TMP_CONF"
|
||
|
||
# 测试配置
|
||
mv "$TMP_CONF" "$NGINX_CONF"
|
||
if ! nginx -t 2>&1; then
|
||
echo " ❌ Nginx 配置测试失败,回滚..."
|
||
cp "$BAK_FILE" "$NGINX_CONF"
|
||
nginx -t
|
||
exit 1
|
||
fi
|
||
|
||
nginx -s reload
|
||
echo " ✅ Nginx 已 reload,灰度生效"
|
||
|
||
# 7. 验证灰度流量
|
||
echo ""
|
||
echo ">>> 验证灰度流量..."
|
||
gray_hits=0
|
||
total_hits=20
|
||
for i in $(seq 1 $total_hits); do
|
||
resp=$(curl -s -o /dev/null -w "%{http_code}" -H "X-Gray-Test: 1" http://127.0.0.1:${CANARY_API_PORT}/health 2>/dev/null || echo "000")
|
||
if [[ "$resp" == "200" ]]; then
|
||
gray_hits=$((gray_hits + 1))
|
||
fi
|
||
sleep 0.1
|
||
done
|
||
echo " Canary 健康验证: $gray_hits/$total_hits 请求成功"
|
||
|
||
echo ""
|
||
echo "============================================"
|
||
echo " ✅ 灰度发布完成"
|
||
echo " 版本: $VERSION (${GRAY_PCT}%流量)"
|
||
echo " API: 127.0.0.1:$CANARY_API_PORT"
|
||
echo " Web: 127.0.0.1:$CANARY_WEB_PORT"
|
||
echo " Nginx 备份: $BAK_FILE"
|
||
echo " 回滚: ./scripts/rollback.sh"
|
||
echo " Worker: 暂不灰度(队列消费无法按比例切流)"
|
||
echo "============================================"
|