#!/bin/bash # GraphRAG Studio — 一键部署脚本 (test + prod 双环境) # 用法: bash k8s/deploy.sh # 前提: k3s 集群已运行, docker 可用, registry.plfai.cn 可推送 set -e SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" PROJECT_DIR="$(dirname "$SCRIPT_DIR")" REGISTRY="registry.plfai.cn" BACKEND_IMAGE="${REGISTRY}/graphrag-backend" FRONTEND_IMAGE="${REGISTRY}/graphrag-frontend" KUBECTL="k3s kubectl" RED='\033[0;31m' GREEN='\033[0;32m' BLUE='\033[0;34m' NC='\033[0m' log() { echo -e "${BLUE}[$(date +%H:%M:%S)]${NC} $1"; } ok() { echo -e "${GREEN}[OK]${NC} $1"; } err() { echo -e "${RED}[ERR]${NC} $1"; exit 1; } # ──────────────── 加载密钥 ──────────────── if [ -f "$PROJECT_DIR/backend/.env" ]; then set -a; source "$PROJECT_DIR/backend/.env"; set +a else err "请先创建 backend/.env(参考 backend/.env.example)" fi if [ -z "$DEEPSEEK_API_KEY" ] || [ "$DEEPSEEK_API_KEY" = "sk-your-key-here" ]; then err "请在 backend/.env 中填入 DEEPSEEK_API_KEY" fi # ──────────────── 1. 构建镜像 ──────────────── log "Building backend image..." cd "$PROJECT_DIR" docker build --network host -f backend/Dockerfile -t "${BACKEND_IMAGE}:latest" . || err "backend build" docker tag "${BACKEND_IMAGE}:latest" "${BACKEND_IMAGE}:v1" ok "Backend built" log "Building frontend image..." docker build --network host -f frontend/Dockerfile -t "${FRONTEND_IMAGE}:latest" . || err "frontend build" docker tag "${FRONTEND_IMAGE}:latest" "${FRONTEND_IMAGE}:v1" ok "Frontend built" # ──────────────── 2. 推送镜像 ──────────────── log "Pushing images to registry..." docker push "${BACKEND_IMAGE}:latest" "${BACKEND_IMAGE}:v1" >/dev/null 2>&1 docker push "${FRONTEND_IMAGE}:latest" "${FRONTEND_IMAGE}:v1" >/dev/null 2>&1 ok "Images pushed" # ──────────────── 3. 导入 containerd ──────────────── log "Importing into containerd..." docker save "${BACKEND_IMAGE}:v1" | k3s ctr images import - >/dev/null 2>&1 docker save "${FRONTEND_IMAGE}:v1" | k3s ctr images import - >/dev/null 2>&1 ok "Containerd loaded" # ──────────────── 4. 部署 Kubernetes ──────────────── deploy_env() { local NS="$1" label="$2" log "Deploying ${label} (ns: ${NS})..." # 创建 namespace $KUBECTL create namespace "$NS" --dry-run=client -o yaml | $KUBECTL apply -f - 2>/dev/null # apply ConfigMap $KUBECTL apply -n "$NS" -f "$SCRIPT_DIR/base/configmap.yaml" 2>/dev/null # envsubst 注入密钥 → apply Secret envsubst < "$SCRIPT_DIR/base/secret.yaml" | $KUBECTL apply -n "$NS" -f - 2>/dev/null # apply 其余 base 资源 for f in "$SCRIPT_DIR/base"/*.yaml; do case "$(basename "$f")" in kustomization.yaml|configmap.yaml|secret.yaml) continue ;; *) $KUBECTL apply -n "$NS" -f "$f" 2>/dev/null ;; esac done # 修正镜像 tag $KUBECTL set image deployment/backend "backend=${BACKEND_IMAGE}:v1" -n "$NS" 2>/dev/null $KUBECTL set image deployment/frontend "frontend=${FRONTEND_IMAGE}:v1" -n "$NS" 2>/dev/null # 修正 backend command(镜像 CMD 可能为 sleep) $KUBECTL patch deployment backend -n "$NS" --type json -p '[ {"op":"add","path":"/spec/template/spec/containers/0/command","value":["/opt/venv/bin/python","-m","uvicorn","main:app","--host","0.0.0.0","--port","8000"]} ]' 2>/dev/null # 节点亲和性 $KUBECTL patch deployment backend -n "$NS" --type merge -p \ '{"spec":{"template":{"spec":{"affinity":{"nodeAffinity":{"requiredDuringSchedulingIgnoredDuringExecution":{"nodeSelectorTerms":[{"matchExpressions":[{"key":"kubernetes.io/hostname","operator":"In","values":["k8smaster"]}]}]}}}}}}}' 2>/dev/null $KUBECTL patch deployment frontend -n "$NS" --type merge -p \ '{"spec":{"template":{"spec":{"affinity":{"nodeAffinity":{"requiredDuringSchedulingIgnoredDuringExecution":{"nodeSelectorTerms":[{"matchExpressions":[{"key":"kubernetes.io/hostname","operator":"In","values":["plf-cvm-worker","k8smaster"]}]}]}}}}}}}' 2>/dev/null ok "${label} deployed" } deploy_env "graphrag-test" "TEST" deploy_env "graphrag-prod" "PROD" # ──────────────── 5. 等待就绪 ──────────────── log "Waiting for pods..." for ns in graphrag-test graphrag-prod; do $KUBECTL wait --for=condition=ready pod -l component=backend -n "$ns" --timeout=360s 2>/dev/null && ok "${ns} backend ready" || log " ${ns} backend still starting" $KUBECTL wait --for=condition=ready pod -l component=frontend -n "$ns" --timeout=60s 2>/dev/null && ok "${ns} frontend ready" || log " ${ns} frontend still starting" done # ──────────────── 6. 配置 nginx ──────────────── log "Setting up nginx..." setup_nginx() { local domain="$1" frontend_ip="$2" backend_ip="$3" backend_only="${4:-false}" if [ "$backend_only" = "true" ]; then cat > "/etc/nginx/conf.d/${domain}.conf" << NGINX server { listen 443 ssl; server_name ${domain}; ssl_certificate /etc/letsencrypt/live/plfai.cn/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/plfai.cn/privkey.pem; ssl_protocols TLSv1.2 TLSv1.3; client_max_body_size 200m; location / { proxy_pass http://${backend_ip}:8000; proxy_set_header Host \$host; proxy_set_header X-Real-IP \$remote_addr; proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto \$scheme; proxy_read_timeout 120s; proxy_buffering off; } } server { listen 80; server_name ${domain}; return 301 https://\$server_name\$request_uri; } NGINX else cat > "/etc/nginx/conf.d/${domain}.conf" << NGINX server { listen 443 ssl; server_name ${domain}; ssl_certificate /etc/letsencrypt/live/plfai.cn/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/plfai.cn/privkey.pem; ssl_protocols TLSv1.2 TLSv1.3; client_max_body_size 200m; location /api/ { proxy_pass http://${backend_ip}:8000; proxy_set_header Host \$host; proxy_set_header X-Real-IP \$remote_addr; proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto \$scheme; proxy_read_timeout 120s; proxy_buffering off; } location / { proxy_pass http://${frontend_ip}:80; proxy_set_header Host \$host; proxy_set_header X-Real-IP \$remote_addr; proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto \$scheme; } } server { listen 80; server_name ${domain}; return 301 https://\$server_name\$request_uri; } NGINX fi ok "nginx: ${domain}" } TEST_FE=$($KUBECTL get svc frontend -n graphrag-test -o jsonpath='{.spec.clusterIP}') TEST_BE=$($KUBECTL get svc backend -n graphrag-test -o jsonpath='{.spec.clusterIP}') PROD_FE=$($KUBECTL get svc frontend -n graphrag-prod -o jsonpath='{.spec.clusterIP}') PROD_BE=$($KUBECTL get svc backend -n graphrag-prod -o jsonpath='{.spec.clusterIP}') setup_nginx "test-graphrag.plfai.cn" "$TEST_FE" "$TEST_BE" setup_nginx "test-graphrag-backend.plfai.cn" "$TEST_BE" "$TEST_BE" true setup_nginx "graphrag.plfai.cn" "$PROD_FE" "$PROD_BE" setup_nginx "graphrag-backend.plfai.cn" "$PROD_BE" "$PROD_BE" true nginx -t >/dev/null 2>&1 && nginx -s reload >/dev/null 2>&1 ok "nginx reloaded" # ──────────────── 7. 验证 ──────────────── echo "" echo -e "${BLUE}========================================${NC}" echo -e "${BLUE} GraphRAG Studio 部署完成${NC}" echo -e "${BLUE}========================================${NC}" echo "" echo "Test: https://test-graphrag.plfai.cn" echo "Prod: https://graphrag.plfai.cn" echo "" for url in \ "https://test-graphrag.plfai.cn" \ "https://graphrag.plfai.cn" \ "https://test-graphrag-backend.plfai.cn/api/v1/health" \ "https://graphrag-backend.plfai.cn/api/v1/health"; do code=$(curl -sk -o /dev/null -w "%{http_code}" --connect-timeout 5 "$url" 2>/dev/null || echo "ERR") echo " $url → HTTP $code" done echo "" echo -e "${GREEN}Done.${NC}"