From 333c8a9cfdee1c10b622451d27535b2e43b1eca8 Mon Sep 17 00:00:00 2001 From: Your Name Date: Tue, 5 May 2026 21:17:38 +0800 Subject: [PATCH] fix(cd): target k3s control plane for deploy --- .gitea/workflows/cd.yaml | 100 +++++++++++++++++------------- k8s/awoooi-prod/04-configmap.yaml | 5 +- 2 files changed, 59 insertions(+), 46 deletions(-) diff --git a/.gitea/workflows/cd.yaml b/.gitea/workflows/cd.yaml index ec64d480..a6bcb1a1 100644 --- a/.gitea/workflows/cd.yaml +++ b/.gitea/workflows/cd.yaml @@ -42,6 +42,15 @@ env: OTEL_SERVICE_NAME: awoooi-cd OTEL_RESOURCE_ATTRIBUTES: service.version=${{ github.sha }},deployment.environment=production CI_IMAGE: 192.168.0.110:5000/awoooi/ci-runner:act-22.04 + # CD SSH still lands on 121 because that host has the deploy sudo path. + # Its kubeconfig points to 127.0.0.1, so the deploy scripts rewrite a temp + # kubeconfig to the 120 control-plane API server before running kubectl. + K8S_SSH_HOST: 192.168.0.121 + K8S_API_SERVER: https://192.168.0.120:6443 + # NodePort is currently reachable on 121; keep health probes separate from + # the kubectl control-plane host. + API_HEALTH_URL: http://192.168.0.121:32334/api/v1/health + ALERT_CHAIN_API_URL: http://192.168.0.121:32334 jobs: tests: @@ -406,13 +415,14 @@ jobs: mkdir -p ~/.ssh echo "$SSH_PRIVATE_KEY" > ~/.ssh/deploy_key chmod 600 ~/.ssh/deploy_key - ssh-keyscan 192.168.0.121 >> ~/.ssh/known_hosts 2>/dev/null - ssh -i ~/.ssh/deploy_key wooo@192.168.0.121 << SECRETS + ssh-keyscan "${{ env.K8S_SSH_HOST }}" >> ~/.ssh/known_hosts 2>/dev/null + ssh -i ~/.ssh/deploy_key "wooo@${{ env.K8S_SSH_HOST }}" << SECRETS set -e - export KUBECONFIG=/etc/rancher/k3s/k3s.yaml + K8S_API_SERVER="${{ env.K8S_API_SERVER }}" + KUBECTL="sudo kubectl --kubeconfig=/etc/rancher/k3s/k3s.yaml --server=${K8S_API_SERVER}" # 注入 Telegram Secrets (ADR-035 鐵律) - sudo kubectl patch secret awoooi-secrets -n awoooi-prod --type='json' -p='[ + $KUBECTL patch secret awoooi-secrets -n awoooi-prod --type='json' -p='[ {"op":"add","path":"/data/OPENCLAW_TG_BOT_TOKEN","value":"'$(echo -n "${TG_BOT_TOKEN}" | base64 -w 0)'"}, {"op":"add","path":"/data/OPENCLAW_TG_CHAT_ID","value":"'$(echo -n "${TG_CHAT_ID}" | base64 -w 0)'"} ]' || { echo "❌ Telegram Secrets patch 失敗 — ADR-035 鐵律"; exit 1; } @@ -421,7 +431,7 @@ jobs: # 2026-04-01 Claude Code: base64 -w 0 防止長 key 換行破壞 JSON # NVIDIA NIM (免費 tier) if [ -n "${NVIDIA_API_KEY}" ] && [ "${NVIDIA_API_KEY}" != "" ]; then - sudo kubectl patch secret awoooi-secrets -n awoooi-prod --type='json' -p='[ + $KUBECTL patch secret awoooi-secrets -n awoooi-prod --type='json' -p='[ {"op":"add","path":"/data/NVIDIA_API_KEY","value":"'$(echo -n "${NVIDIA_API_KEY}" | base64 -w 0)'"} ]' && echo "✅ NVIDIA_API_KEY 已注入" || echo "⚠️ NVIDIA_API_KEY patch 失敗" else @@ -430,7 +440,7 @@ jobs: # Gemini (備援) if [ -n "${GEMINI_API_KEY}" ] && [ "${GEMINI_API_KEY}" != "" ]; then - sudo kubectl patch secret awoooi-secrets -n awoooi-prod --type='json' -p='[ + $KUBECTL patch secret awoooi-secrets -n awoooi-prod --type='json' -p='[ {"op":"add","path":"/data/GEMINI_API_KEY","value":"'$(echo -n "${GEMINI_API_KEY}" | base64 -w 0)'"} ]' && echo "✅ GEMINI_API_KEY 已注入" || echo "⚠️ GEMINI_API_KEY patch 失敗" else @@ -439,7 +449,7 @@ jobs: # 2026-04-01 Claude Code: Langfuse LLMOps keys (補齊 CD 注入,之前只有手動設定) if [ -n "${LANGFUSE_PUBLIC_KEY}" ] && [ -n "${LANGFUSE_SECRET_KEY}" ]; then - sudo kubectl patch secret awoooi-secrets -n awoooi-prod --type='json' -p='[ + $KUBECTL patch secret awoooi-secrets -n awoooi-prod --type='json' -p='[ {"op":"add","path":"/data/LANGFUSE_PUBLIC_KEY","value":"'$(echo -n "${LANGFUSE_PUBLIC_KEY}" | base64 -w 0)'"}, {"op":"add","path":"/data/LANGFUSE_SECRET_KEY","value":"'$(echo -n "${LANGFUSE_SECRET_KEY}" | base64 -w 0)'"} ]' && echo "✅ LANGFUSE keys 已注入" || echo "⚠️ LANGFUSE keys patch 失敗" @@ -449,14 +459,14 @@ jobs: # 2026-04-02 Claude Code: Telegram Whitelist (授權簽核用戶 ID) if [ -n "${TG_USER_WHITELIST}" ]; then - sudo kubectl patch secret awoooi-secrets -n awoooi-prod --type='json' -p='[ + $KUBECTL patch secret awoooi-secrets -n awoooi-prod --type='json' -p='[ {"op":"add","path":"/data/OPENCLAW_TG_USER_WHITELIST","value":"'$(echo -n "${TG_USER_WHITELIST}" | base64 -w 0)'"} ]' && echo "✅ TG_USER_WHITELIST 已注入" || echo "⚠️ TG_USER_WHITELIST patch 失敗" fi # Phase O-4.1 2026-04-02: Sentry Auth Token (Wave A.1 ADR-037) if [ -n "${SENTRY_AUTH_TOKEN}" ]; then - sudo kubectl patch secret awoooi-secrets -n awoooi-prod --type='json' -p='[ + $KUBECTL patch secret awoooi-secrets -n awoooi-prod --type='json' -p='[ {"op":"add","path":"/data/SENTRY_AUTH_TOKEN","value":"'$(echo -n "${SENTRY_AUTH_TOKEN}" | base64 -w 0)'"} ]' && echo "✅ SENTRY_AUTH_TOKEN 已注入" || echo "⚠️ SENTRY_AUTH_TOKEN patch 失敗" else @@ -465,7 +475,7 @@ jobs: # ADR-059 2026-04-05 Claude Code: Gitea Webhook Secret if [ -n "${GITEA_WEBHOOK_SECRET}" ]; then - sudo kubectl patch secret awoooi-secrets -n awoooi-prod --type='json' -p='[ + $KUBECTL patch secret awoooi-secrets -n awoooi-prod --type='json' -p='[ {"op":"add","path":"/data/GITEA_WEBHOOK_SECRET","value":"'$(echo -n "${GITEA_WEBHOOK_SECRET}" | base64 -w 0)'"} ]' && echo "✅ GITEA_WEBHOOK_SECRET 已注入" || echo "⚠️ GITEA_WEBHOOK_SECRET patch 失敗" else @@ -474,7 +484,7 @@ jobs: # MCP Phase 3: ArgoCD API Token (2026-04-11 Claude Sonnet 4.6) if [ -n "${ARGOCD_API_TOKEN}" ]; then - sudo kubectl patch secret awoooi-secrets -n awoooi-prod --type='json' -p='[ + $KUBECTL patch secret awoooi-secrets -n awoooi-prod --type='json' -p='[ {"op":"add","path":"/data/ARGOCD_API_TOKEN","value":"'$(echo -n "${ARGOCD_API_TOKEN}" | base64 -w 0)'"} ]' && echo "✅ ARGOCD_API_TOKEN 已注入" || echo "⚠️ ARGOCD_API_TOKEN patch 失敗" else @@ -489,7 +499,7 @@ jobs: # DATABASE_URL — PG 應用連線串(2026-04-18 輪替) if [ -n "${DATABASE_URL}" ]; then - sudo kubectl patch secret awoooi-secrets -n awoooi-prod --type='json' -p='[ + $KUBECTL patch secret awoooi-secrets -n awoooi-prod --type='json' -p='[ {"op":"add","path":"/data/DATABASE_URL","value":"'$(echo -n "${DATABASE_URL}" | base64 -w 0)'"} ]' && echo "✅ DATABASE_URL 已注入" || echo "⚠️ DATABASE_URL patch 失敗" else @@ -498,14 +508,14 @@ jobs: # MIGRATION_DATABASE_URL — CI migration 用 awoooi_migrator 限權帳號(ADR-090-B) if [ -n "${MIGRATION_DATABASE_URL}" ]; then - sudo kubectl patch secret awoooi-secrets -n awoooi-prod --type='json' -p='[ + $KUBECTL patch secret awoooi-secrets -n awoooi-prod --type='json' -p='[ {"op":"add","path":"/data/MIGRATION_DATABASE_URL","value":"'$(echo -n "${MIGRATION_DATABASE_URL}" | base64 -w 0)'"} ]' && echo "✅ MIGRATION_DATABASE_URL 已注入" || echo "⚠️ MIGRATION_DATABASE_URL patch 失敗" fi # REDIS_URL — Redis 連線(6380 on 188) if [ -n "${REDIS_URL}" ]; then - sudo kubectl patch secret awoooi-secrets -n awoooi-prod --type='json' -p='[ + $KUBECTL patch secret awoooi-secrets -n awoooi-prod --type='json' -p='[ {"op":"add","path":"/data/REDIS_URL","value":"'$(echo -n "${REDIS_URL}" | base64 -w 0)'"} ]' && echo "✅ REDIS_URL 已注入" || echo "⚠️ REDIS_URL patch 失敗" else @@ -514,64 +524,64 @@ jobs: # JWT_SECRET / JWT_ALGORITHM — API 認證 if [ -n "${JWT_SECRET}" ]; then - sudo kubectl patch secret awoooi-secrets -n awoooi-prod --type='json' -p='[ + $KUBECTL patch secret awoooi-secrets -n awoooi-prod --type='json' -p='[ {"op":"add","path":"/data/JWT_SECRET","value":"'$(echo -n "${JWT_SECRET}" | base64 -w 0)'"} ]' && echo "✅ JWT_SECRET 已注入" || echo "⚠️ JWT_SECRET patch 失敗" fi if [ -n "${JWT_ALGORITHM}" ]; then - sudo kubectl patch secret awoooi-secrets -n awoooi-prod --type='json' -p='[ + $KUBECTL patch secret awoooi-secrets -n awoooi-prod --type='json' -p='[ {"op":"add","path":"/data/JWT_ALGORITHM","value":"'$(echo -n "${JWT_ALGORITHM}" | base64 -w 0)'"} ]' && echo "✅ JWT_ALGORITHM 已注入" || echo "⚠️ JWT_ALGORITHM patch 失敗" fi # WEBHOOK_HMAC_SECRET — Alertmanager webhook HMAC 簽章 if [ -n "${WEBHOOK_HMAC_SECRET}" ]; then - sudo kubectl patch secret awoooi-secrets -n awoooi-prod --type='json' -p='[ + $KUBECTL patch secret awoooi-secrets -n awoooi-prod --type='json' -p='[ {"op":"add","path":"/data/WEBHOOK_HMAC_SECRET","value":"'$(echo -n "${WEBHOOK_HMAC_SECRET}" | base64 -w 0)'"} ]' && echo "✅ WEBHOOK_HMAC_SECRET 已注入" || echo "⚠️ WEBHOOK_HMAC_SECRET patch 失敗" fi # SENTRY_DSN — Sentry 錯誤追蹤(不是 auth token) if [ -n "${SENTRY_DSN}" ]; then - sudo kubectl patch secret awoooi-secrets -n awoooi-prod --type='json' -p='[ + $KUBECTL patch secret awoooi-secrets -n awoooi-prod --type='json' -p='[ {"op":"add","path":"/data/SENTRY_DSN","value":"'$(echo -n "${SENTRY_DSN}" | base64 -w 0)'"} ]' && echo "✅ SENTRY_DSN 已注入" || echo "⚠️ SENTRY_DSN patch 失敗" fi # CLAUDE_API_KEY — Claude 備援 LLM if [ -n "${CLAUDE_API_KEY}" ]; then - sudo kubectl patch secret awoooi-secrets -n awoooi-prod --type='json' -p='[ + $KUBECTL patch secret awoooi-secrets -n awoooi-prod --type='json' -p='[ {"op":"add","path":"/data/CLAUDE_API_KEY","value":"'$(echo -n "${CLAUDE_API_KEY}" | base64 -w 0)'"} ]' && echo "✅ CLAUDE_API_KEY 已注入" || echo "⚠️ CLAUDE_API_KEY patch 失敗" fi # GITEA_API_TOKEN — Gitea API Token(從 AWOOOI_GITEA_API_TOKEN 映射) if [ -n "${GITEA_API_TOKEN}" ]; then - sudo kubectl patch secret awoooi-secrets -n awoooi-prod --type='json' -p='[ + $KUBECTL patch secret awoooi-secrets -n awoooi-prod --type='json' -p='[ {"op":"add","path":"/data/GITEA_API_TOKEN","value":"'$(echo -n "${GITEA_API_TOKEN}" | base64 -w 0)'"} ]' && echo "✅ GITEA_API_TOKEN 已注入" || echo "⚠️ GITEA_API_TOKEN patch 失敗" fi # NEMOTRON_BOT_TOKEN / OPENCLAW_BOT_TOKEN — 多 Bot 架構 if [ -n "${NEMOTRON_BOT_TOKEN}" ]; then - sudo kubectl patch secret awoooi-secrets -n awoooi-prod --type='json' -p='[ + $KUBECTL patch secret awoooi-secrets -n awoooi-prod --type='json' -p='[ {"op":"add","path":"/data/NEMOTRON_BOT_TOKEN","value":"'$(echo -n "${NEMOTRON_BOT_TOKEN}" | base64 -w 0)'"} ]' && echo "✅ NEMOTRON_BOT_TOKEN 已注入" || echo "⚠️ NEMOTRON_BOT_TOKEN patch 失敗" fi if [ -n "${OPENCLAW_BOT_TOKEN}" ]; then - sudo kubectl patch secret awoooi-secrets -n awoooi-prod --type='json' -p='[ + $KUBECTL patch secret awoooi-secrets -n awoooi-prod --type='json' -p='[ {"op":"add","path":"/data/OPENCLAW_BOT_TOKEN","value":"'$(echo -n "${OPENCLAW_BOT_TOKEN}" | base64 -w 0)'"} ]' && echo "✅ OPENCLAW_BOT_TOKEN 已注入" || echo "⚠️ OPENCLAW_BOT_TOKEN patch 失敗" fi # SMTP_HOST / SRE_GROUP_CHAT_ID if [ -n "${SMTP_HOST}" ]; then - sudo kubectl patch secret awoooi-secrets -n awoooi-prod --type='json' -p='[ + $KUBECTL patch secret awoooi-secrets -n awoooi-prod --type='json' -p='[ {"op":"add","path":"/data/SMTP_HOST","value":"'$(echo -n "${SMTP_HOST}" | base64 -w 0)'"} ]' && echo "✅ SMTP_HOST 已注入" || echo "⚠️ SMTP_HOST patch 失敗" fi if [ -n "${SRE_GROUP_CHAT_ID}" ]; then - sudo kubectl patch secret awoooi-secrets -n awoooi-prod --type='json' -p='[ + $KUBECTL patch secret awoooi-secrets -n awoooi-prod --type='json' -p='[ {"op":"add","path":"/data/SRE_GROUP_CHAT_ID","value":"'$(echo -n "${SRE_GROUP_CHAT_ID}" | base64 -w 0)'"} ]' && echo "✅ SRE_GROUP_CHAT_ID 已注入" || echo "⚠️ SRE_GROUP_CHAT_ID patch 失敗" fi @@ -597,13 +607,13 @@ jobs: fi done if [ "$PRESENT" -eq "$EXPECTED_HOSTS" ]; then - sudo kubectl create secret generic awoooi-repair-known-hosts \ + $KUBECTL create secret generic awoooi-repair-known-hosts \ -n awoooi-prod \ --from-file=known_hosts=/tmp/known_hosts_repair \ - --dry-run=client -o yaml | sudo kubectl apply -f - \ + --dry-run=client -o yaml | $KUBECTL apply -f - \ && echo "✅ awoooi-repair-known-hosts Secret 已建立/更新" \ || echo "⚠️ awoooi-repair-known-hosts Secret 建立失敗 (非致命)" - sudo kubectl patch secret ssh-mcp-key -n awoooi-prod --type=merge \ + $KUBECTL patch secret ssh-mcp-key -n awoooi-prod --type=merge \ -p='{"data":{"known_hosts":"'$(base64 -w 0 /tmp/known_hosts_repair)'"}}' \ && echo "✅ ssh-mcp-key known_hosts 已更新(4 台主機完整)" \ || echo "⚠️ ssh-mcp-key known_hosts 更新失敗 (非致命)" @@ -634,20 +644,20 @@ jobs: mkdir -p ~/.ssh echo "$SSH_PRIVATE_KEY" > ~/.ssh/deploy_key chmod 600 ~/.ssh/deploy_key - ssh-keyscan 192.168.0.121 >> ~/.ssh/known_hosts 2>/dev/null + ssh-keyscan "${{ env.K8S_SSH_HOST }}" >> ~/.ssh/known_hosts 2>/dev/null IMAGE_TAG="${{ github.sha }}" HARBOR=192.168.0.110:5000 # ─── Step 1: Apply ConfigMap + ServiceRegistry (ArgoCD 管的是 Deployment,ConfigMap 仍直接 apply) ─── cat k8s/awoooi-prod/04-configmap.yaml | \ - ssh -i ~/.ssh/deploy_key wooo@192.168.0.121 \ - "export KUBECONFIG=/etc/rancher/k3s/k3s.yaml && sudo kubectl apply -f -" + ssh -i ~/.ssh/deploy_key "wooo@${{ env.K8S_SSH_HOST }}" \ + "KUBECTL='sudo kubectl --kubeconfig=/etc/rancher/k3s/k3s.yaml --server=${{ env.K8S_API_SERVER }}'; \$KUBECTL apply -f -" echo "✅ ConfigMap 已更新" cat k8s/awoooi-prod/15-service-registry-configmap.yaml | \ - ssh -i ~/.ssh/deploy_key wooo@192.168.0.121 \ - "export KUBECONFIG=/etc/rancher/k3s/k3s.yaml && sudo kubectl apply -f -" + ssh -i ~/.ssh/deploy_key "wooo@${{ env.K8S_SSH_HOST }}" \ + "KUBECTL='sudo kubectl --kubeconfig=/etc/rancher/k3s/k3s.yaml --server=${{ env.K8S_API_SERVER }}'; \$KUBECTL apply -f -" echo "✅ Service Registry ConfigMap 已更新" # ─── Step 2: 更新 kustomization.yaml image tag ─── @@ -688,23 +698,24 @@ jobs: } # ─── Step 4: 等待 ArgoCD sync + rollout ─── - ssh -i ~/.ssh/deploy_key wooo@192.168.0.121 \ + ssh -i ~/.ssh/deploy_key "wooo@${{ env.K8S_SSH_HOST }}" \ "EXPECTED_REVISION='${DEPLOY_REVISION}' bash -s" << 'ARGOCD_WAIT' set -e - export KUBECONFIG=/etc/rancher/k3s/k3s.yaml + K8S_API_SERVER="${{ env.K8S_API_SERVER }}" + KUBECTL="sudo kubectl --kubeconfig=/etc/rancher/k3s/k3s.yaml --server=${K8S_API_SERVER}" # 等待 ArgoCD Application Synced(最多 180s)。只看 # Synced/Healthy 可能誤判成上一個 revision 已同步,因此有 # deploy commit 時必須同時確認 status.sync.revision。 echo "⏳ 等待 ArgoCD sync..." - sudo kubectl annotate application awoooi-prod -n argocd \ + $KUBECTL annotate application awoooi-prod -n argocd \ argocd.argoproj.io/refresh=hard --overwrite >/dev/null 2>&1 || true for i in $(seq 1 36); do - SYNC=$(sudo kubectl get application awoooi-prod -n argocd \ + SYNC=$($KUBECTL get application awoooi-prod -n argocd \ -o jsonpath='{.status.sync.status}' 2>/dev/null || echo "Unknown") - HEALTH=$(sudo kubectl get application awoooi-prod -n argocd \ + HEALTH=$($KUBECTL get application awoooi-prod -n argocd \ -o jsonpath='{.status.health.status}' 2>/dev/null || echo "Unknown") - REVISION=$(sudo kubectl get application awoooi-prod -n argocd \ + REVISION=$($KUBECTL get application awoooi-prod -n argocd \ -o jsonpath='{.status.sync.revision}' 2>/dev/null || echo "Unknown") SHORT_REVISION=$(echo "$REVISION" | cut -c1-8) SHORT_EXPECTED=$(echo "$EXPECTED_REVISION" | cut -c1-8) @@ -723,15 +734,15 @@ jobs: done # 確認 rollout 完成 - sudo kubectl rollout status deployment/awoooi-api -n awoooi-prod --timeout=120s - sudo kubectl rollout status deployment/awoooi-web -n awoooi-prod --timeout=120s - sudo kubectl rollout status deployment/awoooi-worker -n awoooi-prod --timeout=120s + $KUBECTL rollout status deployment/awoooi-api -n awoooi-prod --timeout=120s + $KUBECTL rollout status deployment/awoooi-web -n awoooi-prod --timeout=120s + $KUBECTL rollout status deployment/awoooi-worker -n awoooi-prod --timeout=120s echo "✅ 部署完成" # Health Check HEALTH_PASS=0 for i in 1 2 3; do - HTTP_CODE=$(curl -s -w "%{http_code}" -o /dev/null --connect-timeout 10 "http://localhost:32334/api/v1/health") + HTTP_CODE=$(curl -s -w "%{http_code}" -o /dev/null --connect-timeout 10 "${{ env.API_HEALTH_URL }}") if [ "$HTTP_CODE" = "200" ]; then echo "✅ API 健康檢查通過" HEALTH_PASS=1 @@ -814,7 +825,8 @@ jobs: - name: Alert Chain Smoke Test id: alert_chain_smoke run: | - # 2026-04-05 Claude Code: 使用真實 API 地址(192.168.0.121:32334 NodePort) + # 2026-05-05 Codex: kubectl 走 control-plane 120,但 NodePort health + # 目前由 121 對外可達;兩者分開設定,避免 role drift 互相拖垮。 # Host runner launches the CI image explicitly to avoid act RWLayer=nil. if docker run --rm \ --name "awoooi-cd-${GITHUB_RUN_ID:-manual}-${GITHUB_RUN_ATTEMPT:-1}-alert-smoke" \ @@ -824,7 +836,7 @@ jobs: -v awoooi-api-venv-cache:/opt/api-venv \ -w /workspace \ "${{ env.CI_IMAGE }}" \ - bash -lc 'source /opt/api-venv/bin/activate && python3 scripts/alert_chain_smoke_test.py --api-url http://192.168.0.121:32334 --json | tee /tmp/alert_chain_result.json'; then + bash -lc 'source /opt/api-venv/bin/activate && python3 scripts/alert_chain_smoke_test.py --api-url ${{ env.ALERT_CHAIN_API_URL }} --json | tee /tmp/alert_chain_result.json'; then echo "alert_chain_status=pass" >> $GITHUB_OUTPUT else echo "alert_chain_status=fail" >> $GITHUB_OUTPUT diff --git a/k8s/awoooi-prod/04-configmap.yaml b/k8s/awoooi-prod/04-configmap.yaml index 036c3886..1a90554e 100644 --- a/k8s/awoooi-prod/04-configmap.yaml +++ b/k8s/awoooi-prod/04-configmap.yaml @@ -21,10 +21,11 @@ data: # GCP-A(via 110:11435) → GCP-B(via 110:11436) → Local(via 110:11437) 統一走 nginx proxy # 110 nginx proxy 轉發:11435→GCP-A, 11436→GCP-B, 11437→192.168.0.111:11434 # K8s pods 不可直連 GCP:11434(NetworkPolicy 外網 egress 只開 443) - # 2026-05-05 ogt C2 修復:Local fallback 改走 110:11437 統一 nginx proxy,增強可觀測性 + # 2026-05-05 Codex: 110:11437 proxy 從 pod 內 connection refused,暫回直連 111; + # 保持告警成本路由為 GCP-A → GCP-B → 111 → Gemini backup。 OLLAMA_URL: "http://192.168.0.110:11435" OLLAMA_SECONDARY_URL: "http://192.168.0.110:11436" - OLLAMA_FALLBACK_URL: "http://192.168.0.110:11437" + OLLAMA_FALLBACK_URL: "http://192.168.0.111:11434" OPENCLAW_URL: "http://192.168.0.188:8088" KALI_SCANNER_URL: "http://192.168.0.112:8080" SIGNOZ_URL: "http://192.168.0.188:3301"