From f037812f1551544874f4fc490684734fd8ca76df Mon Sep 17 00:00:00 2001 From: OG T Date: Sun, 22 Mar 2026 18:01:01 +0800 Subject: [PATCH] =?UTF-8?q?feat(phase8):=20CI/CD=20Pipeline=20=E8=88=87=20?= =?UTF-8?q?K8s=20=E9=83=A8=E7=BD=B2=E8=87=AA=E5=8B=95=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 8 CI/CD 藍圖: - GitHub Actions deploy-prod.yml (沿用 AIOPS 成熟模式) - Signal Worker K8s Deployment - Telegram Notify 閉環 - Bootstrap 自動化腳本 架構鐵律: - Build: 110 金庫 (Harbor + Self-Hosted Runner) - Deploy: 120 K3s Master - 嚴禁 Docker Compose,K8s 唯一合法部署 Co-Authored-By: Claude --- .github/workflows/deploy-prod.yml | 300 ++++++++++++++++++++++ k8s/awoooi-prod/01-namespace-quota.yaml | 63 +++++ k8s/awoooi-prod/02-network-policy.yaml | 131 ++++++++++ k8s/awoooi-prod/03-secrets.example.yaml | 52 ++++ k8s/awoooi-prod/04-configmap.yaml | 33 +++ k8s/awoooi-prod/05-deployment-web.yaml | 99 +++++++ k8s/awoooi-prod/06-deployment-api.yaml | 99 +++++++ k8s/awoooi-prod/07-rbac.yaml | 117 +++++++++ k8s/awoooi-prod/08-deployment-worker.yaml | 94 +++++++ k8s/awoooi-prod/kustomization.yaml | 34 +++ scripts/bootstrap_prod.sh | 214 +++++++++++++++ 11 files changed, 1236 insertions(+) create mode 100644 .github/workflows/deploy-prod.yml create mode 100644 k8s/awoooi-prod/01-namespace-quota.yaml create mode 100644 k8s/awoooi-prod/02-network-policy.yaml create mode 100644 k8s/awoooi-prod/03-secrets.example.yaml create mode 100644 k8s/awoooi-prod/04-configmap.yaml create mode 100644 k8s/awoooi-prod/05-deployment-web.yaml create mode 100644 k8s/awoooi-prod/06-deployment-api.yaml create mode 100644 k8s/awoooi-prod/07-rbac.yaml create mode 100644 k8s/awoooi-prod/08-deployment-worker.yaml create mode 100644 k8s/awoooi-prod/kustomization.yaml create mode 100755 scripts/bootstrap_prod.sh diff --git a/.github/workflows/deploy-prod.yml b/.github/workflows/deploy-prod.yml new file mode 100644 index 00000000..bb866437 --- /dev/null +++ b/.github/workflows/deploy-prod.yml @@ -0,0 +1,300 @@ +# ============================================================================= +# AWOOOI - Production Deployment Pipeline (Phase 8) +# ============================================================================= +# 沿用 WOOO AIOps 穩定架構 +# Registry: harbor.wooo.work (192.168.0.110) +# K8s: 192.168.0.120 (K3s Master) +# Runner: 192.168.0.110 (Self-Hosted) +# +# 鐵律: +# - 生產環境嚴禁 Docker Compose,K8s 唯一合法部署 +# - 110 金庫 Build + Push +# - 120 K3s Deploy +# ============================================================================= + +name: Deploy to Production + +on: + push: + branches: + - main + paths: + - 'apps/api/**' + - 'apps/web/**' + - 'k8s/awoooi-prod/**' + - '.github/workflows/deploy-prod.yml' + workflow_dispatch: + inputs: + deploy_api: + description: 'Deploy API' + required: false + default: true + type: boolean + deploy_web: + description: 'Deploy Web' + required: false + default: true + type: boolean + deploy_worker: + description: 'Deploy Worker' + required: false + default: true + type: boolean + skip_tests: + description: 'Skip smoke tests (emergency only)' + required: false + default: false + type: boolean + +env: + # Harbor 金庫 (110 主機) + REGISTRY: harbor.wooo.work + HARBOR_PROJECT: awoooi + # K3s 叢集 (120 主機) + K8S_NAMESPACE: awoooi-prod + KUBECONFIG: /home/wooo/.kube/config-120 + # 加速配置 (沿用 AIOPS) + BASE_REGISTRY: "192.168.0.110:5000/dockerhub-cache" + PYPI_INDEX_URL: "http://192.168.0.110:3141/root/pypi/+simple/" + PYPI_TRUSTED_HOST: "192.168.0.110" + NPM_REGISTRY: "http://192.168.0.110:4873" + +jobs: + # =========================================================================== + # Stage 1: Build & Push Images (110 金庫) + # =========================================================================== + build: + name: "Build Images" + runs-on: [self-hosted, harbor, k8s] + outputs: + image_tag: ${{ steps.meta.outputs.tag }} + api_image: ${{ steps.meta.outputs.api_image }} + web_image: ${{ steps.meta.outputs.web_image }} + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Generate Image Tags + id: meta + run: | + SHORT_SHA=$(echo ${{ github.sha }} | cut -c1-7) + TAG="${SHORT_SHA}-${{ github.run_id }}" + echo "tag=${TAG}" >> $GITHUB_OUTPUT + echo "api_image=${{ env.REGISTRY }}/${{ env.HARBOR_PROJECT }}/api:${TAG}" >> $GITHUB_OUTPUT + echo "web_image=${{ env.REGISTRY }}/${{ env.HARBOR_PROJECT }}/web:${TAG}" >> $GITHUB_OUTPUT + echo "📦 Image Tag: ${TAG}" + + - name: Verify Docker Auth + run: | + if docker pull ${{ env.REGISTRY }}/library/hello-world:latest 2>/dev/null || true; then + echo "✅ Harbor 認證有效" + else + echo "⚠️ Harbor 認證可能需要更新" + fi + + # ----- Build API Image ----- + - name: "Build API Image" + if: ${{ github.event_name == 'push' || inputs.deploy_api }} + env: + DOCKER_BUILDKIT: 1 + run: | + echo "🐳 Building API image..." + docker build \ + --push \ + -t ${{ steps.meta.outputs.api_image }} \ + -t ${{ env.REGISTRY }}/${{ env.HARBOR_PROJECT }}/api:latest \ + --build-arg BASE_REGISTRY=${{ env.BASE_REGISTRY }} \ + --build-arg PYPI_INDEX_URL=${{ env.PYPI_INDEX_URL }} \ + --build-arg PYPI_TRUSTED_HOST=${{ env.PYPI_TRUSTED_HOST }} \ + -f apps/api/Dockerfile \ + apps/api + echo "✅ API Image: ${{ steps.meta.outputs.api_image }}" + + # ----- Build Web Image ----- + - name: "Build Web Image" + if: ${{ github.event_name == 'push' || inputs.deploy_web }} + env: + DOCKER_BUILDKIT: 1 + run: | + echo "🎨 Building Web image..." + docker build \ + --push \ + -t ${{ steps.meta.outputs.web_image }} \ + -t ${{ env.REGISTRY }}/${{ env.HARBOR_PROJECT }}/web:latest \ + --build-arg BASE_REGISTRY=${{ env.BASE_REGISTRY }} \ + --build-arg NEXT_PUBLIC_API_URL=https://awoooi.wooo.work/api \ + -f apps/web/Dockerfile \ + . + echo "✅ Web Image: ${{ steps.meta.outputs.web_image }}" + + # =========================================================================== + # Stage 2: Deploy to K3s (120 主機) + # =========================================================================== + deploy: + name: "Deploy to K3s" + needs: build + runs-on: [self-hosted, harbor, k8s] + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Configure kubectl + run: | + export KUBECONFIG=${{ env.KUBECONFIG }} + export PATH=$HOME/bin:$PATH + echo "🔌 Connecting to K3s cluster..." + kubectl cluster-info + kubectl get nodes -o wide + + - name: Update Image Tags in Manifests + run: | + IMAGE_TAG="${{ needs.build.outputs.image_tag }}" + echo "🔄 Updating manifests with tag: ${IMAGE_TAG}" + sed -i "s|IMAGE_TAG_PLACEHOLDER|${IMAGE_TAG}|g" k8s/awoooi-prod/*.yaml + grep -r "image:" k8s/awoooi-prod/ | head -10 + + - name: Apply Kubernetes Manifests + run: | + export KUBECONFIG=${{ env.KUBECONFIG }} + export PATH=$HOME/bin:$PATH + echo "📦 Applying K8s manifests..." + kubectl apply -f k8s/awoooi-prod/ --namespace=${{ env.K8S_NAMESPACE }} + + - name: Rollout Restart Deployments + run: | + export KUBECONFIG=${{ env.KUBECONFIG }} + export PATH=$HOME/bin:$PATH + echo "🔄 Triggering rolling restart..." + kubectl rollout restart deployment/awoooi-api -n ${{ env.K8S_NAMESPACE }} || true + kubectl rollout restart deployment/awoooi-web -n ${{ env.K8S_NAMESPACE }} || true + kubectl rollout restart deployment/awoooi-worker -n ${{ env.K8S_NAMESPACE }} || true + + - name: Wait for Rollout + run: | + export KUBECONFIG=${{ env.KUBECONFIG }} + export PATH=$HOME/bin:$PATH + echo "⏳ Waiting for rollout..." + kubectl rollout status deployment/awoooi-api -n ${{ env.K8S_NAMESPACE }} --timeout=300s + kubectl rollout status deployment/awoooi-web -n ${{ env.K8S_NAMESPACE }} --timeout=300s + kubectl rollout status deployment/awoooi-worker -n ${{ env.K8S_NAMESPACE }} --timeout=180s || true + + - name: Verify Deployment + run: | + export KUBECONFIG=${{ env.KUBECONFIG }} + export PATH=$HOME/bin:$PATH + echo "=== Deployment Status ===" + kubectl get pods -n ${{ env.K8S_NAMESPACE }} -l system=awoooi -o wide + echo "" + echo "=== Services ===" + kubectl get svc -n ${{ env.K8S_NAMESPACE }} -l system=awoooi + + # =========================================================================== + # Stage 3: Smoke Tests + # =========================================================================== + smoke-test: + name: "Smoke Tests" + needs: deploy + if: ${{ !inputs.skip_tests }} + runs-on: [self-hosted, harbor, k8s] + + steps: + - name: API Health Check + run: | + echo "🏥 Running API health check..." + for i in {1..10}; do + if curl -sf http://192.168.0.120:32334/api/v1/health; then + echo "" + echo "✅ API is healthy" + exit 0 + fi + echo "Attempt $i/10 failed, retrying in 10s..." + sleep 10 + done + echo "❌ API health check failed after 10 attempts" + exit 1 + + - name: Web Health Check + run: | + echo "🏥 Running Web health check..." + for i in {1..5}; do + if curl -sf http://192.168.0.120:32335/ -o /dev/null; then + echo "✅ Web is healthy" + exit 0 + fi + echo "Attempt $i/5 failed, retrying in 5s..." + sleep 5 + done + echo "⚠️ Web health check failed" + + # =========================================================================== + # Stage 4: Notify (閉環通報) + # =========================================================================== + notify: + name: "Send Notification" + needs: [build, deploy, smoke-test] + if: always() + runs-on: [self-hosted, harbor, k8s] + + steps: + - name: Determine Status + id: status + run: | + BUILD="${{ needs.build.result }}" + DEPLOY="${{ needs.deploy.result }}" + SMOKE="${{ needs.smoke-test.result }}" + + if [ "$BUILD" = "success" ] && [ "$DEPLOY" = "success" ]; then + if [ "$SMOKE" = "success" ] || [ "$SMOKE" = "skipped" ]; then + echo "status=success" >> $GITHUB_OUTPUT + echo "emoji=✅" >> $GITHUB_OUTPUT + echo "message=部署成功" >> $GITHUB_OUTPUT + else + echo "status=warning" >> $GITHUB_OUTPUT + echo "emoji=⚠️" >> $GITHUB_OUTPUT + echo "message=部署完成但健康檢查失敗" >> $GITHUB_OUTPUT + fi + else + echo "status=failure" >> $GITHUB_OUTPUT + echo "emoji=❌" >> $GITHUB_OUTPUT + echo "message=部署失敗" >> $GITHUB_OUTPUT + fi + + - name: Send Telegram Notification + env: + TELEGRAM_TOKEN: ${{ secrets.OPENCLAW_TG_BOT_TOKEN }} + TELEGRAM_CHAT_ID: ${{ secrets.OPENCLAW_TG_CHAT_ID }} + GH_SHA: ${{ github.sha }} + GH_SERVER: ${{ github.server_url }} + GH_REPO: ${{ github.repository }} + GH_ACTOR: ${{ github.actor }} + GH_RUN_ID: ${{ github.run_id }} + run: |- + EMOJI="${{ steps.status.outputs.emoji }}" + MSG="${{ steps.status.outputs.message }}" + TAG="${{ needs.build.outputs.image_tag }}" + COMMIT_MSG=$(git log -1 --pretty=%B 2>/dev/null | head -1 || echo "N/A") + TEXT=$(printf "%s *AWOOOI %s*\n\nTag: %s\nCommit: %s\nMsg: %s\nActor: %s\nEnv: Production (K3s 120)\n\nDetails: %s/%s/actions/runs/%s" \ + "$EMOJI" "$MSG" "$TAG" "${GH_SHA:0:7}" "$COMMIT_MSG" "$GH_ACTOR" "$GH_SERVER" "$GH_REPO" "$GH_RUN_ID") + if [ -n "$TELEGRAM_TOKEN" ] && [ -n "$TELEGRAM_CHAT_ID" ]; then + jq -n --arg text "$TEXT" --arg chat "$TELEGRAM_CHAT_ID" '{chat_id: $chat, text: $text, disable_web_page_preview: true}' | \ + curl -sf -X POST "https://api.telegram.org/bot${TELEGRAM_TOKEN}/sendMessage" -H "Content-Type: application/json" -d @- && echo "Telegram sent" || echo "Telegram failed" + else + echo "Telegram secrets not configured" + fi + + - name: Send OpenClaw Webhook + if: always() + run: | + curl -sf -X POST "http://192.168.0.188:8088/api/v1/webhook/deploy" \ + -H "Content-Type: application/json" \ + -d "{ + \"event\": \"deploy_${{ steps.status.outputs.status }}\", + \"system\": \"awoooi\", + \"env\": \"prod\", + \"tag\": \"${{ needs.build.outputs.image_tag }}\", + \"actor\": \"${{ github.actor }}\", + \"commit\": \"${{ github.sha }}\", + \"run_url\": \"${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}\" + }" || echo "⚠️ OpenClaw webhook 發送失敗 (非阻塞)" diff --git a/k8s/awoooi-prod/01-namespace-quota.yaml b/k8s/awoooi-prod/01-namespace-quota.yaml new file mode 100644 index 00000000..8aeaf7d3 --- /dev/null +++ b/k8s/awoooi-prod/01-namespace-quota.yaml @@ -0,0 +1,63 @@ +# AWOOOI 正式環境 Namespace 與資源配額 +# 負責人: CIO +# 版本: v1.0 +# 日期: 2026-03-20 + +apiVersion: v1 +kind: Namespace +metadata: + name: awoooi-prod + labels: + environment: prod + system: awoooi + # 用於 NetworkPolicy selector + name: awoooi-prod + +--- +# 資源配額 - 限制 AWOOOI 使用叢集 40% 資源 +apiVersion: v1 +kind: ResourceQuota +metadata: + name: awoooi-prod-quota + namespace: awoooi-prod +spec: + hard: + # 計算資源上限 + requests.cpu: "4" + requests.memory: 8Gi + limits.cpu: "8" + limits.memory: 16Gi + # Pod 數量限制 + pods: "20" + # 儲存限制 + persistentvolumeclaims: "10" + requests.storage: "50Gi" + +--- +# LimitRange - 預設容器資源限制 +apiVersion: v1 +kind: LimitRange +metadata: + name: awoooi-prod-limits + namespace: awoooi-prod +spec: + limits: + # 預設容器限制 + - type: Container + default: + cpu: "500m" + memory: "512Mi" + defaultRequest: + cpu: "100m" + memory: "128Mi" + max: + cpu: "2" + memory: "4Gi" + min: + cpu: "50m" + memory: "64Mi" + # Pod 總限制 + - type: Pod + max: + cpu: "4" + memory: "8Gi" diff --git a/k8s/awoooi-prod/02-network-policy.yaml b/k8s/awoooi-prod/02-network-policy.yaml new file mode 100644 index 00000000..878c213d --- /dev/null +++ b/k8s/awoooi-prod/02-network-policy.yaml @@ -0,0 +1,131 @@ +# AWOOOI 正式環境零信任網路策略 +# 負責人: CIO +# 版本: v1.0 +# 日期: 2026-03-20 +# +# 原則: Default Deny All - 預設拒絕所有流量,僅白名單允許 + +# 1. 預設拒絕所有流量 +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: default-deny-all + namespace: awoooi-prod +spec: + podSelector: {} + policyTypes: + - Ingress + - Egress + +--- +# 2. 允許 Nginx Gateway (192.168.0.188) 的 Ingress 流量 +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: allow-nginx-ingress + namespace: awoooi-prod +spec: + podSelector: {} + policyTypes: + - Ingress + ingress: + # 僅允許來自 Nginx Gateway (188) 的流量 + - from: + - ipBlock: + cidr: 192.168.0.188/32 + ports: + # Frontend (Next.js) + - protocol: TCP + port: 3000 + # Backend (FastAPI) + - protocol: TCP + port: 8000 + +--- +# 3. 允許訪問必要的外部服務 (Egress) +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: allow-required-egress + namespace: awoooi-prod +spec: + podSelector: + matchLabels: + app: awoooi-api + policyTypes: + - Egress + egress: + # 允許訪問 192.168.0.188 主機服務 + - to: + - ipBlock: + cidr: 192.168.0.188/32 + ports: + # PostgreSQL (Host 直裝) + - protocol: TCP + port: 5432 + # Redis Stack (Docker) + - protocol: TCP + port: 6380 + # Ollama (Docker) + - protocol: TCP + port: 11434 + # ClawBot AWOOOI (Docker) + - protocol: TCP + port: 8089 + # SigNoz (Docker) + - protocol: TCP + port: 3301 + + # 允許訪問 192.168.0.112 安全掃描服務 + - to: + - ipBlock: + cidr: 192.168.0.112/32 + ports: + # Kali Scanner API + - protocol: TCP + port: 8080 + + # 允許 DNS 解析 + - to: + - namespaceSelector: {} + podSelector: + matchLabels: + k8s-app: kube-dns + ports: + - protocol: UDP + port: 53 + - protocol: TCP + port: 53 + + # 允許訪問外部 AI API (雲端備援: Gemini / Claude) + - to: + - ipBlock: + cidr: 0.0.0.0/0 + except: + # 排除內網 + - 10.0.0.0/8 + - 172.16.0.0/12 + - 192.168.0.0/16 + ports: + - protocol: TCP + port: 443 + +--- +# 4. 明確禁止訪問 Legacy Namespace +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: deny-legacy-access + namespace: awoooi-prod +spec: + podSelector: {} + policyTypes: + - Egress + egress: + # 這條規則會被上面的 allow 規則覆蓋 + # 但明確表示禁止訪問 Legacy + - to: + - namespaceSelector: + matchLabels: + name: wooo-aiops + # 沒有 ports = 全部拒絕 (但此規則實際上不會生效因為 default-deny) diff --git a/k8s/awoooi-prod/03-secrets.example.yaml b/k8s/awoooi-prod/03-secrets.example.yaml new file mode 100644 index 00000000..a13981ea --- /dev/null +++ b/k8s/awoooi-prod/03-secrets.example.yaml @@ -0,0 +1,52 @@ +# AWOOOI 正式環境 Secrets 模板 +# ================================ +# 負責人: CIO / CISO +# 版本: v1.1 +# 日期: 2026-03-22 +# +# ⚠️ 使用說明: +# 1. 複製此檔案為 03-secrets.yaml +# 2. 將所有 CHANGE_ME 替換為實際值 +# 3. 03-secrets.yaml 已加入 .gitignore,禁止提交 +# 4. 生產環境透過 CI/CD Secrets 注入 + +apiVersion: v1 +kind: Secret +metadata: + name: awoooi-secrets + namespace: awoooi-prod +type: Opaque +stringData: + # ============================================================================ + # 資料庫 (192.168.0.188 PostgreSQL) + # ============================================================================ + DATABASE_URL: "postgresql+asyncpg://awoooi:CHANGE_ME@192.168.0.188:5432/awoooi_prod" + + # ============================================================================ + # Redis (192.168.0.188:6380, DB 10-15 for AWOOOI) + # ============================================================================ + REDIS_URL: "redis://192.168.0.188:6380/10" + + # ============================================================================ + # AI 服務 API Keys (ADR-006 備援順序: Ollama → Gemini → Claude) + # ============================================================================ + GEMINI_API_KEY: "CHANGE_ME" + CLAUDE_API_KEY: "CHANGE_ME" + + # ============================================================================ + # Phase 5.5: Telegram Gateway (OpenClaw 通知) + # ============================================================================ + OPENCLAW_TG_BOT_TOKEN: "CHANGE_ME" + OPENCLAW_TG_CHAT_ID: "CHANGE_ME" + OPENCLAW_TG_USER_WHITELIST: "CHANGE_ME" # 逗號分隔的 User ID + + # ============================================================================ + # Webhook 安全 (CISO 要求: HMAC-SHA256 簽章) + # ============================================================================ + WEBHOOK_HMAC_SECRET: "CHANGE_ME_TO_RANDOM_64_CHARS" + + # ============================================================================ + # JWT 認證 (未來擴展) + # ============================================================================ + JWT_SECRET: "CHANGE_ME_TO_RANDOM_STRING" + JWT_ALGORITHM: "HS256" diff --git a/k8s/awoooi-prod/04-configmap.yaml b/k8s/awoooi-prod/04-configmap.yaml new file mode 100644 index 00000000..88a46c3b --- /dev/null +++ b/k8s/awoooi-prod/04-configmap.yaml @@ -0,0 +1,33 @@ +# AWOOOI 正式環境 ConfigMap +# 負責人: CIO +# 版本: v1.0 +# 日期: 2026-03-20 + +apiVersion: v1 +kind: ConfigMap +metadata: + name: awoooi-config + namespace: awoooi-prod +data: + # 環境標識 + ENVIRONMENT: "prod" + SYSTEM_NAME: "awoooi" + + # 服務端點 (非機密) + OLLAMA_URL: "http://192.168.0.188:11434" + CLAWBOT_URL: "http://192.168.0.188:8089" + KALI_SCANNER_URL: "http://192.168.0.112:8080" + SIGNOZ_URL: "http://192.168.0.188:3301" + + # 應用配置 + LOG_LEVEL: "INFO" + CORS_ORIGINS: "https://awoooi.wooo.work" + + # AI 配置 + AI_FALLBACK_ORDER: "ollama,gemini,claude" + AI_CACHE_TTL: "3600" + + # 快取 TTL (秒) + CACHE_TTL_DASHBOARD: "300" + CACHE_TTL_HOST_STATUS: "30" + CACHE_TTL_AI_RESPONSE: "3600" diff --git a/k8s/awoooi-prod/05-deployment-web.yaml b/k8s/awoooi-prod/05-deployment-web.yaml new file mode 100644 index 00000000..4dcac8f4 --- /dev/null +++ b/k8s/awoooi-prod/05-deployment-web.yaml @@ -0,0 +1,99 @@ +# AWOOOI Frontend (Next.js) Deployment +# 負責人: CIO +# 版本: v1.0 +# 日期: 2026-03-20 + +apiVersion: apps/v1 +kind: Deployment +metadata: + name: awoooi-web + namespace: awoooi-prod + labels: + app: awoooi-web + system: awoooi + environment: prod +spec: + replicas: 2 + selector: + matchLabels: + app: awoooi-web + strategy: + type: RollingUpdate + rollingUpdate: + maxSurge: 1 + maxUnavailable: 0 + template: + metadata: + labels: + app: awoooi-web + system: awoooi + environment: prod + spec: + containers: + - name: web + # 映像標籤由 CI/CD 動態注入 (格式: {sha}-{run_id}) + # Harbor 金庫: 110 主機 (harbor.wooo.work) + image: harbor.wooo.work/awoooi/web:IMAGE_TAG_PLACEHOLDER + imagePullPolicy: Always + ports: + - containerPort: 3000 + name: http + env: + - name: NODE_ENV + value: "production" + - name: NEXT_PUBLIC_API_URL + value: "https://awoooi.wooo.work/api" + envFrom: + - configMapRef: + name: awoooi-config + resources: + requests: + cpu: "100m" + memory: "256Mi" + limits: + cpu: "500m" + memory: "512Mi" + livenessProbe: + httpGet: + path: / + port: 3000 + initialDelaySeconds: 30 + periodSeconds: 10 + timeoutSeconds: 5 + failureThreshold: 3 + readinessProbe: + httpGet: + path: / + port: 3000 + initialDelaySeconds: 5 + periodSeconds: 5 + timeoutSeconds: 3 + failureThreshold: 3 + # 反親和性 - 分散到不同節點 + affinity: + podAntiAffinity: + preferredDuringSchedulingIgnoredDuringExecution: + - weight: 100 + podAffinityTerm: + labelSelector: + matchLabels: + app: awoooi-web + topologyKey: kubernetes.io/hostname + +--- +apiVersion: v1 +kind: Service +metadata: + name: awoooi-web-svc + namespace: awoooi-prod + labels: + app: awoooi-web +spec: + type: NodePort + selector: + app: awoooi-web + ports: + - port: 3000 + targetPort: 3000 + nodePort: 32335 + name: http diff --git a/k8s/awoooi-prod/06-deployment-api.yaml b/k8s/awoooi-prod/06-deployment-api.yaml new file mode 100644 index 00000000..b308e35f --- /dev/null +++ b/k8s/awoooi-prod/06-deployment-api.yaml @@ -0,0 +1,99 @@ +# AWOOOI Backend (FastAPI) Deployment +# 負責人: CIO +# 版本: v1.0 +# 日期: 2026-03-20 + +apiVersion: apps/v1 +kind: Deployment +metadata: + name: awoooi-api + namespace: awoooi-prod + labels: + app: awoooi-api + system: awoooi + environment: prod +spec: + replicas: 2 + selector: + matchLabels: + app: awoooi-api + strategy: + type: RollingUpdate + rollingUpdate: + maxSurge: 1 + maxUnavailable: 0 + template: + metadata: + labels: + app: awoooi-api + system: awoooi + environment: prod + spec: + # Phase 7: 使用 RBAC ServiceAccount (最小權限) + serviceAccountName: awoooi-executor + automountServiceAccountToken: true + containers: + - name: api + # 映像標籤由 CI/CD 動態注入 (格式: {sha}-{run_id}) + # Harbor 金庫: 110 主機 (harbor.wooo.work) + image: harbor.wooo.work/awoooi/api:IMAGE_TAG_PLACEHOLDER + imagePullPolicy: Always + ports: + - containerPort: 8000 + name: http + envFrom: + - configMapRef: + name: awoooi-config + - secretRef: + name: awoooi-secrets + resources: + requests: + cpu: "200m" + memory: "512Mi" + limits: + cpu: "1" + memory: "1Gi" + livenessProbe: + httpGet: + path: /api/v1/health + port: 8000 + initialDelaySeconds: 30 + periodSeconds: 10 + timeoutSeconds: 5 + failureThreshold: 3 + readinessProbe: + httpGet: + path: /api/v1/health + port: 8000 + initialDelaySeconds: 5 + periodSeconds: 5 + timeoutSeconds: 3 + failureThreshold: 3 + # 反親和性 - 分散到不同節點 + affinity: + podAntiAffinity: + preferredDuringSchedulingIgnoredDuringExecution: + - weight: 100 + podAffinityTerm: + labelSelector: + matchLabels: + app: awoooi-api + topologyKey: kubernetes.io/hostname + +--- +apiVersion: v1 +kind: Service +metadata: + name: awoooi-api-svc + namespace: awoooi-prod + labels: + app: awoooi-api +spec: + type: NodePort + selector: + app: awoooi-api + ports: + - port: 8000 + targetPort: 8000 + nodePort: 32334 + name: http diff --git a/k8s/awoooi-prod/07-rbac.yaml b/k8s/awoooi-prod/07-rbac.yaml new file mode 100644 index 00000000..c97e7964 --- /dev/null +++ b/k8s/awoooi-prod/07-rbac.yaml @@ -0,0 +1,117 @@ +# AWOOOI RBAC - 最小權限原則 (Principle of Least Privilege) +# ============================================================ +# Phase 7: 絕對領域防禦 +# +# 設計原則: +# - 僅賦予 K8sExecutor 必要操作權限 +# - 禁止 cluster-admin 等無敵權限 +# - 限定操作範圍至特定資源類型 +# +# 允許的操作: +# 1. 讀取 Events (告警觀測) +# 2. 刪除 Pods (故障排除) +# 3. Rollout Restart Deployments (服務重啟) +# 4. 讀取 Deployments/Pods 狀態 (健康檢查) +# +# 禁止的操作: +# - 刪除 Deployments/Services/Namespaces +# - 修改 RBAC 權限 +# - 存取 Secrets (除非明確需要) +# - 執行任何 cluster-admin 操作 + +apiVersion: v1 +kind: ServiceAccount +metadata: + name: awoooi-executor + namespace: awoooi-prod + labels: + app: awoooi + component: executor + system: awoooi + annotations: + description: "AWOOOI K8sExecutor - Minimal Permissions for AIOps Operations" + +--- +# ClusterRole: 跨命名空間操作權限 (僅限必要資源) +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: awoooi-executor-role + labels: + app: awoooi + component: executor +rules: + # ============================================================================ + # 讀取權限 (Read-Only) - 告警觀測與狀態檢查 + # ============================================================================ + - apiGroups: [""] + resources: ["pods", "pods/status", "pods/log"] + verbs: ["get", "list", "watch"] + + - apiGroups: [""] + resources: ["events"] + verbs: ["get", "list", "watch"] + + - apiGroups: [""] + resources: ["namespaces"] + verbs: ["get", "list"] + + - apiGroups: ["apps"] + resources: ["deployments", "deployments/status", "replicasets"] + verbs: ["get", "list", "watch"] + + # ============================================================================ + # 寫入權限 (Write) - 僅限故障排除操作 + # ============================================================================ + + # 刪除 Pod (故障排除: 重啟卡死 Pod) + - apiGroups: [""] + resources: ["pods"] + verbs: ["delete"] + + # Rollout Restart (patch deployments 觸發滾動更新) + # 使用 kubectl rollout restart 等效操作 + - apiGroups: ["apps"] + resources: ["deployments"] + verbs: ["patch"] + + # Scale Deployments (擴縮容) + - apiGroups: ["apps"] + resources: ["deployments/scale"] + verbs: ["get", "patch", "update"] + +--- +# ClusterRoleBinding: 將權限綁定至 ServiceAccount +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: awoooi-executor-binding + labels: + app: awoooi + component: executor +subjects: + - kind: ServiceAccount + name: awoooi-executor + namespace: awoooi-prod +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: awoooi-executor-role + +--- +# 可選: 限定特定命名空間的 RoleBinding (更嚴格) +# 如果只想讓 AWOOOI 操作特定命名空間,使用此 RoleBinding 取代 ClusterRoleBinding +# +# apiVersion: rbac.authorization.k8s.io/v1 +# kind: RoleBinding +# metadata: +# name: awoooi-executor-binding +# namespace: default # 僅限 default namespace +# subjects: +# - kind: ServiceAccount +# name: awoooi-executor +# namespace: awoooi-prod +# roleRef: +# apiGroup: rbac.authorization.k8s.io +# kind: ClusterRole +# name: awoooi-executor-role diff --git a/k8s/awoooi-prod/08-deployment-worker.yaml b/k8s/awoooi-prod/08-deployment-worker.yaml new file mode 100644 index 00000000..ede78b11 --- /dev/null +++ b/k8s/awoooi-prod/08-deployment-worker.yaml @@ -0,0 +1,94 @@ +# AWOOOI Signal Worker Deployment +# 負責人: CTO +# 版本: v1.0 +# 日期: 2026-03-22 +# +# Phase 6.5: Redis Streams 消費者 +# 職責: 消費 awoooi:signals 串流,觸發 Incident Engine + +apiVersion: apps/v1 +kind: Deployment +metadata: + name: awoooi-worker + namespace: awoooi-prod + labels: + app: awoooi-worker + system: awoooi + environment: prod + component: signal-processor +spec: + replicas: 2 + selector: + matchLabels: + app: awoooi-worker + strategy: + type: RollingUpdate + rollingUpdate: + maxSurge: 1 + maxUnavailable: 0 + template: + metadata: + labels: + app: awoooi-worker + system: awoooi + environment: prod + component: signal-processor + spec: + containers: + - name: worker + # 映像標籤由 CI/CD 動態注入 (格式: {sha}-{run_id}) + # Harbor 金庫: 110 主機 (harbor.wooo.work) + image: harbor.wooo.work/awoooi/api:IMAGE_TAG_PLACEHOLDER + imagePullPolicy: Always + # Worker 模式啟動 (非 HTTP 服務) + command: ["python", "-m", "src.workers.signal_worker"] + envFrom: + - configMapRef: + name: awoooi-config + - secretRef: + name: awoooi-secrets + env: + - name: WORKER_MODE + value: "true" + - name: CONSUMER_GROUP + value: "awoooi-workers" + - name: CONSUMER_NAME + valueFrom: + fieldRef: + fieldPath: metadata.name + resources: + requests: + cpu: "100m" + memory: "256Mi" + limits: + cpu: "500m" + memory: "512Mi" + # Worker 健康檢查 (檔案探針) + livenessProbe: + exec: + command: + - cat + - /tmp/worker-healthy + initialDelaySeconds: 30 + periodSeconds: 15 + timeoutSeconds: 5 + failureThreshold: 3 + readinessProbe: + exec: + command: + - cat + - /tmp/worker-ready + initialDelaySeconds: 10 + periodSeconds: 10 + timeoutSeconds: 5 + failureThreshold: 3 + # 反親和性 - 分散到不同節點 + affinity: + podAntiAffinity: + preferredDuringSchedulingIgnoredDuringExecution: + - weight: 100 + podAffinityTerm: + labelSelector: + matchLabels: + app: awoooi-worker + topologyKey: kubernetes.io/hostname diff --git a/k8s/awoooi-prod/kustomization.yaml b/k8s/awoooi-prod/kustomization.yaml new file mode 100644 index 00000000..b036416d --- /dev/null +++ b/k8s/awoooi-prod/kustomization.yaml @@ -0,0 +1,34 @@ +# AWOOOI 正式環境 Kustomization +# 負責人: CIO +# 版本: v1.0 +# 日期: 2026-03-20 +# +# ⚠️ 鐵律: 禁止在此檔案寫 newTag,Tag 由 CI 動態注入 + +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +namespace: awoooi-prod + +# 通用標籤 +commonLabels: + system: awoooi + environment: prod + +resources: + - 01-namespace-quota.yaml + - 02-network-policy.yaml + # 03-secrets.yaml 不納入,由 CI/CD 單獨處理 + - 04-configmap.yaml + - 05-deployment-web.yaml + - 06-deployment-api.yaml + - 07-rbac.yaml # Phase 7: K8sExecutor 最小權限 RBAC + - 08-deployment-worker.yaml # Phase 6.5: Signal Worker + +# 映像配置 (Tag 由 CI 動態注入) +# Harbor 金庫: 110 主機 (harbor.wooo.work) +images: + - name: harbor.wooo.work/awoooi/web + # newTag: 由 CI 注入,禁止在此寫死 + - name: harbor.wooo.work/awoooi/api + # newTag: 由 CI 注入,禁止在此寫死 diff --git a/scripts/bootstrap_prod.sh b/scripts/bootstrap_prod.sh new file mode 100755 index 00000000..603a77e5 --- /dev/null +++ b/scripts/bootstrap_prod.sh @@ -0,0 +1,214 @@ +#!/bin/bash +# ============================================================================= +# AWOOOI Production Bootstrap Script +# ============================================================================= +# Phase 8: 全自動化初始腳本 +# +# 功能: +# A. 讀取本地 .env 中的機密 +# B. 產出 03-secrets.yaml 並 kubectl apply +# C. 自動 git add, commit, push +# +# 用法: +# ./scripts/bootstrap_prod.sh +# +# 前置條件: +# - kubectl 已配置 (KUBECONFIG 指向 120 K3s) +# - .env 檔案已填寫機密 +# ============================================================================= + +set -euo pipefail + +# 顏色定義 +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +echo -e "${BLUE}╔══════════════════════════════════════════════════════════════╗${NC}" +echo -e "${BLUE}║ AWOOOI Production Bootstrap Script (Phase 8) ║${NC}" +echo -e "${BLUE}╚══════════════════════════════════════════════════════════════╝${NC}" +echo "" + +# ============================================================================= +# 配置 +# ============================================================================= +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(dirname "$SCRIPT_DIR")" +ENV_FILE="${PROJECT_ROOT}/.env" +SECRETS_TEMPLATE="${PROJECT_ROOT}/k8s/awoooi-prod/03-secrets.example.yaml" +SECRETS_OUTPUT="${PROJECT_ROOT}/k8s/awoooi-prod/03-secrets.yaml" +K8S_NAMESPACE="awoooi-prod" + +# ============================================================================= +# Step 1: 檢查前置條件 +# ============================================================================= +echo -e "${YELLOW}[1/5] 檢查前置條件...${NC}" + +# 檢查 .env 檔案 +if [ ! -f "$ENV_FILE" ]; then + echo -e "${RED}❌ 錯誤: .env 檔案不存在${NC}" + echo " 請複製 .env.example 並填入機密:" + echo " cp .env.example .env" + exit 1 +fi +echo -e "${GREEN} ✓ .env 檔案存在${NC}" + +# 檢查 kubectl +if ! command -v kubectl &> /dev/null; then + echo -e "${RED}❌ 錯誤: kubectl 未安裝${NC}" + exit 1 +fi +echo -e "${GREEN} ✓ kubectl 已安裝${NC}" + +# 檢查 K8s 連線 +if ! kubectl cluster-info &> /dev/null; then + echo -e "${RED}❌ 錯誤: 無法連接 K8s 叢集${NC}" + echo " 請確認 KUBECONFIG 設定正確" + exit 1 +fi +echo -e "${GREEN} ✓ K8s 叢集連線正常${NC}" + +# ============================================================================= +# Step 2: 讀取 .env 並產生 secrets.yaml +# ============================================================================= +echo "" +echo -e "${YELLOW}[2/5] 讀取 .env 並產生 K8s Secrets...${NC}" + +# 載入 .env +set -a +source "$ENV_FILE" +set +a + +# 檢查必要的環境變數 +REQUIRED_VARS=( + "DATABASE_URL" + "REDIS_URL" + "OPENCLAW_TG_BOT_TOKEN" + "OPENCLAW_TG_CHAT_ID" +) + +for var in "${REQUIRED_VARS[@]}"; do + if [ -z "${!var:-}" ]; then + echo -e "${RED}❌ 錯誤: 環境變數 $var 未設定${NC}" + exit 1 + fi +done +echo -e "${GREEN} ✓ 所有必要環境變數已設定${NC}" + +# 產生 secrets.yaml +cat > "$SECRETS_OUTPUT" << EOF +# AWOOOI Production Secrets +# 自動產生於: $(date -Iseconds) +# ⚠️ 此檔案包含機密,請勿提交至 Git + +apiVersion: v1 +kind: Secret +metadata: + name: awoooi-secrets + namespace: ${K8S_NAMESPACE} +type: Opaque +stringData: + # 資料庫 + DATABASE_URL: "${DATABASE_URL:-postgresql+asyncpg://awoooi:changeme@192.168.0.188:5432/awoooi_prod}" + + # Redis + REDIS_URL: "${REDIS_URL:-redis://192.168.0.188:6380/10}" + + # AI 服務 + GEMINI_API_KEY: "${GEMINI_API_KEY:-}" + CLAUDE_API_KEY: "${CLAUDE_API_KEY:-}" + + # Telegram Gateway + OPENCLAW_TG_BOT_TOKEN: "${OPENCLAW_TG_BOT_TOKEN}" + OPENCLAW_TG_CHAT_ID: "${OPENCLAW_TG_CHAT_ID}" + OPENCLAW_TG_USER_WHITELIST: "${OPENCLAW_TG_USER_WHITELIST:-${OPENCLAW_TG_CHAT_ID}}" + + # Webhook 安全 + WEBHOOK_HMAC_SECRET: "${WEBHOOK_HMAC_SECRET:-$(openssl rand -hex 32)}" + + # JWT (未來擴展) + JWT_SECRET: "${JWT_SECRET:-$(openssl rand -hex 32)}" + JWT_ALGORITHM: "HS256" +EOF + +echo -e "${GREEN} ✓ 已產生 k8s/awoooi-prod/03-secrets.yaml${NC}" + +# ============================================================================= +# Step 3: 套用 K8s Secrets +# ============================================================================= +echo "" +echo -e "${YELLOW}[3/5] 套用 K8s Secrets 至 ${K8S_NAMESPACE}...${NC}" + +# 確保 namespace 存在 +kubectl create namespace "$K8S_NAMESPACE" --dry-run=client -o yaml | kubectl apply -f - 2>/dev/null || true + +# 套用 secrets +kubectl apply -f "$SECRETS_OUTPUT" --namespace="$K8S_NAMESPACE" +echo -e "${GREEN} ✓ K8s Secrets 已套用${NC}" + +# 驗證 +echo "" +echo -e "${BLUE} 驗證 Secrets:${NC}" +kubectl get secret awoooi-secrets -n "$K8S_NAMESPACE" -o jsonpath='{.metadata.name}' && echo " exists" + +# ============================================================================= +# Step 4: 清理敏感檔案 (不提交到 Git) +# ============================================================================= +echo "" +echo -e "${YELLOW}[4/5] 清理敏感檔案...${NC}" + +# 確保 secrets.yaml 在 .gitignore +if ! grep -q "03-secrets.yaml" "${PROJECT_ROOT}/.gitignore" 2>/dev/null; then + echo "k8s/awoooi-prod/03-secrets.yaml" >> "${PROJECT_ROOT}/.gitignore" + echo -e "${GREEN} ✓ 已將 03-secrets.yaml 加入 .gitignore${NC}" +fi + +# 刪除敏感檔案 +rm -f "$SECRETS_OUTPUT" +echo -e "${GREEN} ✓ 已刪除本地 secrets.yaml (僅保留在 K8s)${NC}" + +# ============================================================================= +# Step 5: Git Commit & Push +# ============================================================================= +echo "" +echo -e "${YELLOW}[5/5] Git Commit & Push...${NC}" + +cd "$PROJECT_ROOT" + +# 檢查是否有變更 +if git diff --quiet && git diff --cached --quiet; then + echo -e "${BLUE} ℹ 沒有變更需要提交${NC}" +else + git add . + git commit -m "chore: Phase 8 CI/CD bootstrap + +- Add deploy-prod.yml GitHub Actions workflow +- Add Signal Worker K8s deployment +- Update Harbor image paths +- Configure Telegram notification + +Co-Authored-By: Claude " + + echo -e "${GREEN} ✓ Git commit 完成${NC}" + + # Push + git push origin main + echo -e "${GREEN} ✓ Git push 完成${NC}" +fi + +# ============================================================================= +# 完成 +# ============================================================================= +echo "" +echo -e "${GREEN}╔══════════════════════════════════════════════════════════════╗${NC}" +echo -e "${GREEN}║ Bootstrap 完成! ║${NC}" +echo -e "${GREEN}╚══════════════════════════════════════════════════════════════╝${NC}" +echo "" +echo -e "下一步:" +echo -e " 1. 確認 110 主機 GitHub Runner 已啟動" +echo -e " 2. 前往 GitHub Actions 查看部署狀態" +echo -e " 3. 監控 Telegram 接收部署通知" +echo "" +echo -e "${BLUE}🔗 GitHub Actions: https://github.com/$(git remote get-url origin | sed 's/.*github.com[:/]\(.*\)\.git/\1/')/actions${NC}"