12686 字
63 分钟
springboot零停机发布方案
springboot零停机发布方案
环境:阿里云云效 + APISIX + Spring Boot(双节点)
适用场景:单台 ECS(IP: 1.2.3.4)上以 /home/backend/demo-node1 与 /home/backend/demo-node2 目录运行的两个 Spring Boot 服务实例,通过 APISIX 网关做流量灰度与切换,并使用云效流水线实现一键无感发布 / 回滚。
1. 系统架构
┌───────────────┐ │ 开发 / CI/CD │ └───────┬───────┘ │ (云效 Pipeline) ┌───────▼───────┐ │ 云效制品库 │ └───────┬───────┘ │ ┌─────────────────▼──────────────────┐ │ ECS 1.2.3.4 (CentOS 7) │ │ │ │ ┌──────────┐ ┌──────────┐ │ │ │ demo‑node1│ │ demo‑node2│ │ │ │ :8081 │ │ :8082 │ │ │ └──────────┘ └──────────┘ │ │ ▲ ▲ │ │ │ (health check) │ │ │ ┌──────┴──────────────────┴─────┐ │ │ │ Apache APISIX (9000) │ │ │ └───────────────────────────────┘ │ └─────────────────────────────────────┘2. 主机与端口
| 角色 | IP | 端口 | 路径 |
|---|---|---|---|
| APISIX 网关 | 1.2.3.4 | 9000 / 9180 (dashboard) | /usr/local/apisix |
| Spring Boot node1 | 1.2.3.4 | 8081 | /home/backend/demo-node1 |
| Spring Boot node2 | 1.2.3.4 | 8082 | /home/backend/demo-node2 |
3. 部署流水线核心步骤(云效)
构建配置

节点1部署配置

节点2部署配置

4. 完整配置与脚本
下面各文件已按原始目录结构列出,内容未做任何改动:
安装Docker
config.yaml
deployment: admin: admin_key_required: true # 建议开发后期再打开 admin_key: - name: admin role: admin key: CKjcGDoqxcJfpIXWigyqjdUSACvwzEJy allow_admin: - 127.0.0.0/24 - 172.17.0.0/16 # Docker 默认桥接 - 0.0.0.0/0 # 仅本地演示可全开apisix-manager.sh
#!/bin/bash
# APISIX 80端口管理脚本
CONTAINER_NAME="apisix-main"NETWORK_NAME="apisix-quickstart-net"ETCD_HOST="http://etcd-quickstart:2379"APISIX_IMAGE="apache/apisix:3.12.0-debian"
# 停止APISIXstop() { echo "停止所有APISIX相关容器..."
# 停止所有可能的APISIX容器名称 for name in apisix-main apisix-no-auth apisix-quickstart apisix-quickstart-80; do if docker ps -a --format "{{.Names}}" | grep -q "^${name}$"; then echo "停止容器: $name" docker stop $name 2>/dev/null docker rm $name 2>/dev/null fi done
echo "所有APISIX容器已停止"}
# 启动APISIX (80端口)start() { echo "启动APISIX容器 (80端口)..."
# 先检查容器是否已存在 if docker ps -a --format "{{.Names}}" | grep -q "^${CONTAINER_NAME}$"; then echo "容器 $CONTAINER_NAME 已存在,先删除..." docker stop $CONTAINER_NAME 2>/dev/null docker rm $CONTAINER_NAME 2>/dev/null fi
# 启动新容器 CONTAINER_ID=$(docker run -d --name $CONTAINER_NAME \ --network $NETWORK_NAME \ -v $(pwd)/config.yaml:/usr/local/apisix/conf/config.yaml:ro \ -p 80:9080 \ -p 443:9443 \ -p 9180:9180 \ -e APISIX_DEPLOYMENT_ETCD_HOST="[\"$ETCD_HOST\"]" \ $APISIX_IMAGE 2>&1)
if [ $? -eq 0 ]; then echo "容器启动成功,ID: ${CONTAINER_ID:0:12}" echo "等待服务启动..." sleep 5
# 检查容器是否真正运行 if docker ps --format "{{.Names}}" | grep -q "^${CONTAINER_NAME}$"; then echo "APISIX已成功启动在80端口" else echo "容器启动失败,查看日志:" docker logs $CONTAINER_NAME 2>/dev/null | tail -10 fi else echo "容器启动失败: $CONTAINER_ID" fi}
# 重启restart() { stop sleep 2 start}
# 状态检查status() { if docker ps --format "{{.Names}}" | grep -q "^${CONTAINER_NAME}$"; then echo "APISIX运行中" echo "端口映射:" docker port $CONTAINER_NAME else echo "APISIX未运行" fi}
case "$1" in start) start ;; stop) stop ;; restart) restart ;; status) status ;; *) echo "用法: $0 {start|stop|restart|status}" exit 1 ;;esacapisix-dashboard-manager.sh
#!/bin/bash
# APISIX Dashboard Docker 管理脚本# 支持安装、启动、停止、卸载功能
# 颜色定义RED='\033[0;31m'GREEN='\033[0;32m'YELLOW='\033[1;33m'BLUE='\033[0;34m'NC='\033[0m' # No Color
# 配置变量DOCKER_IMAGE="apache/apisix-dashboard"CONTAINER_NAME="apisix-dashboard"DASHBOARD_PORT="9000"CONFIG_DIR="/tmp/apisix-dashboard-config"CONFIG_FILE="$CONFIG_DIR/conf.yaml"
# 日志函数log_info() { echo -e "${BLUE}[INFO]${NC} $1"}
log_success() { echo -e "${GREEN}[SUCCESS]${NC} $1"}
log_warning() { echo -e "${YELLOW}[WARNING]${NC} $1"}
log_error() { echo -e "${RED}[ERROR]${NC} $1"}
# 检查Docker是否安装check_docker() { if ! command -v docker &> /dev/null; then log_error "Docker 未安装,请先安装 Docker" exit 1 fi
if ! docker info &> /dev/null; then log_error "Docker 服务未运行,请启动 Docker 服务" exit 1 fi}
# 检查etcd是否运行check_etcd() { log_info "检查 etcd 服务状态..."
# 检查APISIX快速安装的etcd容器 if docker ps --format "{{.Names}}" | grep -q "etcd-quickstart"; then log_success "发现 APISIX 快速安装的 etcd 容器正在运行" return 0 fi
# 检查本地etcd服务 if curl -s http://127.0.0.1:2379/health &> /dev/null; then log_success "etcd 服务运行正常" return 0 fi
log_warning "未发现运行中的 etcd 服务" log_warning "请确保以下任一条件满足:" log_warning "1. APISIX 快速安装脚本已运行(etcd-quickstart 容器存在)" log_warning "2. 本地 etcd 服务正在 127.0.0.1:2379 运行"
read -p "是否继续安装?(y/N): " -n 1 -r echo if [[ ! $REPLY =~ ^[Yy]$ ]]; then exit 1 fi}
# 生成配置文件generate_config() { log_info "生成配置文件..." mkdir -p "$CONFIG_DIR"
# 检测etcd连接地址 local etcd_endpoint="http://127.0.0.1:2379" local admin_api_endpoint="http://127.0.0.1:9180"
# 如果发现APISIX快速安装的etcd容器,使用容器网络连接 if docker ps --format "{{.Names}}" | grep -q "etcd-quickstart"; then log_info "检测到 APISIX 快速安装环境,配置容器网络连接" etcd_endpoint="http://etcd-quickstart:2379" admin_api_endpoint="http://apisix-quickstart:9180" fi
cat > "$CONFIG_FILE" << EOFconf: listen: host: 0.0.0.0 port: 9000 etcd: endpoints: - $etcd_endpoint log: error_log: level: warn file_path: logs/error.log access_log: file_path: logs/access.logauthentication: secret: secret expire_time: 3600 users: - username: admin password: adminplugins: - api-breaker - authz-keycloak - basic-auth - batch-requests - consumer-restriction - cors - echo - fault-injection - grpc-transcode - hmac-auth - http-logger - ip-restriction - jwt-auth - kafka-logger - key-auth - limit-conn - limit-count - limit-req - node-status - openid-connect - prometheus - proxy-cache - proxy-mirror - proxy-rewrite - redirect - referer-restriction - request-id - request-validation - response-rewrite - serverless-post-function - serverless-pre-function - sls-logger - syslog - tcp-logger - udp-logger - uri-blocker - wolf-rbac - zipkin - server-info - traffic-splitapisix: base_url: $admin_api_endpoint api_key: ""EOF
log_success "配置文件已生成: $CONFIG_FILE" log_info "etcd 连接地址: $etcd_endpoint" log_info "Admin API 地址: $admin_api_endpoint"}
# 检查容器状态check_container_status() { if docker ps -q -f name="$CONTAINER_NAME" | grep -q .; then echo "running" elif docker ps -aq -f name="$CONTAINER_NAME" | grep -q .; then echo "stopped" else echo "not_exists" fi}
# 安装Dashboardinstall_dashboard() { log_info "开始安装 APISIX Dashboard..."
# 检查是否已经安装 status=$(check_container_status) if [ "$status" != "not_exists" ]; then log_warning "Dashboard 容器已存在" read -p "是否重新安装?这将删除现有容器 (y/N): " -n 1 -r echo if [[ $REPLY =~ ^[Yy]$ ]]; then uninstall_dashboard else return 0 fi fi
# 检查依赖 check_docker check_etcd
# 生成配置文件 generate_config
# 拉取镜像 log_info "拉取 Docker 镜像..." if ! docker pull "$DOCKER_IMAGE"; then log_error "拉取镜像失败" exit 1 fi
# 创建并启动容器 log_info "创建并启动容器..."
# 检测是否需要连接到APISIX网络 local network_option="" if docker ps --format "{{.Names}}" | grep -q "etcd-quickstart"; then # 检查apisix-quickstart-net网络是否存在 if docker network ls --format "{{.Name}}" | grep -q "apisix-quickstart-net"; then log_info "连接到 APISIX 快速安装网络" network_option="--network apisix-quickstart-net" fi fi
if docker run -d \ --name "$CONTAINER_NAME" \ $network_option \ -p "$DASHBOARD_PORT:9000" \ -v "$CONFIG_FILE:/usr/local/apisix-dashboard/conf/conf.yaml" \ "$DOCKER_IMAGE"; then
log_success "APISIX Dashboard 安装成功!" log_info "访问地址: http://127.0.0.1:$DASHBOARD_PORT" log_info "默认用户名: admin" log_info "默认密码: admin" else log_error "容器启动失败" exit 1 fi}
# 启动Dashboardstart_dashboard() { log_info "启动 APISIX Dashboard..."
status=$(check_container_status) case $status in "running") log_warning "Dashboard 已经在运行中" ;; "stopped") if docker start "$CONTAINER_NAME"; then log_success "Dashboard 启动成功" log_info "访问地址: http://127.0.0.1:$DASHBOARD_PORT" else log_error "Dashboard 启动失败" exit 1 fi ;; "not_exists") log_error "Dashboard 容器不存在,请先安装" exit 1 ;; esac}
# 停止Dashboardstop_dashboard() { log_info "停止 APISIX Dashboard..."
status=$(check_container_status) case $status in "running") if docker stop "$CONTAINER_NAME"; then log_success "Dashboard 已停止" else log_error "停止 Dashboard 失败" exit 1 fi ;; "stopped") log_warning "Dashboard 已经停止" ;; "not_exists") log_error "Dashboard 容器不存在" exit 1 ;; esac}
# 查看状态show_status() { log_info "检查 APISIX Dashboard 状态..."
status=$(check_container_status) case $status in "running") log_success "Dashboard 正在运行" echo echo "容器信息:" docker ps --filter name="$CONTAINER_NAME" --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}" echo log_info "访问地址: http://127.0.0.1:$DASHBOARD_PORT" ;; "stopped") log_warning "Dashboard 已停止" ;; "not_exists") log_error "Dashboard 容器不存在,请先安装" ;; esac}
# 查看日志show_logs() { log_info "显示 APISIX Dashboard 日志..."
status=$(check_container_status) if [ "$status" = "not_exists" ]; then log_error "Dashboard 容器不存在" exit 1 fi
echo "最近50行日志:" docker logs --tail 50 "$CONTAINER_NAME" echo read -p "按回车键继续..."}
# 卸载Dashboarduninstall_dashboard() { log_info "卸载 APISIX Dashboard..."
status=$(check_container_status) if [ "$status" = "not_exists" ]; then log_warning "Dashboard 容器不存在" return 0 fi
read -p "确认卸载 APISIX Dashboard?这将删除容器和配置文件 (y/N): " -n 1 -r echo if [[ ! $REPLY =~ ^[Yy]$ ]]; then log_info "取消卸载" return 0 fi
# 停止并删除容器 if [ "$status" = "running" ]; then docker stop "$CONTAINER_NAME" fi
docker rm "$CONTAINER_NAME"
# 删除配置文件 if [ -d "$CONFIG_DIR" ]; then rm -rf "$CONFIG_DIR" fi
log_success "APISIX Dashboard 已卸载"}
# 显示主菜单show_menu() { clear echo -e "${BLUE}================================${NC}" echo -e "${BLUE} APISIX Dashboard 管理工具${NC}" echo -e "${BLUE}================================${NC}" echo echo "1. 安装 Dashboard" echo "2. 启动 Dashboard" echo "3. 停止 Dashboard" echo "4. 查看状态" echo "5. 查看日志" echo "6. 卸载 Dashboard" echo "7. 退出" echo}
# 主函数main() { while true; do show_menu read -p "请选择操作 [1-7]: " choice echo
case $choice in 1) install_dashboard ;; 2) start_dashboard ;; 3) stop_dashboard ;; 4) show_status ;; 5) show_logs ;; 6) uninstall_dashboard ;; 7) log_info "退出程序" exit 0 ;; *) log_error "无效选择,请输入 1-7" ;; esac
echo read -p "按回车键继续..." done}
# 检查是否以root权限运行if [ "$EUID" -eq 0 ]; then log_warning "建议不要以 root 权限运行此脚本"fi
# 启动主程序mainapisix_config_manager.sh
#!/bin/bash
# APISIX 配置管理脚本# 支持一键导出和导入所有配置# 作者: Manus AI# 版本: 1.0
# 颜色定义RED='\033[0;31m'GREEN='\033[0;32m'YELLOW='\033[1;33m'BLUE='\033[0;34m'NC='\033[0m' # No Color
# 默认配置DEFAULT_APISIX_HOST="1.2.3.4"DEFAULT_APISIX_PORT="9180"DEFAULT_API_KEY="CKjcGDoqxcJfpIXWigyqjdUSACvwzEJy"
# 配置文件列表CONFIG_TYPES=( "routes" "upstreams" "services" "consumers" "ssls" "global_rules" "plugin_configs" "stream_routes" "plugins/list")
# 配置文件名映射declare -A CONFIG_FILES=( ["routes"]="apisix_routes.json" ["upstreams"]="apisix_upstreams.json" ["services"]="apisix_services.json" ["consumers"]="apisix_consumers.json" ["ssls"]="apisix_ssls.json" ["global_rules"]="apisix_global_rules.json" ["plugin_configs"]="apisix_plugin_configs.json" ["stream_routes"]="apisix_stream_routes.json" ["plugins/list"]="apisix_plugins.json")
# 显示标题show_header() { echo -e "${BLUE}================================================${NC}" echo -e "${BLUE} APISIX 配置管理工具${NC}" echo -e "${BLUE}================================================${NC}" echo ""}
# 显示菜单show_menu() { echo -e "${YELLOW}请选择操作:${NC}" echo -e "${GREEN}1.${NC} 一键导出所有配置" echo -e "${GREEN}2.${NC} 一键导入所有配置" echo -e "${GREEN}3.${NC} 退出" echo ""}
# 去除字符串两边的空格trim_spaces() { local var="$1" # 去除前导空格 var="${var#"${var%%[![:space:]]*}"}" # 去除尾随空格 var="${var%"${var##*[![:space:]]}"}" echo "$var"}
# 检查 APISIX 连接check_apisix_connection() { local host="$1" local port="$2" local api_key="$3"
echo -e "${YELLOW}正在检查 APISIX 连接...${NC}"
local response=$(curl -s -w "%{http_code}" -H "X-API-KEY: $api_key" \ "http://$host:$port/apisix/admin/routes" -o /dev/null)
if [ "$response" = "200" ]; then echo -e "${GREEN}✓ APISIX 连接成功${NC}" return 0 else echo -e "${RED}✗ APISIX 连接失败 (HTTP状态码: $response)${NC}" echo -e "${RED}请检查服务器地址、端口和API密钥${NC}" return 1 fi}
# 获取用户输入的配置信息get_apisix_config() { echo -e "${YELLOW}请输入 APISIX 配置信息:${NC}"
read -p "APISIX 服务器地址 [默认: $DEFAULT_APISIX_HOST]: " APISIX_HOST APISIX_HOST=$(trim_spaces "${APISIX_HOST:-$DEFAULT_APISIX_HOST}")
read -p "APISIX 端口 [默认: $DEFAULT_APISIX_PORT]: " APISIX_PORT APISIX_PORT=$(trim_spaces "${APISIX_PORT:-$DEFAULT_APISIX_PORT}")
read -p "API 密钥 [默认: $DEFAULT_API_KEY]: " API_KEY API_KEY=$(trim_spaces "${API_KEY:-$DEFAULT_API_KEY}")
echo ""}
# 导出配置export_configs() { echo -e "${BLUE}开始导出 APISIX 配置...${NC}" echo ""
# 获取配置信息 get_apisix_config
# 检查连接 if ! check_apisix_connection "$APISIX_HOST" "$APISIX_PORT" "$API_KEY"; then return 1 fi
# 获取导出目录 read -p "请输入导出目录路径 [默认: ./apisix_export]: " EXPORT_DIR EXPORT_DIR=$(trim_spaces "${EXPORT_DIR:-./apisix_export}")
# 创建导出目录 if [ ! -d "$EXPORT_DIR" ]; then mkdir -p "$EXPORT_DIR" echo -e "${GREEN}✓ 创建导出目录: $EXPORT_DIR${NC}" fi
echo "" echo -e "${YELLOW}正在导出配置文件...${NC}"
# 导出各类配置 local success_count=0 local total_count=${#CONFIG_TYPES[@]}
for config_type in "${CONFIG_TYPES[@]}"; do local file_name="${CONFIG_FILES[$config_type]}" local output_file="$EXPORT_DIR/$file_name"
echo -n "导出 $config_type ... "
local response=$(curl -s -H "X-API-KEY: $API_KEY" \ "http://$APISIX_HOST:$APISIX_PORT/apisix/admin/$config_type")
if [ $? -eq 0 ] && [ -n "$response" ]; then echo "$response" | python3 -m json.tool > "$output_file" 2>/dev/null if [ $? -eq 0 ]; then echo -e "${GREEN}✓${NC}" ((success_count++)) else echo -e "${RED}✗ (JSON格式化失败)${NC}" fi else echo -e "${RED}✗ (请求失败)${NC}" fi done
echo ""
# 创建配置汇总报告 create_export_summary "$EXPORT_DIR"
# 创建压缩包 local timestamp=$(date +%Y%m%d_%H%M%S) local archive_name="apisix_config_export_$timestamp.tar.gz"
echo -e "${YELLOW}正在创建压缩包...${NC}" cd "$(dirname "$EXPORT_DIR")" tar -czf "$archive_name" "$(basename "$EXPORT_DIR")" 2>/dev/null
if [ $? -eq 0 ]; then echo -e "${GREEN}✓ 压缩包已创建: $archive_name${NC}" fi
echo "" echo -e "${GREEN}导出完成! ($success_count/$total_count 个配置文件导出成功)${NC}" echo -e "${BLUE}导出目录: $EXPORT_DIR${NC}" echo ""}
# 创建导出汇总报告create_export_summary() { local export_dir="$1" local summary_file="$export_dir/export_summary.md"
cat > "$summary_file" << EOF# APISIX 配置导出报告
## 导出信息- **导出时间**: $(date '+%Y年%m月%d日 %H:%M:%S')- **服务器地址**: $APISIX_HOST:$APISIX_PORT- **导出目录**: $export_dir
## 配置文件列表EOF
for config_type in "${CONFIG_TYPES[@]}"; do local file_name="${CONFIG_FILES[$config_type]}" local file_path="$export_dir/$file_name"
if [ -f "$file_path" ]; then local file_size=$(du -h "$file_path" | cut -f1) echo "- ✓ $file_name ($file_size)" >> "$summary_file" else echo "- ✗ $file_name (导出失败)" >> "$summary_file" fi done
cat >> "$summary_file" << EOF
## 使用说明1. 所有配置文件均为 JSON 格式2. 可使用本脚本的导入功能恢复配置3. 建议定期备份配置文件
---*由 APISIX 配置管理工具自动生成*EOF
echo -e "${GREEN}✓ 导出汇总报告已创建: export_summary.md${NC}"}
# 导入配置import_configs() { echo -e "${BLUE}开始导入 APISIX 配置...${NC}" echo ""
# 获取配置信息 get_apisix_config
# 检查连接 if ! check_apisix_connection "$APISIX_HOST" "$APISIX_PORT" "$API_KEY"; then return 1 fi
# 获取导入目录 read -p "请输入配置文件目录路径: " IMPORT_DIR IMPORT_DIR=$(trim_spaces "$IMPORT_DIR")
if [ -z "$IMPORT_DIR" ]; then echo -e "${RED}✗ 目录路径不能为空${NC}" return 1 fi
if [ ! -d "$IMPORT_DIR" ]; then echo -e "${RED}✗ 目录不存在: $IMPORT_DIR${NC}" return 1 fi
echo "" echo -e "${YELLOW}正在检查配置文件...${NC}"
# 检查配置文件是否存在 local missing_files=() local existing_files=()
for config_type in "${CONFIG_TYPES[@]}"; do # 跳过插件列表,因为它是只读的 if [ "$config_type" = "plugins/list" ]; then continue fi
local file_name="${CONFIG_FILES[$config_type]}" local file_path="$IMPORT_DIR/$file_name"
if [ -f "$file_path" ]; then existing_files+=("$config_type:$file_path") echo -e "${GREEN}✓${NC} $file_name" else missing_files+=("$file_name") echo -e "${YELLOW}!${NC} $file_name (文件不存在,将跳过)" fi done
if [ ${#existing_files[@]} -eq 0 ]; then echo -e "${RED}✗ 没有找到任何可导入的配置文件${NC}" return 1 fi
echo "" echo -e "${YELLOW}警告: 导入操作将覆盖现有配置!${NC}" read -p "确认继续导入? (y/N): " confirm
if [[ ! "$confirm" =~ ^[Yy]$ ]]; then echo -e "${YELLOW}导入操作已取消${NC}" return 0 fi
echo "" echo -e "${YELLOW}正在导入配置...${NC}"
local success_count=0 local error_count=0
# 导入配置的顺序很重要:先导入upstreams,再导入routes local import_order=("upstreams" "services" "consumers" "routes" "ssls" "global_rules" "plugin_configs" "stream_routes")
for config_type in "${import_order[@]}"; do local file_name="${CONFIG_FILES[$config_type]}" local file_path="$IMPORT_DIR/$file_name"
# 检查文件是否存在 local found=false for item in "${existing_files[@]}"; do if [[ "$item" == "$config_type:"* ]]; then found=true break fi done
if [ "$found" = false ]; then continue fi
echo -n "导入 $config_type ... "
# 读取配置文件 local config_data=$(cat "$file_path")
if [ -z "$config_data" ]; then echo -e "${RED}✗ (文件为空)${NC}" ((error_count++)) continue fi
# 解析JSON并导入每个配置项 local items=$(echo "$config_data" | python3 -c "import json, systry: data = json.load(sys.stdin) if 'list' in data: for item in data['list']: if 'value' in item and 'key' in item: # 提取ID key_parts = item['key'].split('/') if len(key_parts) > 0: item_id = key_parts[-1] print(f\"{item_id}|||{json.dumps(item['value'])}\")except: pass")
if [ -z "$items" ]; then echo -e "${YELLOW}! (无配置项)${NC}" continue fi
local item_success=0 local item_total=0
while IFS='|||' read -r item_id item_value; do if [ -n "$item_id" ] && [ -n "$item_value" ]; then ((item_total++))
# 发送PUT请求导入配置 local response=$(curl -s -w "%{http_code}" -X PUT \ -H "X-API-KEY: $API_KEY" \ -H "Content-Type: application/json" \ -d "$item_value" \ "http://$APISIX_HOST:$APISIX_PORT/apisix/admin/$config_type/$item_id" \ -o /dev/null)
if [[ "$response" =~ ^20[0-9]$ ]]; then ((item_success++)) fi fi done <<< "$items"
if [ $item_total -eq 0 ]; then echo -e "${YELLOW}! (无配置项)${NC}" elif [ $item_success -eq $item_total ]; then echo -e "${GREEN}✓ ($item_success/$item_total)${NC}" ((success_count++)) else echo -e "${YELLOW}! ($item_success/$item_total)${NC}" ((error_count++)) fi done
echo ""
if [ $success_count -gt 0 ]; then echo -e "${GREEN}导入完成! ($success_count 个配置类型导入成功)${NC}" fi
if [ $error_count -gt 0 ]; then echo -e "${YELLOW}注意: $error_count 个配置类型导入时出现问题${NC}" fi
echo ""}
# 主函数main() { show_header
while true; do show_menu read -p "请输入选项 (1-3): " choice
case $choice in 1) echo "" export_configs ;; 2) echo "" import_configs ;; 3) echo -e "${GREEN}感谢使用 APISIX 配置管理工具!${NC}" exit 0 ;; *) echo -e "${RED}无效选项,请重新选择${NC}" echo "" ;; esac
echo "" read -p "按回车键继续..." echo "" done}
# 检查依赖check_dependencies() { local missing_deps=()
if ! command -v curl &> /dev/null; then missing_deps+=("curl") fi
if ! command -v python3 &> /dev/null; then missing_deps+=("python3") fi
if ! command -v tar &> /dev/null; then missing_deps+=("tar") fi
if [ ${#missing_deps[@]} -gt 0 ]; then echo -e "${RED}错误: 缺少必要的依赖程序:${NC}" for dep in "${missing_deps[@]}"; do echo -e "${RED} - $dep${NC}" done echo "" echo -e "${YELLOW}请安装缺少的程序后重新运行脚本${NC}" exit 1 fi}
# 脚本入口if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then check_dependencies main "$@"fiapisix_ssl_manager.sh
#!/bin/bash
# APISIX SSL证书管理脚本# 功能:查看、添加、删除、更新SSL证书配置# 作者:Manus AI Assistant# 版本:1.2
# ==================== 配置区域 ====================
# APISIX Admin API配置APISIX_HOST="1.2.3.4"APISIX_ADMIN_PORT="9180"APISIX_API_KEY="CKjcGDoqxcJfpIXWigyqjdUSACvwzEJy"
# API基础URLAPI_BASE_URL="http://${APISIX_HOST}:${APISIX_ADMIN_PORT}/apisix/admin"
# 全局变量存储证书列表declare -a SSL_CERT_IDSdeclare -a SSL_CERT_SNIS
# ==================== 工具函数 ====================
# 去除字符串两边空格trim() { local var="$*" # 去除前导空格 var="${var#"${var%%[![:space:]]*}"}" # 去除尾随空格 var="${var%"${var##*[![:space:]]}"}" echo "$var"}
# 安全读取用户输入read_input() { local prompt="$1" local var_name="$2" local default_value="$3"
if [ -n "$default_value" ]; then echo -n "$prompt [$default_value]: " else echo -n "$prompt: " fi
read -r input input=$(trim "$input")
if [ -z "$input" ] && [ -n "$default_value" ]; then input="$default_value" fi
eval "$var_name='$input'"}
# 检查文件是否存在check_file() { local file_path="$1" if [ ! -f "$file_path" ]; then echo "❌ 错误:文件不存在 - $file_path" return 1 fi return 0}
# 验证证书文件validate_cert_file() { local cert_file="$1" if ! openssl x509 -in "$cert_file" -noout -text >/dev/null 2>&1; then echo "❌ 错误:无效的证书文件 - $cert_file" return 1 fi return 0}
# 验证私钥文件validate_key_file() { local key_file="$1" if ! openssl rsa -in "$key_file" -check -noout >/dev/null 2>&1; then # 尝试其他私钥格式 if ! openssl pkey -in "$key_file" -check -noout >/dev/null 2>&1; then echo "❌ 错误:无效的私钥文件 - $key_file" return 1 fi fi return 0}
# 获取证书信息get_cert_info() { local cert_file="$1" echo "📋 证书信息:" echo " 主题: $(openssl x509 -in "$cert_file" -noout -subject | sed 's/subject=//')" echo " 颁发者: $(openssl x509 -in "$cert_file" -noout -issuer | sed 's/issuer=//')" echo " 有效期: $(openssl x509 -in "$cert_file" -noout -dates | grep notBefore | sed 's/notBefore=//') 到 $(openssl x509 -in "$cert_file" -noout -dates | grep notAfter | sed 's/notAfter=//')"
# 提取SAN域名 local san_domains san_domains=$(openssl x509 -in "$cert_file" -noout -text | grep -A1 "Subject Alternative Name" | tail -1 | sed 's/DNS://g' | sed 's/,//g' | sed 's/^[[:space:]]*//') if [ -n "$san_domains" ]; then echo " 支持域名: $san_domains" fi}
# 调用APISIX Admin APIcall_api() { local method="$1" local endpoint="$2" local data="$3"
local url="${API_BASE_URL}${endpoint}" local curl_cmd="curl -s -H 'X-API-KEY: $APISIX_API_KEY'"
if [ "$method" = "GET" ]; then curl_cmd="$curl_cmd '$url'" elif [ "$method" = "DELETE" ]; then curl_cmd="$curl_cmd -X DELETE '$url'" elif [ "$method" = "PUT" ]; then curl_cmd="$curl_cmd -X PUT -d '$data' '$url'" fi
eval "$curl_cmd"}
# 获取证书ID通过序号get_cert_id_by_index() { local index="$1" if [[ "$index" =~ ^[0-9]+$ ]] && [ "$index" -ge 1 ] && [ "$index" -le "${#SSL_CERT_IDS[@]}" ]; then echo "${SSL_CERT_IDS[$((index-1))]}" return 0 else return 1 fi}
# ==================== 主要功能函数 ====================
# 显示当前SSL证书列表show_ssl_list() { echo "🔍 正在获取SSL证书列表..."
# 清空全局数组 SSL_CERT_IDS=() SSL_CERT_SNIS=()
local response response=$(call_api "GET" "/ssls")
if [ $? -ne 0 ]; then echo "❌ 错误:无法连接到APISIX Admin API" return 1 fi
# 使用Python解析JSON响应并填充全局数组 local cert_info cert_info=$(echo "$response" | python3 -c "import json, systry: data = json.load(sys.stdin) if 'list' not in data or len(data['list']) == 0: print('EMPTY') sys.exit(0)
cert_ids = [] cert_snis = []
print('📋 当前SSL证书配置:') print('=' * 80)
for i, ssl in enumerate(data['list'], 1): ssl_data = ssl['value'] cert_id = ssl_data['id'] sni_list = ssl_data['snis']
cert_ids.append(cert_id) cert_snis.append(','.join(sni_list))
print(str(i) + '. ID: ' + cert_id) print(' SNI域名: ' + ', '.join(sni_list))
# 显示过期时间 if 'validity_end' in ssl_data and ssl_data['validity_end']: import datetime expire_time = datetime.datetime.fromtimestamp(ssl_data['validity_end']) print(' 过期时间: ' + expire_time.strftime('%Y-%m-%d %H:%M:%S')) else: print(' 过期时间: 未知')
# 显示更新时间 if 'update_time' in ssl_data: import datetime update_time = datetime.datetime.fromtimestamp(ssl_data['update_time']) print(' 更新时间: ' + update_time.strftime('%Y-%m-%d %H:%M:%S'))
print(' ' + '-' * 60)
# 输出证书ID列表,用于bash数组 print('CERT_IDS:' + '|'.join(cert_ids)) print('CERT_SNIS:' + '|'.join(cert_snis))
except json.JSONDecodeError: print('❌ 错误:API响应格式错误')except Exception as e: print('❌ 错误:' + str(e))")
if echo "$cert_info" | grep -q "EMPTY"; then echo "📝 当前没有配置SSL证书" return 0 fi
if echo "$cert_info" | grep -q "❌ 错误"; then echo "$cert_info" return 1 fi
# 解析证书ID和SNI信息到全局数组 local cert_ids_line cert_snis_line cert_ids_line=$(echo "$cert_info" | grep "^CERT_IDS:" | sed 's/CERT_IDS://') cert_snis_line=$(echo "$cert_info" | grep "^CERT_SNIS:" | sed 's/CERT_SNIS://')
if [ -n "$cert_ids_line" ]; then IFS='|' read -ra SSL_CERT_IDS <<< "$cert_ids_line" IFS='|' read -ra SSL_CERT_SNIS <<< "$cert_snis_line" fi
# 显示证书信息(去除辅助行) echo "$cert_info" | grep -v "^CERT_IDS:" | grep -v "^CERT_SNIS:"}
# 添加SSL证书add_ssl_cert() { echo "➕ 添加SSL证书" echo "=" * 50
# 输入证书ID local cert_id read_input "请输入证书ID(用于标识此证书)" cert_id if [ -z "$cert_id" ]; then echo "❌ 证书ID不能为空" return 1 fi
# 输入证书文件路径 local cert_file read_input "请输入证书文件路径(支持.crt, .pem, .cert等格式)" cert_file if [ -z "$cert_file" ]; then echo "❌ 证书文件路径不能为空" return 1 fi
# 检查证书文件 if ! check_file "$cert_file" || ! validate_cert_file "$cert_file"; then return 1 fi
# 输入私钥文件路径 local key_file read_input "请输入私钥文件路径(支持.key, .pem等格式)" key_file if [ -z "$key_file" ]; then echo "❌ 私钥文件路径不能为空" return 1 fi
# 检查私钥文件 if ! check_file "$key_file" || ! validate_key_file "$key_file"; then return 1 fi
# 显示证书信息 get_cert_info "$cert_file"
# 输入SNI域名 echo "" echo "请输入要绑定的域名(SNI),多个域名用逗号分隔" echo "例如:example.com,*.example.com,api.example.com" local sni_input read_input "SNI域名" sni_input if [ -z "$sni_input" ]; then echo "❌ SNI域名不能为空" return 1 fi
# 处理SNI域名列表 local sni_array="" IFS=',' read -ra ADDR <<< "$sni_input" for domain in "${ADDR[@]}"; do domain=$(trim "$domain") if [ -n "$domain" ]; then if [ -z "$sni_array" ]; then sni_array="\"$domain\"" else sni_array="$sni_array,\"$domain\"" fi fi done
# 确认配置 echo "" echo "📋 配置确认:" echo " 证书ID: $cert_id" echo " 证书文件: $cert_file" echo " 私钥文件: $key_file" echo " SNI域名: $sni_input" echo ""
local confirm read_input "确认添加此SSL证书?(y/N)" confirm "n" if [ "$confirm" != "y" ] && [ "$confirm" != "Y" ]; then echo "❌ 操作已取消" return 1 fi
# 读取证书和私钥内容 echo "🔄 正在读取证书文件..." local cert_content cert_content=$(cat "$cert_file" | sed ':a;N;$!ba;s/\n/\\n/g')
echo "🔄 正在读取私钥文件..." local key_content key_content=$(cat "$key_file" | sed ':a;N;$!ba;s/\n/\\n/g')
# 构建JSON数据 local json_data="{ \"cert\": \"$cert_content\", \"key\": \"$key_content\", \"snis\": [$sni_array] }"
# 调用API添加证书 echo "🔄 正在添加SSL证书..." local response response=$(call_api "PUT" "/ssls/$cert_id" "$json_data")
if echo "$response" | grep -q "\"key\""; then echo "✅ SSL证书添加成功!" echo " 证书ID: $cert_id" echo " 绑定域名: $sni_input" else echo "❌ SSL证书添加失败" echo " 错误信息: $response" return 1 fi}
# 删除SSL证书delete_ssl_cert() { echo "🗑️ 删除SSL证书" echo "=" * 50
# 先显示当前证书列表 show_ssl_list
if [ ${#SSL_CERT_IDS[@]} -eq 0 ]; then echo "没有可删除的SSL证书" return 0 fi
echo "" echo "请选择要删除的证书:" echo "输入序号 (1-${#SSL_CERT_IDS[@]}) 或直接输入证书ID"
# 输入要删除的证书 local cert_input read_input "证书序号或ID" cert_input if [ -z "$cert_input" ]; then echo "❌ 输入不能为空" return 1 fi
# 判断输入的是序号还是证书ID local cert_id if [[ "$cert_input" =~ ^[0-9]+$ ]]; then # 输入的是序号 cert_id=$(get_cert_id_by_index "$cert_input") if [ $? -ne 0 ]; then echo "❌ 无效的序号: $cert_input" return 1 fi echo "选择的证书: $cert_id (序号: $cert_input)" else # 输入的是证书ID cert_id="$cert_input" echo "选择的证书: $cert_id" fi
# 确认删除 local confirm read_input "确认删除SSL证书 '$cert_id'?(y/N)" confirm "n" if [ "$confirm" != "y" ] && [ "$confirm" != "Y" ]; then echo "❌ 操作已取消" return 1 fi
# 调用API删除证书 echo "🔄 正在删除SSL证书..." local response response=$(call_api "DELETE" "/ssls/$cert_id")
if echo "$response" | grep -q "\"deleted\""; then echo "✅ SSL证书删除成功!" echo " 证书ID: $cert_id" else echo "❌ SSL证书删除失败" echo " 错误信息: $response" return 1 fi}
# 更新域名绑定的证书update_domain_cert() { echo "🔄 更新域名绑定的证书" echo "=" * 50
# 先显示当前证书列表 show_ssl_list
if [ ${#SSL_CERT_IDS[@]} -eq 0 ]; then echo "没有可更新的SSL证书" return 0 fi
echo "" echo "请选择要更新的证书:" echo "输入序号 (1-${#SSL_CERT_IDS[@]}) 或直接输入证书ID"
# 输入要更新的证书 local cert_input read_input "证书序号或ID" cert_input if [ -z "$cert_input" ]; then echo "❌ 输入不能为空" return 1 fi
# 判断输入的是序号还是证书ID local cert_id if [[ "$cert_input" =~ ^[0-9]+$ ]]; then # 输入的是序号 cert_id=$(get_cert_id_by_index "$cert_input") if [ $? -ne 0 ]; then echo "❌ 无效的序号: $cert_input" return 1 fi echo "选择的证书: $cert_id (序号: $cert_input)" else # 输入的是证书ID cert_id="$cert_input" echo "选择的证书: $cert_id" fi
# 获取当前证书信息 echo "🔍 正在获取当前证书信息..." local current_response current_response=$(call_api "GET" "/ssls/$cert_id")
if ! echo "$current_response" | grep -q "\"snis\""; then echo "❌ 错误:找不到证书ID '$cert_id'" return 1 fi
# 显示当前SNI信息 echo "$current_response" | python3 -c "import json, systry: data = json.load(sys.stdin) ssl_data = data['value'] print('📋 当前证书信息:') print(' 证书ID: ' + ssl_data['id']) print(' 当前SNI域名: ' + ', '.join(ssl_data['snis']))except: print('❌ 错误:无法解析证书信息') sys.exit(1)"
if [ $? -ne 0 ]; then return 1 fi
echo "" echo "选择更新方式:" echo "1. 更新证书文件(保持域名不变)" echo "2. 更新SNI域名(保持证书不变)" echo "3. 同时更新证书文件和SNI域名"
local update_type read_input "请选择更新方式 (1-3)" update_type
case "$update_type" in 1) update_cert_files "$cert_id" "$current_response" ;; 2) update_sni_domains "$cert_id" "$current_response" ;; 3) update_cert_and_sni "$cert_id" ;; *) echo "❌ 无效的选择" return 1 ;; esac}
# 更新证书文件update_cert_files() { local cert_id="$1" local current_response="$2"
echo "🔄 更新证书文件"
# 输入新的证书文件路径 local cert_file read_input "请输入新的证书文件路径" cert_file if [ -z "$cert_file" ] || ! check_file "$cert_file" || ! validate_cert_file "$cert_file"; then return 1 fi
# 输入新的私钥文件路径 local key_file read_input "请输入新的私钥文件路径" key_file if [ -z "$key_file" ] || ! check_file "$key_file" || ! validate_key_file "$key_file"; then return 1 fi
# 显示新证书信息 get_cert_info "$cert_file"
# 获取当前SNI域名 local current_snis current_snis=$(echo "$current_response" | python3 -c "import json, sysdata = json.load(sys.stdin)snis = data['value']['snis']print(','.join(['\"' + sni + '\"' for sni in snis]))")
# 确认更新 local confirm read_input "确认更新证书文件?(y/N)" confirm "n" if [ "$confirm" != "y" ] && [ "$confirm" != "Y" ]; then echo "❌ 操作已取消" return 1 fi
# 执行更新 perform_cert_update "$cert_id" "$cert_file" "$key_file" "$current_snis"}
# 更新SNI域名update_sni_domains() { local cert_id="$1" local current_response="$2"
echo "🔄 更新SNI域名"
# 输入新的SNI域名 local sni_input read_input "请输入新的SNI域名(多个域名用逗号分隔)" sni_input if [ -z "$sni_input" ]; then echo "❌ SNI域名不能为空" return 1 fi
# 处理SNI域名列表 local sni_array="" IFS=',' read -ra ADDR <<< "$sni_input" for domain in "${ADDR[@]}"; do domain=$(trim "$domain") if [ -n "$domain" ]; then if [ -z "$sni_array" ]; then sni_array="\"$domain\"" else sni_array="$sni_array,\"$domain\"" fi fi done
# 获取当前证书内容 local current_cert current_key current_cert=$(echo "$current_response" | python3 -c "import json, sysdata = json.load(sys.stdin)print(data['value']['cert'])") current_key=$(echo "$current_response" | python3 -c "import json, sysdata = json.load(sys.stdin)print(data['value']['key'])")
# 确认更新 local confirm read_input "确认更新SNI域名为 '$sni_input'?(y/N)" confirm "n" if [ "$confirm" != "y" ] && [ "$confirm" != "Y" ]; then echo "❌ 操作已取消" return 1 fi
# 构建JSON数据 local json_data="{ \"cert\": \"$current_cert\", \"key\": \"$current_key\", \"snis\": [$sni_array] }"
# 执行更新 echo "🔄 正在更新SNI域名..." local response response=$(call_api "PUT" "/ssls/$cert_id" "$json_data")
if echo "$response" | grep -q "\"key\""; then echo "✅ SNI域名更新成功!" echo " 证书ID: $cert_id" echo " 新域名: $sni_input" else echo "❌ SNI域名更新失败" echo " 错误信息: $response" return 1 fi}
# 同时更新证书和SNIupdate_cert_and_sni() { local cert_id="$1"
echo "🔄 同时更新证书文件和SNI域名"
# 输入新的证书文件路径 local cert_file read_input "请输入新的证书文件路径" cert_file if [ -z "$cert_file" ] || ! check_file "$cert_file" || ! validate_cert_file "$cert_file"; then return 1 fi
# 输入新的私钥文件路径 local key_file read_input "请输入新的私钥文件路径" key_file if [ -z "$key_file" ] || ! check_file "$key_file" || ! validate_key_file "$key_file"; then return 1 fi
# 显示新证书信息 get_cert_info "$cert_file"
# 输入新的SNI域名 local sni_input read_input "请输入新的SNI域名(多个域名用逗号分隔)" sni_input if [ -z "$sni_input" ]; then echo "❌ SNI域名不能为空" return 1 fi
# 处理SNI域名列表 local sni_array="" IFS=',' read -ra ADDR <<< "$sni_input" for domain in "${ADDR[@]}"; do domain=$(trim "$domain") if [ -n "$domain" ]; then if [ -z "$sni_array" ]; then sni_array="\"$domain\"" else sni_array="$sni_array,\"$domain\"" fi fi done
# 确认更新 echo "" echo "📋 更新确认:" echo " 证书ID: $cert_id" echo " 新证书文件: $cert_file" echo " 新私钥文件: $key_file" echo " 新SNI域名: $sni_input" echo ""
local confirm read_input "确认执行更新?(y/N)" confirm "n" if [ "$confirm" != "y" ] && [ "$confirm" != "Y" ]; then echo "❌ 操作已取消" return 1 fi
# 执行更新 perform_cert_update "$cert_id" "$cert_file" "$key_file" "$sni_array"}
# 执行证书更新perform_cert_update() { local cert_id="$1" local cert_file="$2" local key_file="$3" local sni_array="$4"
# 读取证书和私钥内容 echo "🔄 正在读取证书文件..." local cert_content cert_content=$(cat "$cert_file" | sed ':a;N;$!ba;s/\n/\\n/g')
echo "🔄 正在读取私钥文件..." local key_content key_content=$(cat "$key_file" | sed ':a;N;$!ba;s/\n/\\n/g')
# 构建JSON数据 local json_data="{ \"cert\": \"$cert_content\", \"key\": \"$key_content\", \"snis\": [$sni_array] }"
# 调用API更新证书 echo "🔄 正在更新SSL证书..." local response response=$(call_api "PUT" "/ssls/$cert_id" "$json_data")
if echo "$response" | grep -q "\"key\""; then echo "✅ SSL证书更新成功!" echo " 证书ID: $cert_id" else echo "❌ SSL证书更新失败" echo " 错误信息: $response" return 1 fi}
# 测试SSL证书test_ssl_cert() { echo "🧪 测试SSL证书" echo "=" * 50
# 输入要测试的域名 local domain read_input "请输入要测试的域名" domain if [ -z "$domain" ]; then echo "❌ 域名不能为空" return 1 fi
echo "🔄 正在测试SSL证书..."
# 测试SSL连接 echo "1. 测试SSL连接..." if echo | timeout 10 openssl s_client -connect "${APISIX_HOST}:443" -servername "$domain" >/dev/null 2>&1; then echo " ✅ SSL连接成功" else echo " ❌ SSL连接失败" fi
# 获取证书信息 echo "2. 获取证书信息..." local cert_info cert_info=$(echo | timeout 10 openssl s_client -connect "${APISIX_HOST}:443" -servername "$domain" 2>/dev/null | openssl x509 -noout -subject -issuer -dates 2>/dev/null)
if [ -n "$cert_info" ]; then echo " ✅ 证书信息获取成功" echo "$cert_info" | sed 's/^/ /' else echo " ❌ 无法获取证书信息" fi
# 测试HTTPS访问 echo "3. 测试HTTPS访问..." local http_status http_status=$(curl -s -o /dev/null -w "%{http_code}" --connect-timeout 10 "https://$domain/" 2>/dev/null)
if [ "$http_status" = "200" ] || [ "$http_status" = "301" ] || [ "$http_status" = "302" ]; then echo " ✅ HTTPS访问成功 (HTTP状态码: $http_status)" else echo " ⚠️ HTTPS访问异常 (HTTP状态码: $http_status)" fi}
# 显示配置信息show_config() { echo "⚙️ 当前配置信息" echo "=" * 50 echo "APISIX主机: $APISIX_HOST" echo "Admin端口: $APISIX_ADMIN_PORT" echo "API密钥: ${APISIX_API_KEY:0:8}***" echo "API地址: $API_BASE_URL" echo ""
# 测试API连接 echo "🔄 测试API连接..." local response response=$(call_api "GET" "/ssls" 2>/dev/null)
if echo "$response" | grep -q "list"; then echo "✅ API连接正常" else echo "❌ API连接失败" echo " 请检查APISIX服务状态和配置" fi}
# ==================== 主菜单 ====================
show_menu() { echo "" echo "🔐 APISIX SSL证书管理工具" echo "=" * 50 echo "1. 查看SSL证书列表" echo "2. 添加SSL证书" echo "3. 删除SSL证书" echo "4. 更新域名绑定的证书" echo "5. 测试SSL证书" echo "6. 显示配置信息" echo "0. 退出" echo "=" * 50}
# 主程序main() { # 检查依赖 if ! command -v curl >/dev/null 2>&1; then echo "❌ 错误:需要安装curl" exit 1 fi
if ! command -v openssl >/dev/null 2>&1; then echo "❌ 错误:需要安装openssl" exit 1 fi
if ! command -v python3 >/dev/null 2>&1; then echo "❌ 错误:需要安装python3" exit 1 fi
# 显示欢迎信息 echo "🔐 APISIX SSL证书管理工具 v1.2" echo "当前配置 - 主机: $APISIX_HOST, 端口: $APISIX_ADMIN_PORT"
# 主循环 while true; do show_menu
local choice read_input "请选择操作" choice
case "$choice" in 1) show_ssl_list ;; 2) add_ssl_cert ;; 3) delete_ssl_cert ;; 4) update_domain_cert ;; 5) test_ssl_cert ;; 6) show_config ;; 0) echo "👋 再见!" exit 0 ;; *) echo "❌ 无效的选择,请重新输入" ;; esac
echo "" read -p "按回车键继续..." -r done}
# 运行主程序main "$@"apisix_export/apisix_consumers.json
{ "total": 0, "list": []}apisix_export/apisix_global_rules.json
{ "total": 1, "list": [ { "value": { "plugins": { "prometheus": { "_meta": { "disable": false } } }, "create_time": 1750493067, "id": "1", "update_time": 1750493067 }, "modifiedIndex": 351, "createdIndex": 351, "key": "/apisix/global_rules/1" } ]}apisix_export/apisix_plugins.json
[ "real-ip", "ai", "client-control", "proxy-control", "request-id", "zipkin", "ext-plugin-pre-req", "fault-injection", "mocking", "serverless-pre-function", "cors", "ip-restriction", "ua-restriction", "referer-restriction", "csrf", "uri-blocker", "request-validation", "chaitin-waf", "multi-auth", "openid-connect", "cas-auth", "authz-casbin", "authz-casdoor", "wolf-rbac", "ldap-auth", "hmac-auth", "basic-auth", "jwt-auth", "jwe-decrypt", "key-auth", "consumer-restriction", "attach-consumer-label", "forward-auth", "opa", "authz-keycloak", "proxy-cache", "body-transformer", "ai-prompt-guard", "ai-prompt-template", "ai-prompt-decorator", "ai-rag", "ai-aws-content-moderation", "ai-proxy-multi", "ai-proxy", "ai-rate-limiting", "proxy-mirror", "proxy-rewrite", "workflow", "api-breaker", "limit-conn", "limit-count", "limit-req", "gzip", "server-info", "traffic-split", "redirect", "response-rewrite", "degraphql", "kafka-proxy", "grpc-transcode", "grpc-web", "http-dubbo", "public-api", "prometheus", "datadog", "loki-logger", "elasticsearch-logger", "echo", "loggly", "http-logger", "splunk-hec-logging", "skywalking-logger", "google-cloud-logging", "sls-logger", "tcp-logger", "kafka-logger", "rocketmq-logger", "syslog", "udp-logger", "file-logger", "clickhouse-logger", "tencent-cloud-cls", "inspect", "example-plugin", "aws-lambda", "azure-functions", "openwhisk", "openfunction", "serverless-post-function", "ext-plugin-post-req", "ext-plugin-post-resp"]apisix_export/apisix_plugin_configs.json
{ "total": 0, "list": []}apisix_export/apisix_routes.json
{ "total": 1, "list": [ { "value": { "create_time": 1750424231, "upstream_id": "java-app-upstream", "status": 1, "host": "csapi.twenhub.com", "desc": "\u901a\u7528\u8def\u7531 - \u5904\u7406\u6240\u6709\u8bf7\u6c42", "priority": 1, "methods": [ "GET", "POST", "PUT", "DELETE", "OPTIONS", "HEAD", "PATCH" ], "name": "universal-route", "update_time": 1750666631, "id": "00000000000000000053", "uri": "/*" }, "modifiedIndex": 561, "createdIndex": 54, "key": "/apisix/routes/00000000000000000053" } ]}apisix_export/apisix_services.json
{ "total": 0, "list": []}apisix_export/apisix_ssls.json
{ "total": 1, "list": [ { "value": { "snis": [ "*.twenhub.com", "twenhub.com", "csapi.twenhub.com", "csh5.twenhub.com" ], "create_time": 1750670207, "type": "server", "status": 1, "update_time": 1750670207, "cert": "-----BEGIN CERTIFICATE-----\nMIIGYjCCBMqgAwIBAgIQJYW/rQxALl6fxyeb7bQInzANBgkqhkiG9w0BAQsFADBK\nMQswCQYDVQQGEwJVUzEUMBIGA1UECgwLTGVvY2VydCBMTEMxJTAjBgNVBAMMHExl\nb2NlcnQgVExTIElzc3VpbmcgUlNBIENBIDEwHhcNMjUwNjAyMDYxMTQyWhcNMjYw\nNzAzMDYxMTQyWjAYMRYwFAYDVQQDDA0qLnR3ZW5odWIuY29tMIIBIjANBgkqhkiG\n9w0BAQEFAAOCAQ8AMIIBCgKCAQEA1xBi+eGhG5rVlFF8Mn7cYZUpkJscobgsFwqQ\na2VRKngu9ib2lcJ3yT+w4Vv6E2Ua6rKZwn0BcYjQVVyiZUZX3D56kCAeRf9PdS+m\nIAyhHbS/acSOUH6ggXarkIDzOvGDyaCR9Q9c9Jm+gvorfyyIFARnnQWopq0CRiMb\nBO+qfvex/EUHwlBSh50hK9Tntq+n0UEGzFae1DR+oFp4CGxkoXsRSRFv5aVGeQPj\nVri8+n7k9ZzxZwtls3iHS47wv+gqIRs/rs+N98JxCrkLrzkWzpDTR698yfevhsvX\niBVCia6wCkF/PC3k7tSVAbZOaMoummsXb0F5BByk6nC456AZLwIDAQABo4IC9DCC\nAvAwDAYDVR0TAQH/BAIwADAfBgNVHSMEGDAWgBQX2dPy0GnoHje3+VY6B+Yp05Vb\nkTBmBggrBgEFBQcBAQRaMFgwNAYIKwYBBQUHMAKGKGh0dHA6Ly9jZXJ0LnNzbC5j\nb20vTGVvY2VydC1UTFMtSS1SMS5jZXIwIAYIKwYBBQUHMAGGFGh0dHA6Ly9vY3Nw\ncy5zc2wuY29tMCUGA1UdEQQeMByCDSoudHdlbmh1Yi5jb22CC3R3ZW5odWIuY29t\nMCMGA1UdIAQcMBowCAYGZ4EMAQIBMA4GDCsGAQQBgqkwAQMBATAdBgNVHSUEFjAU\nBggrBgEFBQcDAgYIKwYBBQUHAwEwOQYDVR0fBDIwMDAuoCygKoYoaHR0cDovL2Ny\nbHMuc3NsLmNvbS9MZW9jZXJ0LVRMUy1JLVIxLmNybDAdBgNVHQ4EFgQUyQqQkcAW\nlwmp6/HI8MqaixgjnjkwDgYDVR0PAQH/BAQDAgWgMIIBgAYKKwYBBAHWeQIEAgSC\nAXAEggFsAWoAdwDLOPcViXyEoURfW8Hd+8lu8ppZzUcKaQWFsMsUwxRY5wAAAZcv\nTZlUAAAEAwBIMEYCIQCCUbAmZ57pq5mXPNt0Z26O91cTA0i13vn/sRNt8xQKMQIh\nAK0KUqNMmI7nqe64SKdeptYzfBLzyv2FBlcfmDABevMSAHcAlE5Dh/rswe+B8xkk\nJqgYZQHH0184AgE/cmd9VTcuGdgAAAGXL02ZaAAABAMASDBGAiEAtpxdjDjgByRb\nTmCPyQzC4DkDfbXGuH38rTuQ+scPDVsCIQChIQBHPt1k3ab96GXoifBJQHkDEBw/\nigw4vhUIVEoJigB2ANgJVTuUT3r/yBYZb5RPhauw+Pxeh1UmDxXRLnK7RUsUAAAB\nly9NmT4AAAQDAEcwRQIgb7Va+NWKhA6vxz2cEOP5u5opzfIXe9cRQ5qbbSfZfrMC\nIQDHP8WQdIOJdgADWgfRymgzJL60EVS5oy0kPPIaWBHkmjANBgkqhkiG9w0BAQsF\nAAOCAYEAJ9IKAhFG+EAGHs42t4sqvED6YkL0EwUTqRm+B5AcCOc2dh22pCvrPqVY\nKSzQDm/Aa2oIdH7ZrAYP9NDTufyvwJFYALWaeZvkIogid1ukcXQblwgWZ9byOv+/\nCPqiC5iriw2fDuAJVOtXpi7wOkFXUYCGxq5ZEL6fXzHxRYTTDN42hRISn/4/coJX\nH8O/0fV+7XvW/ZDG9QebFtG/3IbFsRUh6EobweOCXOYUYYW1kZuHeCyN/9BVl8LS\nt1WyoW1gumeBeJtCoMOoTJ4LfGjr49BhmoNvZYJApzUXk4PrW2i+SNmk2tJEcqee\nY+SFecihv94sMOx2NLdBmndvRoaQL1n08QjcjC1LjH+IUs72pscFXT62vquaQf1D\nMsZdwoa4dcy9s89CihDyNTWlJQr/ZOfuBRP1Tp1p0RPOKNFl9DS33eAr4kTzwWxv\nbOrdZDWdcAgQDYePJP+91agQwYXQszt6bhyneKm3kGMj2qHtLb1vhjT0dv0nrMTP\n7/wyebJK\n-----END CERTIFICATE-----\r\n-----BEGIN CERTIFICATE-----\n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\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIFfTCCBGWgAwIBAgIQUl2fDZZ6EH1gqmdbOsHpyDANBgkqhkiG9w0BAQsFADB7\nMQswCQYDVQQGEwJHQjEbMBkGA1UECAwSR3JlYXRlciBNYW5jaGVzdGVyMRAwDgYD\nVQQHDAdTYWxmb3JkMRowGAYDVQQKDBFDb21vZG8gQ0EgTGltaXRlZDEhMB8GA1UE\nAwwYQUFBIENlcnRpZmljYXRlIFNlcnZpY2VzMB4XDTI0MDYyMTAwMDAwMFoXDTI4\nMTIzMTIzNTk1OVowTzELMAkGA1UEBhMCVVMxGDAWBgNVBAoMD1NTTCBDb3Jwb3Jh\ndGlvbjEmMCQGA1UEAwwdU1NMLmNvbSBUTFMgVHJhbnNpdCBSU0EgQ0EgUjIwggIi\nMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC6lZ8Z2WYAqAXZr43sbFCdaz+5\nMU8liFkrqzD5CcOL8AOaKtB1Ggx5t8GjUibUAYvuCNXeUzd4awBM4ARRoWp8xzRI\n+AG9YxUH5lWtsmyhfyW4E7VvpRpi5s3tOyQVv8rCpdNB0R/HYM4+wPxigadbL2Cc\n46fTcbeV2MBPOc9lyVJASewIXQQhI0+CsKOVA4AD3Cj27QhnpvJONchn+hVgvTTD\nVSkmlrvaFjH3KPzEJsRIiv6Wyc1bpkDyV6+bPzkzXtntXhdXb/RrAcky7ldxN5I0\nxZd2ESfMUU6yQbqkIfAaBj1Uxd7y780ir5beWijPQU1VsU4NcEpxwQ8CkcFK7Gos\nXIAOS1XhFbbO7QpfF6Nxe8QGAcC5Dx38f5vubR3PMheivltQSxlGJOhMlidqxehx\nbaxH47wRQoT2p/3MXHBDydUetOwJLlRxct6ms9px9qD8ubnUX4TmUc88tfWzFM8t\nWrKNrhQjo/GB/585rBv+MZ+8JwjySCjY0Tiy4WcwBo3sacCyXXWGoJH2hoq2QbVH\nW0QpQLSQ2wXClnyo6juVXRW++ZwtCu2MOx5xnadnqD2u/2fH+EAHsCplAtICVR8j\n/GeFQGWbdPSXTUrSy0OKm9BiQjJ9GCw4eUZa8zYbOKnsFH+2NtZN4pYLP3M7wS8I\nWceGUEkSP5Izm+qWZwIDAQABo4IBJzCCASMwHwYDVR0jBBgwFoAUoBEKIz6W8Qfs\n4q8p74Klf9AwpLQwHQYDVR0OBBYEFCFBNGOOHZEMw922JLCoh3uNZWAIMA4GA1Ud\nDwEB/wQEAwIBhjASBgNVHRMBAf8ECDAGAQH/AgEBMB0GA1UdJQQWMBQGCCsGAQUF\nBwMBBggrBgEFBQcDAjAjBgNVHSAEHDAaMAgGBmeBDAECATAOBgwrBgEEAYKpMAED\nAQEwQwYDVR0fBDwwOjA4oDagNIYyaHR0cDovL2NybC5jb21vZG9jYS5jb20vQUFB\nQ2VydGlmaWNhdGVTZXJ2aWNlcy5jcmwwNAYIKwYBBQUHAQEEKDAmMCQGCCsGAQUF\nBzABhhhodHRwOi8vb2NzcC5jb21vZG9jYS5jb20wDQYJKoZIhvcNAQELBQADggEB\nACZcAf5T5xGRWOoV+HvXXdj8P61F5OxDxY/lC/6feUgFY8qUO9yPipfxlDOgelM0\nYgFkHK4GLCuIKgfP+QpREuKMkJP6k4tylKAEdr+DdFgwHJEWwbPFg/sZHYWzUPEN\nc1ypStXWHXOXlk/fbIylocSSbqN9oRM50+YKeLFy6+1jK12QJVJKMiLYKxiRvhCa\nWyqLhFsVGeNNgbOdrkjRBNTYSv8s2qJA2xZSYF1bkeo3bK94qx/FTbwCOigSsK6y\neLeb585rLBNiO5MhPTY85Cbb8aZfkUryrqJoy993BoyKp9YDiM8QA3Mome64gxVZ\nFNKy3ieVQFNDMoHK9DZgfLk=\n-----END CERTIFICATE-----", "id": "1", "key": "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" }, "modifiedIndex": 569, "createdIndex": 569, "key": "/apisix/ssls/1" } ]}apisix_export/apisix_stream_routes.json
{ "error_msg": "stream mode is disabled, can not add stream routes"}apisix_export/apisix_upstreams.json
{ "total": 1, "list": [ { "value": { "create_time": 1750395883, "retries": 2, "scheme": "http", "nodes": { "172.18.0.1:8080": 1, "172.18.0.1:8081": 1 }, "timeout": { "send": 300, "read": 300, "connect": 5 }, "type": "least_conn", "retry_timeout": 60, "keepalive_pool": { "size": 320, "idle_timeout": 30, "requests": 50 }, "checks": { "active": { "http_path": "/health", "https_verify_certificate": true, "healthy": { "interval": 3, "successes": 3, "http_statuses": [ 200, 302 ] }, "unhealthy": { "http_statuses": [ 429, 404, 500, 501, 502, 503, 504, 505 ], "http_failures": 3, "tcp_failures": 3, "timeouts": 2, "interval": 3 }, "timeout": 5, "type": "http", "concurrency": 3 }, "passive": { "unhealthy": { "tcp_failures": 3, "timeouts": 2, "http_statuses": [ 429, 500, 503 ], "http_failures": 3 }, "type": "http", "healthy": { "http_statuses": [ 200, 201, 202, 203, 204, 205, 206, 207, 208, 226, 300, 301, 302, 303, 304, 305, 306, 307, 308 ], "successes": 2 } } }, "pass_host": "pass", "hash_on": "vars", "update_time": 1750653386, "id": "java-app-upstream", "name": "java-app-upstream" }, "modifiedIndex": 551, "createdIndex": 16, "key": "/apisix/upstreams/java-app-upstream" } ]}apisix_export/export_summary.md
# APISIX 配置导出报告
## 导出信息- **导出时间**: 2025年06月24日 09:50:33- **服务器地址**: 1.2.3.4:9180- **导出目录**: ./apisix_export
## 配置文件列表- ✓ apisix_routes.json (4.0K)- ✓ apisix_upstreams.json (4.0K)- ✓ apisix_services.json (4.0K)- ✓ apisix_consumers.json (4.0K)- ✓ apisix_ssls.json (12K)- ✓ apisix_global_rules.json (4.0K)- ✓ apisix_plugin_configs.json (4.0K)- ✓ apisix_stream_routes.json (4.0K)- ✓ apisix_plugins.json (4.0K)
## 使用说明1. 所有配置文件均为 JSON 格式2. 可使用本脚本的导入功能恢复配置3. 建议定期备份配置文件
---*由 APISIX 配置管理工具自动生成*ecs/demo-node1/deploy.sh
#!/usr/bin/env bash## zero-deploy.sh - 零停机部署脚本(CentOS优化版 2025‑06‑A)## 关键改动# 1) 停止操作不再依赖jar文件,只需端口号# 2) jar文件参数在停止操作时为可选# 3) 针对CentOS环境优化进程检测# 4) 单节点 PATCH,完全符合 APISIX Admin API 最佳实践# -------------------------------------------------------------------
set -euo pipefail
####################### 可调参数 #####################################BASE_DIR="$(dirname "$(readlink -f "$0")")"LOG_DIR="$BASE_DIR/logs"
LOG_LEVEL="${LOG_LEVEL:-INFO}" # INFO | DEBUG
PORT1=8080 ; PROFILE1="prod-node1"PORT2=8081 ; PROFILE2="prod-node2"
HEALTH_PATH="/health"HEALTH_TIMEOUT=60APP_START_TIMEOUT_SECONDS=$HEALTH_TIMEOUT
KEEPALIVE_IDLE_TIMEOUT=30 # 与 upstream.keepalive_pool.idle_timeout 对齐IDLE_TIMEOUT_BUFFER=$((KEEPALIVE_IDLE_TIMEOUT*2))SHUTDOWN_TIMEOUT_SECONDS=60 # 优雅停机超时时间(秒)
JAR_LINK="app.jar"UPSTREAM_ID="java-app-upstream"ADMIN_URL="http://127.0.0.1:9180/apisix/admin/upstreams/$UPSTREAM_ID"
HOST="172.18.0.1" # 可用 `ip route get 1.1.1.1 | awk '{print $7}'`AUTH_HEADER=(-H "X-API-KEY: CKjcGDoqxcJfpIXWigyqjdUSACvwzEJy") # 请替换########################################################################
mkdir -p "$LOG_DIR"
###################### 通用函数 ######################################## 检查必要工具是否存在check_tools() { local missing_tools=()
# 检查必要的命令 command -v curl >/dev/null || missing_tools+=("curl") command -v jq >/dev/null || missing_tools+=("jq")
# 对于停止操作,检查进程查找工具 if [[ "$1" =~ ^(2|4)$ ]]; then if ! command -v lsof >/dev/null && ! command -v ss >/dev/null && ! command -v netstat >/dev/null; then missing_tools+=("lsof或ss或netstat") fi fi
if [[ ${#missing_tools[@]} -gt 0 ]]; then echo "错误: 缺少必要工具: ${missing_tools[*]}" >&2 echo "在CentOS上安装: yum install -y lsof jq curl" >&2 exit 1 fi}
# API 调用(带重试)api() { local method="GET" url data attempt=1 max=3 delay=1 while [[ $# -gt 0 ]]; do case $1 in -X) method="$2"; shift 2;; -d) data="$2"; shift 2;; http*) url="$1"; shift;; *) shift;; esac done while :; do [[ $LOG_LEVEL == "DEBUG" ]] && echo "API attempt $attempt: $method $url" local rsp http_code body if [[ -n ${data:-} ]]; then rsp=$(curl -s -w '\n%{http_code}' --fail "${AUTH_HEADER[@]}" -X "$method" \ -H 'Content-Type: application/json' -d "$data" "$url" 2>&1) || true else rsp=$(curl -s -w '\n%{http_code}' --fail "${AUTH_HEADER[@]}" -X "$method" \ "$url" 2>&1) || true fi http_code=$(echo "$rsp" | tail -n1) body=$(echo "$rsp" | head -n -1) [[ $LOG_LEVEL == "DEBUG" ]] && echo "Response code: $http_code"
if [[ $http_code =~ ^2[0-9][0-9]$ ]]; then [[ $LOG_LEVEL == "DEBUG" ]] && echo "HTTP $method $url -> $http_code SUCCESS" [[ -n $body && $LOG_LEVEL == "DEBUG" ]] && echo "Response body: $body" echo "$body" return 0 fi if (( attempt >= max )); then echo "HTTP $method $url FAILED after $attempt attempts, code=$http_code, body=$body" return 1 fi echo "HTTP $method $url attempt $attempt failed, retry after ${delay}s..." sleep $delay ((attempt++)) delay=$((delay*2)) done}
# 获取当前 upstream.nodesget_current_nodes() { api -X GET "$ADMIN_URL" | jq -r '.value.nodes // {}'}
###################### 节点操作 #######################################add_node() { # add_node host:port [weight] local node="$1" weight="${2:-0}" local nodes=$(get_current_nodes) if echo "$nodes" | jq -e --arg n "$node" 'has($n)' >/dev/null; then echo "Node $node already exists" return 0 fi local patch=$( jq -n --arg node "$node" --argjson w "$weight" '{nodes:{($node):$w}}') api -X PATCH -d "$patch" "$ADMIN_URL" echo "Node $node added with weight=$weight"}
update_node_weight() { # update_node_weight host:port weight local node="$1" weight="$2" [[ $LOG_LEVEL == "DEBUG" ]] && echo "update_node_weight: node=$node, weight=$weight" [[ $LOG_LEVEL == "DEBUG" ]] && echo "Creating patch..." local patch=$( jq -n --arg node "$node" --argjson w "$weight" '{nodes:{($node):$w}}') [[ $LOG_LEVEL == "DEBUG" ]] && echo "Calling API: PATCH $ADMIN_URL with patch: $patch" api -X PATCH -d "$patch" "$ADMIN_URL" >/dev/null [[ $LOG_LEVEL == "DEBUG" ]] && echo "API call completed" echo "Node $node weight updated -> $weight"}
remove_node() { # remove_node host:port local node="$1" local patch=$( jq -n --arg node "$node" '{nodes:{($node):null}}') api -X PATCH -d "$patch" "$ADMIN_URL" echo "Node $node removed from upstream"}
###################### 进程检测工具 #################################### CentOS兼容的端口进程检测get_pid_by_port() { local port="$1" local pid=""
# 优先使用lsof if command -v lsof >/dev/null; then pid=$(lsof -ti tcp:"$port" -sTCP:LISTEN 2>/dev/null || true) # 备用使用ss (较新的CentOS) elif command -v ss >/dev/null; then pid=$(ss -tlnp | grep ":$port " | sed 's/.*pid=\([0-9]*\).*/\1/' | head -n1 || true) # 最后使用netstat elif command -v netstat >/dev/null; then pid=$(netstat -tlnp 2>/dev/null | grep ":$port " | awk '{print $7}' | cut -d'/' -f1 | head -n1 || true) fi
# 验证PID是否有效 if [[ -n $pid && $pid =~ ^[0-9]+$ ]]; then if kill -0 "$pid" 2>/dev/null; then echo "$pid" return 0 fi fi
return 1}
# 检查端口是否被占用is_port_in_use() { local port="$1" get_pid_by_port "$port" >/dev/null}
###################### 健康检查工具 ###################################health_check() { local url="$1" local timeout="${2:-$APP_START_TIMEOUT_SECONDS}" local exptime=0
echo "开始健康检查: $url" while true; do status_code=$(curl -L -o /dev/null --connect-timeout 5 -s -w %{http_code} "${url}" 2>/dev/null) if [ "$?" == "0" ] && [ "$status_code" == "200" ]; then echo "健康检查通过 (HTTP $status_code)" return 0 fi
echo "等待应用启动: $((exptime+1))/$timeout 秒 (状态码:${status_code:-连接失败})"
sleep 1 || true exptime=$((exptime + 1))
if [ $exptime -gt ${timeout} ]; then echo "健康检查超时 (${timeout}秒),应用启动失败" return 1 fi done}
wait_up() { health_check "http://$HOST:$1/health"; }wait_down() { ! health_check "http://$HOST:$1/health"; }
wait_apisix_healthy() { # port local node="$HOST:$1" [[ $LOG_LEVEL == "DEBUG" ]] && echo "等待APISIX配置下发: $node"
for i in $(seq 0 14); do weight=$(get_current_nodes | jq -r --arg n "$node" '.[$n] // empty' 2>/dev/null) if [[ -n $weight ]]; then [[ $LOG_LEVEL == "DEBUG" ]] && echo "APISIX配置已下发: $node (权重:$weight)" return 0 fi echo "等待APISIX配置下发: $((i+1))/15 秒" sleep 1 || true done echo "APISIX配置下发超时,但继续执行..." return 1}
###################### 启停节点 #######################################start_node() { # no port profile local no="$1"; local port="$2"; local profile="$3"; local node="$HOST:$port" echo "==== 开始启动 Node$no (端口:$port) ===="
# 步骤1: 检查端口占用 echo "步骤1/5: 检查端口可用性..." if is_port_in_use "$port"; then echo "端口 $port 已被占用,无法启动" return 1 fi echo "端口 $port 可用"
# 步骤2: 检查jar文件 echo "步骤2/5: 检查应用文件..." if [[ ! -f "$JAR_LINK" ]]; then echo "错误: 应用文件 $JAR_LINK 不存在" return 1 fi echo "应用文件检查通过"
# 步骤3: 启动应用 echo "步骤3/5: 启动Java应用 (配置:$profile)..." nohup java -jar "$JAR_LINK" --spring.profiles.active=prod,"$profile" \ >"$LOG_DIR/node${no}.out" 2>&1 & local app_pid=$! echo "应用已启动,进程PID: $app_pid"
# 步骤4: 等待应用健康检查通过 echo "步骤4/5: 等待应用健康检查通过..." if ! wait_up "$port"; then echo "应用健康检查失败,启动中止" return 1 fi echo "应用健康检查通过"
# 步骤5: 注册到APISIX (权重为0) echo "步骤5/5: 注册到负载均衡器 (权重:0)..." add_node "$node" 0 echo "等待APISIX配置下发..." wait_apisix_healthy "$port" echo "节点已注册到负载均衡器"
# 步骤6: 激活流量 (权重设为1) echo "步骤6/6: 激活流量接收 (权重:0->1)..." update_node_weight "$node" 1
echo "==== Node$no 完全启动成功,开始接收流量 ===="}
stop_node() { # no port local no="$1"; local port="$2"; local node="$HOST:$port" echo "==== 开始停止 Node$no (端口:$port) ===="
# 步骤1: 检查端口监听状态 echo "步骤1/1: 检查端口监听状态..." local pid if ! pid=$(get_pid_by_port "$port"); then echo "端口 $port 没有进程监听,节点已停止" echo "==== Node$no 停止完成 (端口未监听) ====" return 0 fi echo "找到监听进程 PID: $pid,开始停止流程..."
# 步骤2: 降权重 (停止接收新请求) echo "步骤2/5: 从负载均衡器移除流量 (权重->0)..." update_node_weight "$node" 0 || echo "警告: 更新权重失败,但继续执行停止流程" echo "节点权重已降为0,停止接收新请求"
# 步骤3: 等待现有连接耗尽 echo "步骤3/5: 等待现有连接耗尽 (${IDLE_TIMEOUT_BUFFER}秒)..." local drain_time=0 while [ $drain_time -lt $IDLE_TIMEOUT_BUFFER ]; do drain_time=$((drain_time + 1)) echo "等待连接耗尽: ${drain_time}/${IDLE_TIMEOUT_BUFFER} 秒" sleep 1 || true done echo "连接耗尽等待完成"
# 步骤4: 优雅停机 echo "步骤4/5: 发送优雅停机信号 (TERM)..." kill -TERM "$pid" 2>/dev/null || { echo "发送TERM信号失败,进程可能已终止" } echo "TERM信号已发送,等待进程优雅终止..."
# 等待进程终止 (带进度显示) local waited=0 while kill -0 "$pid" 2>/dev/null; do if [ $waited -ge $SHUTDOWN_TIMEOUT_SECONDS ]; then echo "优雅停机超时 (${SHUTDOWN_TIMEOUT_SECONDS}s),执行强制终止..." kill -9 "$pid" 2>/dev/null || true sleep 2 || true break fi waited=$((waited + 1)) echo "等待进程终止: ${waited}/${SHUTDOWN_TIMEOUT_SECONDS} 秒" sleep 1 || true done
# 确认进程状态 if kill -0 "$pid" 2>/dev/null; then echo "警告: 进程 $pid 终止失败,但继续清理" else echo "进程 $pid 已成功终止" fi
# 步骤5: 从upstream完全移除 echo "步骤5/5: 从APISIX完全移除节点..." remove_node "$node" || echo "警告: 从负载均衡器移除节点失败"
echo "==== Node$no 完全停止成功 ===="}
###################### 主逻辑 #########################################
# 显示用法show_usage() { echo "用法: $0 [jar-file] <action>" >&2 echo " jar-file: jar文件路径 (启动操作时必须,停止操作时可选)" >&2 echo " action: 1=启动节点1 2=停止节点1 3=启动节点2 4=停止节点2" >&2 echo "" >&2 echo "示例:" >&2 echo " $0 app-1.0.jar 1 # 启动节点1" >&2 echo " $0 2 # 停止节点1 (不需要jar文件)" >&2 exit 1}
# 解析参数NEW_JAR="" ACTION=""
if [[ $# -eq 1 ]]; then # 只有一个参数,判断是jar文件还是action if [[ "$1" =~ ^[1-4]$ ]]; then ACTION="$1" elif [[ -f "$1" ]]; then NEW_JAR="$1" else echo "错误: 参数 '$1' 不是有效的jar文件或操作号" >&2 show_usage fielif [[ $# -eq 2 ]]; then NEW_JAR="$1" ACTION="$2"elif [[ $# -eq 0 ]]; then # 无参数,交互模式 :else show_usagefi
# 验证action格式if [[ -n $ACTION && ! $ACTION =~ ^[1-4]$ ]]; then echo "错误: action必须是1-4之间的数字" >&2 show_usagefi
# 检查必要工具check_tools "${ACTION:-0}"
cd "$BASE_DIR"
# 处理jar文件链接(仅在启动操作或有新jar文件时)if [[ -n $NEW_JAR ]]; then if [[ ! -f "$NEW_JAR" ]]; then echo "错误: jar文件 '$NEW_JAR' 不存在" >&2 exit 1 fi
if [[ "$NEW_JAR" != "$JAR_LINK" ]]; then ln -f "$NEW_JAR" "$JAR_LINK" echo "已创建jar文件链接: $NEW_JAR -> $JAR_LINK" else echo "源文件和目标文件相同,跳过链接" fifi
# 交互式选择if [[ -z $ACTION ]]; then cat <<'EOF'==================== 选择操作 ==================== 1) 启动节点1 2) 停止节点1 3) 启动节点2 4) 停止节点2==================================================EOF read -rp "请选择 (1‑4): " ACTION
if [[ ! $ACTION =~ ^[1-4]$ ]]; then echo "无效操作: $ACTION" exit 1 fifi
# 对于启动操作,确保有jar文件if [[ $ACTION =~ ^(1|3)$ && ! -f "$JAR_LINK" ]]; then echo "错误: 启动操作需要jar文件,但 $JAR_LINK 不存在" >&2 echo "请指定jar文件: $0 <jar-file> $ACTION" >&2 exit 1fi
# 执行操作case "$ACTION" in 1) start_node 1 "$PORT1" "$PROFILE1" ;; 2) stop_node 1 "$PORT1" ;; 3) start_node 2 "$PORT2" "$PROFILE2" ;; 4) stop_node 2 "$PORT2" ;; *) echo "无效操作 $ACTION"; exit 1 ;;esac
echo "==================== 脚本执行完毕 ===================="ecs/demo-node1/说明.txt
主机1.2.3.4
所在目录/home/backend/demo-node1ecs/demo-node2/deploy.sh
#!/usr/bin/env bash## zero-deploy.sh - 零停机部署脚本(移除锁机制版 2025‑06‑A)## 关键改动# 1) 以 add_node/remove_node/update_node_weight 取代全量 update_nodes# 2) 单节点 PATCH,完全符合 APISIX Admin API 最佳实践# 3) 移除文件锁机制,简化部署流程,确保高效执行# -------------------------------------------------------------------
set -euo pipefail
####################### 可调参数 #####################################BASE_DIR="$(dirname "$(readlink "$0")")"LOG_DIR="$BASE_DIR/logs"
LOG_LEVEL="${LOG_LEVEL:-INFO}" # INFO | DEBUG
PORT1=8080 ; PROFILE1="prod-node1"PORT2=8081 ; PROFILE2="prod-node2"
HEALTH_PATH="/health"HEALTH_TIMEOUT=60APP_START_TIMEOUT_SECONDS=$HEALTH_TIMEOUT
KEEPALIVE_IDLE_TIMEOUT=30 # 与 upstream.keepalive_pool.idle_timeout 对齐IDLE_TIMEOUT_BUFFER=$((KEEPALIVE_IDLE_TIMEOUT*2))SHUTDOWN_TIMEOUT_SECONDS=60 # 优雅停机超时时间(秒)
JAR_LINK="app.jar"UPSTREAM_ID="java-app-upstream"ADMIN_URL="http://127.0.0.1:9180/apisix/admin/upstreams/$UPSTREAM_ID"
HOST="172.18.0.1" # 可用 `ip route get 1.1.1.1 | awk '{print $7}'`AUTH_HEADER=(-H "X-API-KEY: CKjcGDoqxcJfpIXWigyqjdUSACvwzEJy") # 请替换########################################################################
mkdir -p "$LOG_DIR"
###################### 通用函数 ######################################## API 调用(带重试)api() { local method="GET" url data attempt=1 max=3 delay=1 while [[ $# -gt 0 ]]; do case $1 in -X) method="$2"; shift 2;; -d) data="$2"; shift 2;; http*) url="$1"; shift;; *) shift;; esac done while :; do [[ $LOG_LEVEL == "DEBUG" ]] && echo "API attempt $attempt: $method $url" local rsp http_code body if [[ -n ${data:-} ]]; then rsp=$(curl -s -w '\n%{http_code}' --fail "${AUTH_HEADER[@]}" -X "$method" \ -H 'Content-Type: application/json' -d "$data" "$url" 2>&1) || true else rsp=$(curl -s -w '\n%{http_code}' --fail "${AUTH_HEADER[@]}" -X "$method" \ "$url" 2>&1) || true fi http_code=$(echo "$rsp" | tail -n1) body=$(echo "$rsp" | head -n -1) [[ $LOG_LEVEL == "DEBUG" ]] && echo "Response code: $http_code"
if [[ $http_code =~ ^2[0-9][0-9]$ ]]; then [[ $LOG_LEVEL == "DEBUG" ]] && echo "HTTP $method $url -> $http_code SUCCESS" [[ -n $body && $LOG_LEVEL == "DEBUG" ]] && echo "Response body: $body" echo "$body" return 0 fi if (( attempt >= max )); then echo "HTTP $method $url FAILED after $attempt attempts, code=$http_code, body=$body" return 1 fi echo "HTTP $method $url attempt $attempt failed, retry after ${delay}s..." sleep $delay ((attempt++)) delay=$((delay*2)) done}
# 获取当前 upstream.nodesget_current_nodes() { api -X GET "$ADMIN_URL" | jq -r '.value.nodes // {}'}
###################### 节点操作 #######################################add_node() { # add_node host:port [weight] local node="$1" weight="${2:-0}" local nodes=$(get_current_nodes) if echo "$nodes" | jq -e --arg n "$node" 'has($n)' >/dev/null; then echo "Node $node already exists" return 0 fi local patch=$( jq -n --arg node "$node" --argjson w "$weight" '{nodes:{($node):$w}}') api -X PATCH -d "$patch" "$ADMIN_URL" echo "Node $node added with weight=$weight"}
update_node_weight() { # update_node_weight host:port weight local node="$1" weight="$2" [[ $LOG_LEVEL == "DEBUG" ]] && echo "update_node_weight: node=$node, weight=$weight" [[ $LOG_LEVEL == "DEBUG" ]] && echo "Creating patch..." local patch=$( jq -n --arg node "$node" --argjson w "$weight" '{nodes:{($node):$w}}') [[ $LOG_LEVEL == "DEBUG" ]] && echo "Calling API: PATCH $ADMIN_URL with patch: $patch" api -X PATCH -d "$patch" "$ADMIN_URL" >/dev/null [[ $LOG_LEVEL == "DEBUG" ]] && echo "API call completed" echo "Node $node weight updated -> $weight"}
remove_node() { # remove_node host:port local node="$1" local patch=$( jq -n --arg node "$node" '{nodes:{($node):null}}') api -X PATCH -d "$patch" "$ADMIN_URL" echo "Node $node removed from upstream"}
###################### 健康检查工具 ###################################health_check() { local url="$1" local timeout="${2:-$APP_START_TIMEOUT_SECONDS}" local exptime=0
echo "开始健康检查: $url" while true; do status_code=$(curl -L -o /dev/null --connect-timeout 5 -s -w %{http_code} "${url}" 2>/dev/null) if [ "$?" == "0" ] && [ "$status_code" == "200" ]; then echo "健康检查通过 (HTTP $status_code)" return 0 fi
echo "等待应用启动: $((exptime+1))/$timeout 秒 (状态码:${status_code:-连接失败})"
sleep 1 || true exptime=$((exptime + 1))
if [ $exptime -gt ${timeout} ]; then echo "健康检查超时 (${timeout}秒),应用启动失败" return 1 fi done}
wait_up() { health_check "http://$HOST:$1/health"; }wait_down() { ! health_check "http://$HOST:$1/health"; }
wait_apisix_healthy() { # port local node="$HOST:$1" [[ $LOG_LEVEL == "DEBUG" ]] && echo "等待APISIX配置下发: $node"
for i in $(seq 0 14); do weight=$(get_current_nodes | jq -r --arg n "$node" '.[$n] // empty' 2>/dev/null) if [[ -n $weight ]]; then [[ $LOG_LEVEL == "DEBUG" ]] && echo "APISIX配置已下发: $node (权重:$weight)" return 0 fi echo "等待APISIX配置下发: $((i+1))/15 秒" sleep 1 || true done echo "APISIX配置下发超时,但继续执行..." return 1}
###################### 启停节点 #######################################start_node() { # no port profile local no="$1"; local port="$2"; local profile="$3"; local node="$HOST:$port" echo "==== 开始启动 Node$no (端口:$port) ===="
# 步骤1: 检查端口占用 echo "步骤1/5: 检查端口可用性..." if lsof -ti tcp:"$port" -sTCP:LISTEN >/dev/null; then echo "端口 $port 已被占用,无法启动" return 1 fi echo "端口 $port 可用"
# 步骤2: 启动应用 echo "步骤2/5: 启动Java应用 (配置:$profile)..." nohup java -jar "$JAR_LINK" --spring.profiles.active=prod,"$profile" \ >"$LOG_DIR/node${no}.out" 2>&1 & local app_pid=$! echo "应用已启动,进程PID: $app_pid"
# 步骤3: 等待应用健康检查通过 echo "步骤3/5: 等待应用健康检查通过..." if ! wait_up "$port"; then echo "应用健康检查失败,启动中止" return 1 fi echo "应用健康检查通过"
# 步骤4: 注册到APISIX (权重为0) echo "步骤4/5: 注册到负载均衡器 (权重:0)..." add_node "$node" 0 echo "等待APISIX配置下发..." wait_apisix_healthy "$port" echo "节点已注册到负载均衡器"
# 步骤5: 激活流量 (权重设为1) echo "步骤5/5: 激活流量接收 (权重:0->1)..." update_node_weight "$node" 1
echo "==== Node$no 完全启动成功,开始接收流量 ===="}
stop_node() { # no port local no="$1"; local port="$2"; local node="$HOST:$port" echo "==== 开始停止 Node$no (端口:$port) ===="
# 步骤1: 获取进程ID echo "步骤1/5: 检查进程状态..." local pid=$(lsof -ti tcp:"$port" -sTCP:LISTEN || true) if [[ -z $pid ]]; then echo "进程未运行,仅从负载均衡器移除节点" echo "步骤5/5: 从APISIX移除节点..." remove_node "$node" echo "Node$no 停止完成 (进程未运行)" return 0 fi echo "找到进程 PID: $pid"
# 步骤2: 降权重 (停止接收新请求) echo "步骤2/5: 从负载均衡器移除流量 (权重->0)..." update_node_weight "$node" 0 echo "节点权重已降为0,停止接收新请求"
# 步骤3: 等待现有连接耗尽 echo "步骤3/5: 等待现有连接耗尽 (${IDLE_TIMEOUT_BUFFER}秒)..." local drain_time=0 while [ $drain_time -lt $IDLE_TIMEOUT_BUFFER ]; do drain_time=$((drain_time + 1)) echo "等待连接耗尽: ${drain_time}/${IDLE_TIMEOUT_BUFFER} 秒" sleep 1 || true done echo "连接耗尽等待完成"
# 步骤4: 优雅停机 echo "步骤4/5: 发送优雅停机信号 (TERM)..." kill -TERM "$pid" 2>/dev/null || true echo "TERM信号已发送,等待进程优雅终止..."
# 等待进程终止 (带进度显示) local waited=0 while kill -0 "$pid" 2>/dev/null; do if [ $waited -ge $SHUTDOWN_TIMEOUT_SECONDS ]; then echo "优雅停机超时 (${SHUTDOWN_TIMEOUT_SECONDS}s),执行强制终止..." kill -9 "$pid" 2>/dev/null || true sleep 2 || true break fi waited=$((waited + 1)) echo "等待进程终止: ${waited}/${SHUTDOWN_TIMEOUT_SECONDS} 秒" sleep 1 || true done
# 确认进程状态 if kill -0 "$pid" 2>/dev/null; then echo "进程 $pid 终止失败" return 1 else echo "进程 $pid 已成功终止" fi
# 步骤5: 从upstream完全移除 echo "步骤5/5: 从APISIX完全移除节点..." remove_node "$node"
echo "==== Node$no 完全停止成功 ===="}
###################### 主逻辑 #########################################[[ $# -lt 1 || $# -gt 2 ]] && { echo "用法: $0 <jar-file> [action]" >&2 echo "action: 1=启动节点1 2=停止节点1 3=启动节点2 4=停止节点2" >&2 exit 1}NEW_JAR="$1"; ACTION="${2:-}"
cd "$BASE_DIR"; [[ "$NEW_JAR" != "$JAR_LINK" ]] && ln -f "$NEW_JAR" "$JAR_LINK" || echo "源文件和目标文件相同,跳过链接"
if [[ -z $ACTION ]]; then cat <<'EOF'==================== 选择操作 ==================== 1) 启动节点1 2) 停止节点1 3) 启动节点2 4) 停止节点2==================================================EOF read -rp "请选择 (1‑4): " ACTIONfi
case "$ACTION" in 1) start_node 1 "$PORT1" "$PROFILE1" ;; 2) stop_node 1 "$PORT1" ;; 3) start_node 2 "$PORT2" "$PROFILE2" ;; 4) stop_node 2 "$PORT2" ;; *) echo "无效操作 $ACTION"; exit 1 ;;esac
echo "==================== 脚本执行完毕 ===================="ecs/demo-node2/说明.txt
主机1.2.3.4
所在目录/home/backend/demo-node2springboot配置文件/application-prod-node1.yml
server: port: 8080springboot配置文件/application-prod-node2.yml
server: port: 8081springboot配置文件/application.yml
spring: application: name: java-app profiles: active: prod-node1
jackson: time-zone: Asia/Shanghai # 全局时区
lifecycle: timeout-per-shutdown-phase: 120s # 优雅停机总等待
task: execution: # 默认异步/响应式线程池 pool: core-size: 16 max-size: 64 queue-capacity: 1000 keep-alive: 60s shutdown: await-termination: true await-termination-period: 120s
server: shutdown: graceful # Spring Boot 2.3+ 优雅停机 port: 8080 servlet: context-path: / tomcat: threads: max: 200 min-spare: 10 accept-count: 100 # 已满时排队连接 max-connections: 10000 keep-alive-timeout: 20s # Keep‑Alive 保持时长 connection-timeout: 5s # 握手超时
management: server: port: 8090 # 管理端口隔离 endpoints: web: base-path: /actuator exposure: include: health,info,prometheus # shutdown 默认不暴露 endpoint: health: show-details: when_authorized metrics: export: prometheus: enabled: true step: 15s
logging: file: path: ./logs # Boot 负责把 LOG_HOME 解析到 ./logs level: root: INFO com.example: DEBUGspringboot配置文件/logback-spring.xml
<?xml version="1.0" encoding="UTF-8"?><configuration scan="true" scanPeriod="30 seconds">
<include resource="org/springframework/boot/logging/logback/defaults.xml"/> <include resource="org/springframework/boot/logging/logback/console-appender.xml"/>
<!-- 日志根目录:来自 logging.file.path --> <springProperty scope="context" name="LOG_HOME" source="logging.file.path" defaultValue="./logs"/>
<!-- 按“天 + 10 MB” 滚动 --> <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender"> <file>${LOG_HOME}/java-app.log</file> <immediateFlush>true</immediateFlush>
<encoder> <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern> <charset>UTF-8</charset> </encoder>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy"> <fileNamePattern>${LOG_HOME}/archive/java-app-%d{yyyy-MM-dd}.%i.log.gz</fileNamePattern> <maxFileSize>10MB</maxFileSize> <maxHistory>30</maxHistory> <totalSizeCap>3GB</totalSizeCap> </rollingPolicy> </appender>
<!-- 异步包装 --> <appender name="ASYNC_FILE" class="ch.qos.logback.classic.AsyncAppender"> <queueSize>8192</queueSize> <discardingThreshold>0</discardingThreshold> <neverBlock>true</neverBlock> <appender-ref ref="FILE"/> </appender>
<!-- 根日志级别:INFO --> <root level="INFO"> <appender-ref ref="CONSOLE"/> <appender-ref ref="ASYNC_FILE"/> </root>
<!-- 如需包级别 DEBUG,在此追加 --> <!-- <logger name="com.example" level="DEBUG"/> -->
</configuration>云效/节点1/部署脚本.txt
set -e # 遇到错误立即退出# ==== 环境变量配置 ====# 制品实际下载到的路径PACKAGE_PATH=/home/flowapp/demo.tgz#应用目录APP_HOME=/home/backend/demo-node1#jar 文件名JAR_NAME=demo-0.0.1-SNAPSHOT.jarlog() { echo "[`date '+%F %T'`] $*"; }mkdir -p "$(dirname "$PACKAGE_PATH")"mkdir -p "$APP_HOME"deploy_package() { # 检查制品文件是否存在 if [ ! -f "$PACKAGE_PATH" ]; then log "错误: 制品文件不存在 $PACKAGE_PATH" exit 1 fi log "解包 $PACKAGE_PATH 到 $APP_HOME" tar -zxf "$PACKAGE_PATH" -C "$APP_HOME" log "解包完成" #log "删除压缩包 $PACKAGE_PATH" #rm -f "$PACKAGE_PATH" && log "压缩包已删除" || log "压缩包删除失败,可忽略"}# 切换到应用目录cd "$APP_HOME" || { echo "目录不存在: $APP_HOME"; exit 1; }log "停止节点 1"sh deploy.sh $JAR_NAME 2 # 参数 2 = 停止节点deploy_packagelog "启动节点 1"sh deploy.sh "$JAR_NAME" 1 # 参数 1 = 启动节点log "部署脚本执行完毕"云效/节点2/部署脚本.txt
set -e # 遇到错误立即退出# ==== 环境变量配置 ====sleep 5# 制品实际下载到的路径PACKAGE_PATH=/home/flowapp/demo.tgz#应用目录APP_HOME=/home/backend/demo-node2#jar 文件名JAR_NAME=demo-0.0.1-SNAPSHOT.jarlog() { echo "[`date '+%F %T'`] $*"; }mkdir -p "$(dirname "$PACKAGE_PATH")"mkdir -p "$APP_HOME"deploy_package() { # 检查制品文件是否存在 if [ ! -f "$PACKAGE_PATH" ]; then log "错误: 制品文件不存在 $PACKAGE_PATH" exit 1 fi log "解包 $PACKAGE_PATH 到 $APP_HOME" tar -zxf "$PACKAGE_PATH" -C "$APP_HOME" log "解包完成" log "删除压缩包 $PACKAGE_PATH" rm -f "$PACKAGE_PATH" && log "压缩包已删除" || log "压缩包删除失败,可忽略"}# 切换到应用目录cd "$APP_HOME" || { echo "目录不存在: $APP_HOME"; exit 1; }log "停止节点 1"sh deploy.sh $JAR_NAME 4 # 参数 4 = 停止节点deploy_packagelog "启动节点 1"sh deploy.sh "$JAR_NAME" 3 # 参数 3 = 启动节点log "部署脚本执行完毕" springboot零停机发布方案
https://twenhub.com/posts/springbootling-ting-ji-fa-bu-fang-an/