feat(phase8): CI/CD Pipeline 與 K8s 部署自動化
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 <noreply@anthropic.com>
This commit is contained in:
300
.github/workflows/deploy-prod.yml
vendored
Normal file
300
.github/workflows/deploy-prod.yml
vendored
Normal file
@@ -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 發送失敗 (非阻塞)"
|
||||
63
k8s/awoooi-prod/01-namespace-quota.yaml
Normal file
63
k8s/awoooi-prod/01-namespace-quota.yaml
Normal file
@@ -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"
|
||||
131
k8s/awoooi-prod/02-network-policy.yaml
Normal file
131
k8s/awoooi-prod/02-network-policy.yaml
Normal file
@@ -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)
|
||||
52
k8s/awoooi-prod/03-secrets.example.yaml
Normal file
52
k8s/awoooi-prod/03-secrets.example.yaml
Normal file
@@ -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"
|
||||
33
k8s/awoooi-prod/04-configmap.yaml
Normal file
33
k8s/awoooi-prod/04-configmap.yaml
Normal file
@@ -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"
|
||||
99
k8s/awoooi-prod/05-deployment-web.yaml
Normal file
99
k8s/awoooi-prod/05-deployment-web.yaml
Normal file
@@ -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
|
||||
99
k8s/awoooi-prod/06-deployment-api.yaml
Normal file
99
k8s/awoooi-prod/06-deployment-api.yaml
Normal file
@@ -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
|
||||
117
k8s/awoooi-prod/07-rbac.yaml
Normal file
117
k8s/awoooi-prod/07-rbac.yaml
Normal file
@@ -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
|
||||
94
k8s/awoooi-prod/08-deployment-worker.yaml
Normal file
94
k8s/awoooi-prod/08-deployment-worker.yaml
Normal file
@@ -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
|
||||
34
k8s/awoooi-prod/kustomization.yaml
Normal file
34
k8s/awoooi-prod/kustomization.yaml
Normal file
@@ -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 注入,禁止在此寫死
|
||||
214
scripts/bootstrap_prod.sh
Executable file
214
scripts/bootstrap_prod.sh
Executable file
@@ -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 <noreply@anthropic.com>"
|
||||
|
||||
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}"
|
||||
Reference in New Issue
Block a user