2026-09-17 11:49:01 +08:00
|
|
|
#!/usr/bin/env sh
|
|
|
|
|
set -eu
|
|
|
|
|
|
|
|
|
|
if [ "$#" -ne 4 ]; then
|
|
|
|
|
echo "用法: $0 <服务名> <镜像仓库> <镜像标签> <Compose 文件>" >&2
|
|
|
|
|
exit 2
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
SERVICE="$1"
|
|
|
|
|
IMAGE_REPOSITORY="$2"
|
|
|
|
|
IMAGE_TAG="$3"
|
|
|
|
|
COMPOSE_FILE="$4"
|
|
|
|
|
IMAGE="$IMAGE_REPOSITORY:$IMAGE_TAG"
|
|
|
|
|
REGISTRY_HOST="${IMAGE_REPOSITORY%%/*}"
|
|
|
|
|
SSH_TARGET="${TARGET_SSH_TARGET:?TARGET_SSH_TARGET 未设置}"
|
|
|
|
|
SSH_KEY="${TARGET_SSH_KEY:?TARGET_SSH_KEY 未设置}"
|
|
|
|
|
SSH_KNOWN_HOSTS="${TARGET_SSH_KNOWN_HOSTS:?TARGET_SSH_KNOWN_HOSTS 未设置}"
|
|
|
|
|
REMOTE_DIR="${TARGET_DEPLOY_DIR:?TARGET_DEPLOY_DIR 未设置}"
|
|
|
|
|
: "${REGISTRY_USER:?REGISTRY_USER 未设置}"
|
|
|
|
|
: "${REGISTRY_PASSWORD:?REGISTRY_PASSWORD 未设置}"
|
|
|
|
|
|
|
|
|
|
run_ssh() {
|
|
|
|
|
ssh -i "$SSH_KEY" -o BatchMode=yes -o StrictHostKeyChecking=yes \
|
|
|
|
|
-o UserKnownHostsFile="$SSH_KNOWN_HOSTS" -o ConnectTimeout=10 "$SSH_TARGET" "$@"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
run_scp() {
|
|
|
|
|
scp -i "$SSH_KEY" -o BatchMode=yes -o StrictHostKeyChecking=yes \
|
|
|
|
|
-o UserKnownHostsFile="$SSH_KNOWN_HOSTS" -o ConnectTimeout=10 "$@"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
run_ssh install -d -m 750 "$REMOTE_DIR"
|
|
|
|
|
REMOTE_COMPOSE="$REMOTE_DIR/docker-compose.$SERVICE.yml"
|
|
|
|
|
run_scp "$COMPOSE_FILE" "$SSH_TARGET:$REMOTE_COMPOSE.tmp"
|
|
|
|
|
run_ssh sh -s -- "$REMOTE_COMPOSE.tmp" "$REMOTE_COMPOSE" <<'REMOTE_PREPARE'
|
|
|
|
|
set -eu
|
|
|
|
|
compose_tmp="$1"
|
|
|
|
|
compose_target="$2"
|
|
|
|
|
chmod 644 "$compose_tmp"
|
|
|
|
|
mv -f "$compose_tmp" "$compose_target"
|
|
|
|
|
REMOTE_PREPARE
|
|
|
|
|
|
|
|
|
|
printf '%s' "$REGISTRY_PASSWORD" | run_ssh docker login -u "$REGISTRY_USER" --password-stdin "$REGISTRY_HOST"
|
|
|
|
|
trap 'run_ssh docker logout "$REGISTRY_HOST" >/dev/null 2>&1 || true' EXIT HUP INT TERM
|
|
|
|
|
run_ssh docker pull "$IMAGE"
|
|
|
|
|
|
|
|
|
|
run_ssh sh -s -- "$REMOTE_DIR" "$SERVICE" "$IMAGE_REPOSITORY" "$IMAGE_TAG" <<'REMOTE_DEPLOY'
|
|
|
|
|
set -eu
|
|
|
|
|
remote_dir="$1"
|
|
|
|
|
service="$2"
|
|
|
|
|
image_repository="$3"
|
|
|
|
|
image_tag="$4"
|
|
|
|
|
compose_file="docker-compose.$service.yml"
|
|
|
|
|
project_name="firstleg-prod-$service"
|
|
|
|
|
cd "$remote_dir"
|
|
|
|
|
export IMAGE_REPOSITORY="$image_repository" IMAGE_TAG="$image_tag"
|
|
|
|
|
|
|
|
|
|
compose() {
|
|
|
|
|
docker compose --project-name "$project_name" -f "$compose_file" "$@"
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-20 21:44:56 +08:00
|
|
|
case "$service" in
|
|
|
|
|
admin-web)
|
|
|
|
|
base_port=9001
|
2026-09-20 23:39:39 +08:00
|
|
|
nginx_config=/www/server/panel/vhost/nginx/00-blue-green-admin-web.conf
|
2026-09-20 21:44:56 +08:00
|
|
|
legacy_container=firstleg-prod-admin-web
|
|
|
|
|
;;
|
|
|
|
|
client-web)
|
|
|
|
|
base_port=9002
|
2026-09-20 23:39:39 +08:00
|
|
|
nginx_config=/www/server/panel/vhost/nginx/00-blue-green-client-web.conf
|
2026-09-20 21:44:56 +08:00
|
|
|
legacy_container=firstleg-prod-client-web
|
|
|
|
|
;;
|
|
|
|
|
*) echo "不支持蓝绿部署的前端服务: $service" >&2; exit 2 ;;
|
2026-09-17 11:49:01 +08:00
|
|
|
esac
|
2026-09-20 21:44:56 +08:00
|
|
|
|
|
|
|
|
state_root=/data/firstleg/deploy/blue-green-state
|
|
|
|
|
state_file="$state_root/frontend-$service.active"
|
|
|
|
|
routing_network=firstleg-production-routing
|
|
|
|
|
mkdir -p "$state_root"
|
|
|
|
|
chmod 750 "$state_root"
|
|
|
|
|
docker network inspect "$routing_network" >/dev/null 2>&1 || docker network create "$routing_network" >/dev/null
|
|
|
|
|
|
|
|
|
|
for pair in gateway-active:firstleg-production-gateway gateway-overseas-active:firstleg-production-gateway-overseas; do
|
|
|
|
|
alias_name="${pair%%:*}"
|
|
|
|
|
container="${pair#*:}"
|
|
|
|
|
if docker inspect "$container" >/dev/null 2>&1; then
|
|
|
|
|
docker network inspect "$routing_network" --format '{{json .Containers}}' | grep -q "\"$container\"" \
|
|
|
|
|
|| docker network connect --alias "$alias_name" "$routing_network" "$container"
|
|
|
|
|
fi
|
|
|
|
|
done
|
|
|
|
|
|
|
|
|
|
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; fi
|
|
|
|
|
[ "$active_slot" = legacy ] && target_slot=green
|
2026-09-20 23:39:39 +08:00
|
|
|
if [ "$target_slot" = blue ]; then target_port=$((base_port + 20000)); else target_port=$((base_port + 10000)); fi
|
2026-09-20 21:44:56 +08:00
|
|
|
candidate="${legacy_container}-${target_slot}"
|
|
|
|
|
docker rm -f "$candidate" >/dev/null 2>&1 || true
|
|
|
|
|
|
|
|
|
|
if ! compose run -d --no-deps --name "$candidate" \
|
|
|
|
|
-p "127.0.0.1:$target_port:80" "$service" >/dev/null; then
|
|
|
|
|
docker rm -f "$candidate" >/dev/null 2>&1 || true
|
2026-09-17 11:49:01 +08:00
|
|
|
exit 1
|
|
|
|
|
fi
|
2026-09-20 21:44:56 +08:00
|
|
|
|
|
|
|
|
healthy=false
|
|
|
|
|
elapsed=0
|
|
|
|
|
while [ "$elapsed" -lt 120 ]; do
|
|
|
|
|
if docker exec "$candidate" wget -qO- http://127.0.0.1/healthz >/dev/null 2>&1; then
|
|
|
|
|
healthy=true
|
|
|
|
|
break
|
|
|
|
|
fi
|
|
|
|
|
sleep 2
|
|
|
|
|
elapsed=$((elapsed + 2))
|
|
|
|
|
done
|
|
|
|
|
if [ "$healthy" != true ]; then
|
|
|
|
|
docker logs --tail 100 "$candidate" >&2 || true
|
|
|
|
|
docker rm -f "$candidate" >/dev/null 2>&1 || true
|
|
|
|
|
exit 1
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
backup="$nginx_config.blue-green-backup"
|
|
|
|
|
cp -f "$nginx_config" "$backup"
|
|
|
|
|
if ! sed -i "s#127\.0\.0\.1:${active_port:-$base_port}#127.0.0.1:$target_port#g" "$nginx_config" \
|
|
|
|
|
|| cmp -s "$nginx_config" "$backup" || ! nginx -t || ! nginx -s reload; then
|
|
|
|
|
cp -f "$backup" "$nginx_config"
|
|
|
|
|
nginx -t && nginx -s reload || true
|
|
|
|
|
docker rm -f "$candidate" >/dev/null 2>&1 || true
|
|
|
|
|
exit 1
|
|
|
|
|
fi
|
|
|
|
|
rm -f "$backup"
|
|
|
|
|
|
|
|
|
|
if [ -n "$active_container" ] && docker inspect "$active_container" >/dev/null 2>&1; then
|
|
|
|
|
docker stop --time 30 "$active_container" >/dev/null
|
|
|
|
|
docker rm "$active_container" >/dev/null
|
|
|
|
|
fi
|
|
|
|
|
printf '%s|%s|%s|%s\n' "$target_slot" "$candidate" "$target_port" "$image_repository:$image_tag" > "$state_file.tmp"
|
|
|
|
|
chmod 640 "$state_file.tmp"
|
|
|
|
|
mv -f "$state_file.tmp" "$state_file"
|
|
|
|
|
echo "生产前端蓝绿部署成功: service=$service slot=$target_slot port=$target_port image=$image_repository:$image_tag"
|
2026-09-17 11:49:01 +08:00
|
|
|
REMOTE_DEPLOY
|