#!/usr/bin/env sh set -eu if [ "$#" -ne 4 ]; then echo "用法: $0 <服务名> <镜像仓库> <镜像标签> " >&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" "$@" } case "$service" in admin-web) base_port=9001 nginx_config=/www/server/panel/vhost/nginx/admin.buggoglobal.com.conf legacy_container=firstleg-prod-admin-web ;; client-web) base_port=9002 nginx_config=/www/server/panel/vhost/nginx/app.buggoglobal.com.conf legacy_container=firstleg-prod-client-web ;; *) echo "不支持蓝绿部署的前端服务: $service" >&2; exit 2 ;; esac 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 if [ "$target_slot" = blue ]; then target_port="$base_port"; else target_port=$((base_port + 10000)); fi 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 exit 1 fi 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" REMOTE_DEPLOY