From 1d12cb574eff59cb67a84e1c68467cf40fdc71a5 Mon Sep 17 00:00:00 2001 From: plf1996 Date: Tue, 16 Jun 2026 14:15:31 +0800 Subject: [PATCH] fix: cleanup k8s configs, add deploy.sh, remove hardcoded secrets MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add one-click deploy script (k8s/deploy.sh) for test + prod - Add startupProbe to backend (60x10s = 10min grace for ML model loading) - Add configmap.yaml and secret.yaml (envsubst from backend/.env) - Fix image URLs: add registry.plfai.cn prefix - Fix frontend: API base URL relative (/api/v1), login-required mode - Remove hardcoded localhost references from frontend - Clean broken kustomize overlays - Add nodeAffinity for backend→k8smaster, frontend→plf-cvm/k8smaster - Remove dead ReplicaSets and stale pods during deploy --- .gitignore | 1 + backend/Dockerfile | 3 +- frontend/src/app/api.ts | 2 +- frontend/src/app/auth.ts | 4 +- frontend/src/app/components/layout/Header.tsx | 2 +- frontend/src/app/components/pages/QAChat.tsx | 2 +- k8s/base/backend-deployment.yaml | 14 +- k8s/base/configmap.yaml | 12 ++ k8s/base/frontend-deployment.yaml | 2 +- k8s/base/kustomization.yaml | 2 + k8s/base/persistent-volume-claims.yaml | 2 + k8s/base/secret.yaml | 9 + k8s/deploy.sh | 182 ++++++++++++++++++ k8s/overlays/prod/kustomization.yaml | 27 +-- k8s/overlays/test/kustomization.yaml | 26 +-- 15 files changed, 230 insertions(+), 60 deletions(-) create mode 100644 k8s/base/configmap.yaml create mode 100644 k8s/base/secret.yaml create mode 100755 k8s/deploy.sh diff --git a/.gitignore b/.gitignore index a399fa7..8c482b5 100644 --- a/.gitignore +++ b/.gitignore @@ -41,3 +41,4 @@ mineru_mvp/output/ # Claude Code personal config settings.json +backend/.env diff --git a/backend/Dockerfile b/backend/Dockerfile index b757d9f..ce93463 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -23,7 +23,8 @@ RUN uv venv /opt/venv && \ "networkx>=3.0" \ "python-dotenv>=1.0.0" \ "requests>=2.31.0" \ - "beautifulsoup4>=4.12.0" + "beautifulsoup4>=4.12.0" \ + "python-jose[cryptography]>=3.3.0" # ===== Stage 2: Runtime ===== FROM python:3.12-slim diff --git a/frontend/src/app/api.ts b/frontend/src/app/api.ts index 79b3e9f..20da8f6 100644 --- a/frontend/src/app/api.ts +++ b/frontend/src/app/api.ts @@ -4,7 +4,7 @@ * All functions return the `data` field; throw ApiError on code !== 0 */ -const BASE = 'http://localhost:8000/api/v1'; +const BASE = '/api/v1'; export class ApiError extends Error { code: number; diff --git a/frontend/src/app/auth.ts b/frontend/src/app/auth.ts index 313602c..efb2d04 100644 --- a/frontend/src/app/auth.ts +++ b/frontend/src/app/auth.ts @@ -13,9 +13,7 @@ export async function initKeycloak(): Promise { try { const authenticated = await keycloak.init({ - onLoad: 'check-sso', - silentCheckSsoRedirectUri: - window.location.origin + '/silent-check-sso.html', + onLoad: 'login-required', pkceMethod: 'S256', }); _initialized = true; diff --git a/frontend/src/app/components/layout/Header.tsx b/frontend/src/app/components/layout/Header.tsx index 9b2a2d1..9bf7c74 100644 --- a/frontend/src/app/components/layout/Header.tsx +++ b/frontend/src/app/components/layout/Header.tsx @@ -163,7 +163,7 @@ export function Header() { className="inline-block w-2 h-2 rounded-full" style={{ background: allOk ? 'var(--green)' : 'var(--red)' }} /> - API: localhost:8000 + API: /api/v1 ); diff --git a/frontend/src/app/components/pages/QAChat.tsx b/frontend/src/app/components/pages/QAChat.tsx index 42f567d..2a79994 100644 --- a/frontend/src/app/components/pages/QAChat.tsx +++ b/frontend/src/app/components/pages/QAChat.tsx @@ -79,7 +79,7 @@ export function QAChat() { setMessages(prev => [...prev, { id: `err${Date.now()}`, role: 'ai', - content: `⚠️ 请求失败:${msg}\n\n请确认:\n1. 后端服务已启动(localhost:8000)\n2. 知识图谱已有数据(请先上传并索引文档)\n3. DeepSeek API Key 已配置`, + content: `⚠️ 请求失败:${msg}\n\n请确认:\n1. 后端服务已启动\n2. 知识图谱已有数据(请先上传并索引文档)\n3. DeepSeek API Key 已配置`, timestamp: new Date().toISOString(), }]); } finally { diff --git a/k8s/base/backend-deployment.yaml b/k8s/base/backend-deployment.yaml index 925b98b..bd99d49 100644 --- a/k8s/base/backend-deployment.yaml +++ b/k8s/base/backend-deployment.yaml @@ -19,7 +19,7 @@ spec: spec: containers: - name: backend - image: graphrag-backend:latest + image: registry.plfai.cn/graphrag-backend:latest imagePullPolicy: IfNotPresent ports: - containerPort: 8000 @@ -41,17 +41,25 @@ spec: limits: memory: "2Gi" cpu: "1000m" + startupProbe: + httpGet: + path: /api/v1/health + port: 8000 + initialDelaySeconds: 30 + periodSeconds: 10 + failureThreshold: 60 livenessProbe: httpGet: path: /api/v1/health port: 8000 - initialDelaySeconds: 20 + initialDelaySeconds: 0 periodSeconds: 30 + failureThreshold: 3 readinessProbe: httpGet: path: /api/v1/health port: 8000 - initialDelaySeconds: 10 + initialDelaySeconds: 0 periodSeconds: 10 volumes: - name: backend-data diff --git a/k8s/base/configmap.yaml b/k8s/base/configmap.yaml new file mode 100644 index 0000000..bb89f1e --- /dev/null +++ b/k8s/base/configmap.yaml @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: graphrag-config +data: + DEEPSEEK_BASE_URL: "https://api.deepseek.com" + MINERU_PIPELINE: "/app/mineru_mvp/pipeline.py" + MINERU_PYTHON: "/opt/venv/bin/python" + KEYCLOAK_SERVER_URL: "https://keycloak.plfai.cn" + KEYCLOAK_REALM: "plfai" + KEYCLOAK_CLIENT_ID: "graphrag-backend" + KEYCLOAK_AUDIENCE: "account" diff --git a/k8s/base/frontend-deployment.yaml b/k8s/base/frontend-deployment.yaml index 5467c77..4cb30b9 100644 --- a/k8s/base/frontend-deployment.yaml +++ b/k8s/base/frontend-deployment.yaml @@ -19,7 +19,7 @@ spec: spec: containers: - name: frontend - image: graphrag-frontend:latest + image: registry.plfai.cn/graphrag-frontend:latest imagePullPolicy: IfNotPresent ports: - containerPort: 80 diff --git a/k8s/base/kustomization.yaml b/k8s/base/kustomization.yaml index c1b6c79..c24eb4d 100644 --- a/k8s/base/kustomization.yaml +++ b/k8s/base/kustomization.yaml @@ -7,3 +7,5 @@ resources: - frontend-deployment.yaml - frontend-service.yaml - persistent-volume-claims.yaml + - configmap.yaml + - secret.yaml diff --git a/k8s/base/persistent-volume-claims.yaml b/k8s/base/persistent-volume-claims.yaml index 09db2fd..e0d8f08 100644 --- a/k8s/base/persistent-volume-claims.yaml +++ b/k8s/base/persistent-volume-claims.yaml @@ -5,6 +5,7 @@ metadata: spec: accessModes: - ReadWriteOnce + storageClassName: local-path resources: requests: storage: 10Gi @@ -16,6 +17,7 @@ metadata: spec: accessModes: - ReadWriteOnce + storageClassName: local-path resources: requests: storage: 5Gi diff --git a/k8s/base/secret.yaml b/k8s/base/secret.yaml new file mode 100644 index 0000000..e51a3ec --- /dev/null +++ b/k8s/base/secret.yaml @@ -0,0 +1,9 @@ +apiVersion: v1 +kind: Secret +metadata: + name: graphrag-secrets +type: Opaque +stringData: + DEEPSEEK_API_KEY: "${DEEPSEEK_API_KEY}" + MINERU_API_TOKEN: "${MINERU_API_TOKEN}" + KEYCLOAK_CLIENT_SECRET: "${KEYCLOAK_CLIENT_SECRET}" diff --git a/k8s/deploy.sh b/k8s/deploy.sh new file mode 100755 index 0000000..78234e9 --- /dev/null +++ b/k8s/deploy.sh @@ -0,0 +1,182 @@ +#!/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" + 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; + + 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 + 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" +setup_nginx "graphrag.plfai.cn" "$PROD_FE" "$PROD_BE" +setup_nginx "graphrag-backend.plfai.cn" "$PROD_BE" "$PROD_BE" + +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}" diff --git a/k8s/overlays/prod/kustomization.yaml b/k8s/overlays/prod/kustomization.yaml index 71b8711..1069994 100644 --- a/k8s/overlays/prod/kustomization.yaml +++ b/k8s/overlays/prod/kustomization.yaml @@ -1,34 +1,11 @@ +# prod 环境 overlay — 仅供参考 +# 实际部署使用项目根目录的 k8s/deploy.sh(支持从 backend/.env 读密钥) apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization namespace: graphrag-prod - namePrefix: prod- resources: - ../../base - namespace.yaml - -configMapGenerator: - - name: graphrag-config - behavior: replace - literals: - - DEEPSEEK_BASE_URL=https://api.deepseek.com - - MINERU_PIPELINE=/app/mineru_mvp/pipeline.py - - MINERU_PYTHON=/opt/venv/bin/python - - KEYCLOAK_SERVER_URL=https://keycloak.plfai.cn - - KEYCLOAK_REALM=plfai - - KEYCLOAK_CLIENT_ID=graphrag-backend - - KEYCLOAK_AUDIENCE=account - -secretGenerator: - - name: graphrag-secrets - behavior: replace - literals: - - DEEPSEEK_API_KEY=sk-prod-placeholder - - MINERU_API_TOKEN=prod-placeholder - - KEYCLOAK_CLIENT_SECRET=prod-placeholder - -patches: - - path: ingress.yaml - - path: replicas-patch.yaml diff --git a/k8s/overlays/test/kustomization.yaml b/k8s/overlays/test/kustomization.yaml index d7b642b..2bc7fbe 100644 --- a/k8s/overlays/test/kustomization.yaml +++ b/k8s/overlays/test/kustomization.yaml @@ -1,33 +1,11 @@ +# test 环境 overlay — 仅供参考 +# 实际部署使用项目根目录的 k8s/deploy.sh(支持从 backend/.env 读密钥) apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization namespace: graphrag-test - namePrefix: test- resources: - ../../base - namespace.yaml - -configMapGenerator: - - name: graphrag-config - behavior: replace - literals: - - DEEPSEEK_BASE_URL=https://api.deepseek.com - - MINERU_PIPELINE=/app/mineru_mvp/pipeline.py - - MINERU_PYTHON=/opt/venv/bin/python - - KEYCLOAK_SERVER_URL=https://keycloak.plfai.cn - - KEYCLOAK_REALM=plfai - - KEYCLOAK_CLIENT_ID=graphrag-backend - - KEYCLOAK_AUDIENCE=account - -secretGenerator: - - name: graphrag-secrets - behavior: replace - literals: - - DEEPSEEK_API_KEY=sk-test-placeholder - - MINERU_API_TOKEN=test-placeholder - - KEYCLOAK_CLIENT_SECRET=test-placeholder - -patches: - - path: ingress.yaml