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:
OG T
2026-03-22 18:01:01 +08:00
parent ccdf757edd
commit f037812f15
11 changed files with 1236 additions and 0 deletions

300
.github/workflows/deploy-prod.yml vendored Normal file
View 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 ComposeK8s 唯一合法部署
# - 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 發送失敗 (非阻塞)"

View 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"

View 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)

View 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"

View 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"

View 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

View 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

View 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

View 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

View File

@@ -0,0 +1,34 @@
# AWOOOI 正式環境 Kustomization
# 負責人: CIO
# 版本: v1.0
# 日期: 2026-03-20
#
# ⚠️ 鐵律: 禁止在此檔案寫 newTagTag 由 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
View 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}"