diff --git a/Dockerfile.embed b/Dockerfile.embed
new file mode 100644
index 0000000..63665d0
--- /dev/null
+++ b/Dockerfile.embed
@@ -0,0 +1,123 @@
+# ============================================================
+# AIOps Embed Dockerfile — 前端嵌入后端二进制,单容器部署
+# Stage 1: 构建前端 (Node 20)
+# Stage 2: 构建后端 (Go 1.24, go:embed 前端)
+# Stage 3: Agent 交叉编译
+# Stage 4: 运行时 (Alpine, 单二进制服务前后端)
+# 署名 Logan.Li
+# ============================================================
+
+ARG VERSION=dev
+ARG BUILD_TIME=unknown
+
+# ==================== Stage 1: Frontend Builder ====================
+FROM node:20-alpine AS frontend-builder
+
+WORKDIR /app
+
+# 使用淘宝镜像加速
+RUN npm config set registry https://registry.npmmirror.com
+
+# 安装依赖(利用 Docker 缓存层)
+COPY frontend/package.json frontend/package-lock.json ./
+RUN npm ci
+
+# 复制源代码并构建
+COPY frontend/ ./
+RUN npm run build
+
+# ==================== Stage 2: Backend Builder ====================
+FROM golang:1.24-alpine AS backend-builder
+
+ARG VERSION
+ARG BUILD_TIME
+
+WORKDIR /app
+
+# 使用国内 Alpine 镜像源加速
+RUN sed -i 's|dl-cdn.alpinelinux.org|mirrors.aliyun.com|g' /etc/apk/repositories
+RUN apk add --no-cache git ca-certificates tzdata
+
+ENV TZ=Asia/Shanghai
+ENV GOPROXY=https://goproxy.cn,direct
+ENV GONOSUMCHECK=*
+
+# 复制后端 go.mod(利用 Docker 层缓存)
+COPY backend/go.mod backend/go.sum ./
+RUN go mod download
+
+# 复制后端源码
+COPY backend/ ./
+
+# ★ 核心:将前端构建产物复制到 embed 目录
+RUN rm -rf cmd/static && mkdir -p cmd/static
+COPY --from=frontend-builder /app/dist/ cmd/static/
+
+# 构建后端二进制(前端已嵌入,注入版本号)
+RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build \
+ -ldflags="-w -s -X main.Version=${VERSION} -X main.BuildTime=${BUILD_TIME}" \
+ -o aiops ./cmd/
+
+# ==================== Stage 3: Agent Builder ====================
+FROM golang:1.24-alpine AS agent-builder
+
+ARG VERSION
+ARG BUILD_TIME
+
+WORKDIR /agent
+
+ENV GOPROXY=https://goproxy.cn,direct
+ENV GOSUMDB=off
+
+COPY agent/go.mod agent/go.sum ./
+RUN go mod download
+
+COPY agent/ ./
+
+RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-s -w -X main.version=${VERSION} -X main.buildTime=${BUILD_TIME}" -o bin/aiops-agent-linux-amd64 . && \
+ CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -ldflags="-s -w -X main.version=${VERSION} -X main.buildTime=${BUILD_TIME}" -o bin/aiops-agent-linux-arm64 . && \
+ CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 go build -ldflags="-s -w -X main.version=${VERSION} -X main.buildTime=${BUILD_TIME}" -o bin/aiops-agent-darwin-arm64 .
+
+# ==================== Stage 4: Runtime ====================
+FROM alpine:latest
+
+# 使用国内 Alpine 镜像源加速
+RUN sed -i 's|dl-cdn.alpinelinux.org|mirrors.aliyun.com|g' /etc/apk/repositories
+
+# 安装运行时依赖(与主 Dockerfile 保持一致)
+RUN apk --no-cache add ca-certificates tzdata bash curl openssh-client ansible sshpass git docker-cli docker-cli-buildx nodejs npm maven && \
+ curl -sLO "https://dl.k8s.io/release/$(curl -sL https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl" && \
+ chmod +x kubectl && mv kubectl /usr/local/bin/
+
+# Maven 阿里云镜像加速
+RUN mkdir -p /root/.m2 && \
+ echo 'aliyuncentralAliyun Mavenhttps://maven.aliyun.com/repository/public' > /root/.m2/settings.xml
+
+# npm 淘宝镜像加速
+RUN npm config set registry https://registry.npmmirror.com
+
+ENV TZ=Asia/Shanghai
+
+WORKDIR /app
+
+# 复制后端二进制(前端已嵌入)
+COPY --from=backend-builder /app/aiops .
+
+# 创建运行时目录
+RUN mkdir -p /app/agent/bin /app/agent/deploy
+
+# 复制 agent 二进制
+COPY --from=agent-builder /agent/bin/aiops-agent-linux-amd64 /app/agent/bin/
+COPY --from=agent-builder /agent/bin/aiops-agent-linux-arm64 /app/agent/bin/
+COPY --from=agent-builder /agent/bin/aiops-agent-darwin-arm64 /app/agent/bin/
+
+# 复制 agent 安装脚本
+COPY agent/deploy/install.sh /app/agent/deploy/
+COPY agent/deploy/aiops-agent.service /app/agent/deploy/
+
+EXPOSE 8080
+
+HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
+ CMD curl -f http://localhost:8080/health || exit 1
+
+CMD ["./aiops"]
diff --git a/backend/cmd/embed.go b/backend/cmd/embed.go
index 84505bf..d631f2f 100644
--- a/backend/cmd/embed.go
+++ b/backend/cmd/embed.go
@@ -62,7 +62,12 @@ func setupFrontend(r *gin.Engine) {
return
}
- // SPA fallback → index.html(React Router history 模式)
- c.FileFromFS("/index.html", httpFS)
+ // SPA fallback → 直接返回 index.html 字节(避免 http.FileServer 的 301 redirect)
+ indexBytes, err := fs.ReadFile(staticFS, "index.html")
+ if err != nil {
+ c.String(http.StatusInternalServerError, "前端文件缺失")
+ return
+ }
+ c.Data(http.StatusOK, "text/html; charset=utf-8", indexBytes)
})
}
diff --git a/deploy/k8s/README.md b/deploy/k8s/README.md
index 4ca06af..af87348 100644
--- a/deploy/k8s/README.md
+++ b/deploy/k8s/README.md
@@ -2,6 +2,8 @@
AIOps 平台在 Kubernetes 上的部署清单(Go/Gin 后端 + go:embed 前端 + MySQL 8.0 + Redis 7)。
+**单容器架构**:前端通过 `go:embed` 嵌入后端二进制,一个容器同时服务 API + WebSocket + SPA 静态资源,端口统一 8080。
+
所有资源部署在 **`devops`** 命名空间。
## 目录结构
@@ -9,150 +11,141 @@ AIOps 平台在 Kubernetes 上的部署清单(Go/Gin 后端 + go:embed 前端
| 文件 | 说明 |
|------|------|
| `namespace.yaml` | `devops` 命名空间 |
-| `configmap.yaml` | 后端 `config.yaml`(ConfigMap)+ MySQL/JWT 密码(Secret) |
-| `mysql.yaml` | MySQL 8.0 StatefulSet + Headless/ClusterIP Service + 8Gi 持久卷 |
-| `redis.yaml` | Redis 7 StatefulSet + Headless/ClusterIP Service + 2Gi 持久卷 |
-| `backend.yaml` | 后端 Deployment(端口 8080=API/WS,80=embed 前端)+ ClusterIP Service |
-| `ingress.yaml` | Ingress:`/api`、`/ws` → 8080;`/` → 80 |
+| `secret.yaml` | MySQL/JWT 密码(Secret) |
+| `configmap.yaml` | 后端 `config.yaml`(ConfigMap) |
+| `mysql.yaml` | MySQL 8.0 StatefulSet + Headless Service + 10Gi 持久卷 |
+| `redis.yaml` | Redis 7 StatefulSet + Headless Service + 2Gi 持久卷 |
+| `deployment.yaml` | AIOps Deployment(embed 单端口 8080)+ NodePort Service (30080) |
+| `ingress.yaml` | Ingress:所有流量 → aiops:8080(含 WebSocket 支持) |
-## 架构与端口
+## 架构
```
- ┌──────────────── Ingress (nginx) ────────────────┐
- /api, /ws ──▶│ │──▶ aiops-backend:8080 (API + WebSocket)
- / ──▶│ │──▶ aiops-backend:80 (go:embed 前端)
- └──────────────────────────────────────────────────┘
- │
- ┌──────────────┴──────────────┐
- ▼ ▼
- aiops-backend aiops-backend
- │ (DB_HOST=mysql) │ (REDIS_HOST=redis)
- ▼ ▼
- MySQL 8.0 (StatefulSet) Redis 7 (StatefulSet)
- aiops / aiops_pass_2026 无密码, db 0
- 数据库 aiops, 8Gi PV 2Gi PV
+ NodePort:30080 / Ingress
+ │
+ ▼
+ ┌─── aiops (Deployment) ───┐
+ │ 单端口 8080: │
+ │ • API (/api/*) │
+ │ • WebSocket (/ws/*) │
+ │ • 前端 SPA (go:embed) │
+ └───────────┬───────────────┘
+ │
+ ┌──────────────┼──────────────┐
+ ▼ ▼
+ MySQL 8.0 Redis 7
+ (StatefulSet) (StatefulSet)
+ mysql:3306 redis:6379
+ 10Gi PV 2Gi PV
```
-- **8080**:API + WebSocket
-- **80**:前端(go:embed 嵌入后端二进制,由后端进程提供静态资源)
-- 配置文件 `config.yaml` 中 DB/Redis 连接均以环境变量 `${VAR}` 占位,由 Deployment 的 `env` 注入;密码走 Secret。
+## 快速部署
-## 前置条件
+### 方式一:run.sh 一键部署(推荐)
-1. 一个可用的 Kubernetes 集群(≥ 1.22)。
-2. 已安装 **nginx Ingress Controller**。
-3. 集群具备可用的 **StorageClass**(MySQL/Redis 的 `volumeClaimTemplates` 依赖动态卷供应)。
- - minikube / kind 自带默认 StorageClass(如 `standard`)。
- - 如需指定,取消各 StatefulSet 中 `storageClassName` 的注释。
-4. 后端镜像 `aiops-backend:latest` 已构建并推送到集群可访问的镜像仓库(或本地 `kind load docker-image` / `minikube image load`)。
+```bash
+# 构建镜像 + 部署全部资源
+./run.sh k8s deploy
-## 部署步骤
+# 查看状态
+./run.sh k8s status
-按顺序 apply(资源间存在依赖:Secret → 数据库 → 后端 → Ingress):
+# 查看日志
+./run.sh k8s logs
+
+# 更新(重新构建镜像 + 滚动更新)
+./run.sh k8s upgrade
+
+# 清理
+./run.sh k8s delete
+```
+
+### 方式二:手动 kubectl
```bash
cd deploy/k8s
-# 1. 命名空间(必须最先)
+# 1. 全量部署(文件按序: namespace → secret → configmap → mysql → redis → deployment)
kubectl apply -f namespace.yaml
-
-# 2. 配置 + 密码(ConfigMap / Secret)
+kubectl apply -f secret.yaml
kubectl apply -f configmap.yaml
-
-# 3. MySQL(启动后自动建库 aiops 与用户 aiops)
kubectl apply -f mysql.yaml
-
-# 4. Redis
kubectl apply -f redis.yaml
-# 5. 等待数据库就绪
+# 2. 等待数据库就绪
kubectl -n devops rollout status statefulset/mysql
kubectl -n devops rollout status statefulset/redis
-# 6. 后端
-kubectl apply -f backend.yaml
-kubectl -n devops rollout status deployment/aiops-backend
+# 3. 构建镜像(在本地)
+cd ../..
+docker build -f Dockerfile.embed -t aiops:embed .
-# 7. Ingress(部署前修改 host 与 ingressClassName)
+# 4. 部署应用
+kubectl apply -f deployment.yaml
+kubectl -n devops rollout status deployment/aiops
+
+# 5. Ingress(按需修改 host)
kubectl apply -f ingress.yaml
```
-一键全量部署(文件名按字典序,namespace 会最先应用):
-
-```bash
-kubectl apply -f deploy/k8s/
-```
-
## 访问
-- 将 Ingress 中的 `host`(默认 `aiops.local`)解析到 Ingress Controller 的入口 IP:
- ```bash
- echo " aiops.local" | sudo tee -a /etc/hosts
- ```
-- 浏览器打开 `http://aiops.local` 即前端;API 在 `http://aiops.local/api`。
+```bash
+# NodePort 方式
+curl http://:30080/health
+
+# 或配置 Ingress host
+echo " aiops.local" | sudo tee -a /etc/hosts
+# 浏览器: http://aiops.local
+```
## 常用运维命令
```bash
# 查看所有资源
-kubectl -n devops get all,configmap,secret,ingress,pvc
+kubectl -n devops get all,pvc,configmap,secret
-# 查看后端日志
-kubectl -n devops logs -f deployment/aiops-backend
+# 查看应用日志
+kubectl -n devops logs -f deployment/aiops
+
+# 进入应用容器
+kubectl -n devops exec -it deployment/aiops -- bash
# 进入 MySQL
kubectl -n devops exec -it mysql-0 -- mysql -uaiops -paiops_pass_2026 aiops
-# 进入 Redis
-kubectl -n devops exec -it redis-0 -- redis-cli
+# 滚动更新(重新构建镜像后)
+kubectl -n devops rollout restart deployment/aiops
-# 进入后端容器
-kubectl -n devops exec -it deployment/aiops-backend -- sh
-
-# 查看 PVC 绑定状态
+# 查看 PVC
kubectl -n devops get pvc
```
-## 配置与密码
+## 配置说明
| 项目 | 来源 | 值 |
|------|------|----|
| MySQL 业务账号 | Secret `aiops-secret` | 用户 `aiops` / 密码 `aiops_pass_2026` / 库 `aiops` |
| MySQL root | Secret `aiops-secret` | `aiops_root_2026` |
-| JWT | Secret `aiops-secret` | `jwt-secret`(生产请替换为强随机值) |
+| JWT | Secret `aiops-secret` | `aiops-jwt-secret-key-2026` |
| Redis | 无密码 | db `0` |
-> ⚠️ 生产环境请勿使用仓库内默认密码。修改 `configmap.yaml` 中 Secret 的值后重新 apply,
-> 或使用 `kubectl create secret` 覆盖。
+> ⚠️ 生产环境请修改 `secret.yaml` 和 `configmap.yaml` 中的默认密码。
## 资源限额
| 组件 | CPU 请求/上限 | 内存请求/上限 |
|------|---------------|---------------|
-| backend | 100m / 500m | 512Mi / 1Gi |
-| mysql | 250m / 1 | 1Gi / 2Gi |
+| aiops | 100m / 1 | 256Mi / 1Gi |
+| mysql | 250m / 1 | 512Mi / 2Gi |
| redis | 50m / 250m | 128Mi / 256Mi |
-## 关于配置的环境变量占位
-
-`config.yaml`(位于 ConfigMap)中 DB/Redis 的 `host/port/user/password/dbname`、JWT `secret`
-均写成 `${ENV_VAR}` 占位形式,实际值在 `backend.yaml` 的 `env` 段注入:
-非敏感的 host/port/user/dbname 直接给值(指向 K8s Service),密码则 `valueFrom` 引用 Secret。
-
-后端基于 viper 读取配置。若 `${VAR}` 占位需要由环境变量直接替换嵌套键
-(如 `DB_PASSWORD` → `database.password`),建议在程序启动时设置键名映射:
-
-```go
-viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
-```
-
-否则可改为在 `config.yaml` 中直接写明连接串、密码仅由 Secret 注入。两种方式均可,
-当前清单采用「环境变量占位 + Secret」的标准 K8s 模式。
-
## 清理
```bash
+./run.sh k8s delete
+
+# 或手动(持久卷需单独删)
kubectl delete -f deploy/k8s/
-# 持久卷需单独删除(PVC 由 StatefulSet 保护,删除 STS 后 PVC 仍保留)
kubectl -n devops delete pvc --all
```
diff --git a/deploy/k8s/backend.yaml b/deploy/k8s/backend.yaml
deleted file mode 100644
index 17832ba..0000000
--- a/deploy/k8s/backend.yaml
+++ /dev/null
@@ -1,116 +0,0 @@
-# backend.yaml — AIOps 后端 (Deployment + Service)
-# ----------------------------------------------------------------------------
-# - 镜像 aiops-backend:latest(前端通过 go:embed 内嵌进同一二进制)
-# - 暴露两个端口:
-# 8080 = API + WebSocket
-# 80 = go:embed 前端 (SPA 静态资源)
-# - 配置: ConfigMap aiops-config 挂载到 /app/configs/config.yaml
-# - 密码: Secret aiops-secret 以环境变量注入(DB_PASSWORD / JWT_SECRET)
-# - Service: ClusterIP,对外暴露 80 与 8080
-#
-# 部署: kubectl apply -f backend.yaml
-# ----------------------------------------------------------------------------
----
-apiVersion: apps/v1
-kind: Deployment
-metadata:
- name: aiops-backend
- namespace: devops
- labels:
- app.kubernetes.io/name: aiops
- app.kubernetes.io/component: backend
-spec:
- replicas: 1
- strategy:
- type: RollingUpdate
- rollingUpdate:
- maxSurge: 1
- maxUnavailable: 0
- selector:
- matchLabels:
- app.kubernetes.io/name: aiops
- app.kubernetes.io/component: backend
- template:
- metadata:
- labels:
- app.kubernetes.io/name: aiops
- app.kubernetes.io/component: backend
- spec:
- containers:
- - name: aiops-backend
- image: aiops-backend:latest
- imagePullPolicy: IfNotPresent
- env:
- # ---- 数据库连接参数(对应 config.yaml 的 ${DB_*} 占位)----
- - {name: DB_HOST, value: mysql}
- - {name: DB_PORT, value: "3306"}
- - {name: DB_USER, value: aiops}
- - {name: DB_NAME, value: aiops}
- - name: DB_PASSWORD
- valueFrom:
- secretKeyRef:
- name: aiops-secret
- key: database-password
- # ---- Redis 连接参数(本环境无密码)----
- - {name: REDIS_HOST, value: redis}
- - {name: REDIS_PORT, value: "6379"}
- - {name: REDIS_PASSWORD, value: ""}
- # ---- JWT ----
- - name: JWT_SECRET
- valueFrom:
- secretKeyRef:
- name: aiops-secret
- key: jwt-secret
- - {name: TZ, value: Asia/Shanghai}
- ports:
- - {name: api, containerPort: 8080} # API + WebSocket
- - {name: web, containerPort: 80} # go:embed 前端
- resources:
- requests:
- cpu: 100m
- memory: 512Mi
- limits:
- cpu: 500m
- memory: 1Gi
- livenessProbe:
- httpGet:
- path: /health
- port: api
- initialDelaySeconds: 20
- periodSeconds: 30
- timeoutSeconds: 3
- failureThreshold: 3
- readinessProbe:
- httpGet:
- path: /health
- port: api
- initialDelaySeconds: 10
- periodSeconds: 10
- timeoutSeconds: 3
- failureThreshold: 3
- volumeMounts:
- - name: config
- mountPath: /app/configs
- readOnly: true
- volumes:
- - name: config
- configMap:
- name: aiops-config
----
-# Service — 对内暴露 80 (前端) 与 8080 (API)
-apiVersion: v1
-kind: Service
-metadata:
- name: aiops-backend
- namespace: devops
- labels:
- app.kubernetes.io/name: aiops
- app.kubernetes.io/component: backend
-spec:
- type: ClusterIP
- selector:
- app.kubernetes.io/name: aiops
- app.kubernetes.io/component: backend
- ports:
- - {name: api, port: 8080, targetPort: 8080}
- - {name: web, port: 80, targetPort: 80}
diff --git a/deploy/k8s/configmap.yaml b/deploy/k8s/configmap.yaml
index 6ab3520..2c20764 100644
--- a/deploy/k8s/configmap.yaml
+++ b/deploy/k8s/configmap.yaml
@@ -1,78 +1,32 @@
-# configmap.yaml — 后端配置 (ConfigMap) + 敏感信息 (Secret)
-# ----------------------------------------------------------------------------
-# ConfigMap aiops-config:
-# - 内含 config.yaml,DB/Redis 连接信息以环境变量占位(${VAR})。
-# - 后端 Deployment 将其挂载到 /app/configs/config.yaml(viper 搜索路径 ./configs)。
-#
-# Secret aiops-secret:
-# - 存放 MySQL 密码与 JWT 密钥(明文敏感信息不放 ConfigMap)。
-# - 由 backend Deployment / mysql StatefulSet 以环境变量注入。
-#
-# 部署: kubectl apply -f configmap.yaml
-# ----------------------------------------------------------------------------
----
apiVersion: v1
kind: ConfigMap
metadata:
name: aiops-config
namespace: devops
- labels:
- app.kubernetes.io/name: aiops
- app.kubernetes.io/component: backend
data:
config.yaml: |
- # AIOps 后端配置
- # DB/Redis 连接通过环境变量注入(见 backend Deployment 的 env 段)。
- # 敏感字段 (password / jwt) 由 Secret aiops-secret 提供。
- # 注意: viper.AutomaticEnv 默认不替换嵌套键中的点(.),如需 DATABASE_HOST ->
- # database.host 生效,请在 backend 启动时设置:
- # viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
mode: release
port: "8080"
log_level: info
- # ---- 数据库 (MySQL) ----
database:
driver: mysql
- host: "${DB_HOST}" # -> mysql (k8s Service)
+ host: mysql
port: 3306
- user: "${DB_USER}" # -> aiops
- password: "${DB_PASSWORD}" # <- Secret aiops-secret (database-password)
- dbname: "${DB_NAME}" # -> aiops
+ user: aiops
+ password: aiops_pass_2026
+ dbname: aiops
charset: utf8mb4
max_conn: 100
max_idle: 10
- # ---- Redis ----
redis:
- driver: redis # redka(内嵌) / redis(外部)
- host: "${REDIS_HOST}" # -> redis (k8s Service)
+ driver: redis
+ host: redis
port: 6379
- password: "${REDIS_PASSWORD}" # 本环境 Redis 无密码,留空
+ password: ""
db: 0
- # ---- JWT ----
jwt:
- secret: "${JWT_SECRET}" # <- Secret aiops-secret (jwt-secret)
+ secret: "aiops-jwt-secret-key-2026"
expire_hour: 24
-
- # ---- WebSocket ----
- websocket:
- read_buffer_size: 1024
- write_buffer_size: 1024
----
-apiVersion: v1
-kind: Secret
-metadata:
- name: aiops-secret
- namespace: devops
- labels:
- app.kubernetes.io/name: aiops
-type: Opaque
-stringData:
- # MySQL 业务账号密码
- database-password: "aiops_pass_2026"
- # MySQL root 密码(StatefulSet 初始化用)
- mysql-root-password: "aiops_root_2026"
- # JWT 签名密钥(生产请替换为强随机值)
- jwt-secret: "aiops-jwt-secret-change-in-production-2026"
diff --git a/deploy/k8s/deployment.yaml b/deploy/k8s/deployment.yaml
new file mode 100644
index 0000000..360e95a
--- /dev/null
+++ b/deploy/k8s/deployment.yaml
@@ -0,0 +1,104 @@
+# deployment.yaml — AIOps 应用 (embed 前端嵌入后端, 单容器单端口)
+# ----------------------------------------------------------------------------
+# - 镜像 aiops:embed(前端通过 go:embed 内嵌进同一二进制)
+# - 单端口 8080 = API + WebSocket + 前端 SPA 静态资源
+# - 配置: ConfigMap aiops-config 挂载到 /app/config.yaml
+# - Service: NodePort 30080 → 8080
+#
+# 部署: kubectl apply -f deployment.yaml
+# ----------------------------------------------------------------------------
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: aiops
+ namespace: devops
+ labels:
+ app.kubernetes.io/name: aiops
+ app.kubernetes.io/component: app
+spec:
+ replicas: 1
+ strategy:
+ type: RollingUpdate
+ rollingUpdate:
+ maxSurge: 1
+ maxUnavailable: 0
+ selector:
+ matchLabels:
+ app.kubernetes.io/name: aiops
+ app.kubernetes.io/component: app
+ template:
+ metadata:
+ labels:
+ app.kubernetes.io/name: aiops
+ app.kubernetes.io/component: app
+ spec:
+ hostNetwork: true
+ dnsPolicy: ClusterFirstWithHostNet
+ containers:
+ - name: aiops
+ image: aiops-aiops:latest
+ imagePullPolicy: IfNotPresent
+ ports:
+ - name: http
+ containerPort: 8080
+ env:
+ - name: TZ
+ value: Asia/Shanghai
+ volumeMounts:
+ - name: config
+ mountPath: /app/config.yaml
+ subPath: config.yaml
+ readOnly: true
+ - name: uploads
+ mountPath: /app/uploads
+ - name: data
+ mountPath: /app/data
+ readinessProbe:
+ httpGet:
+ path: /version
+ port: 8080
+ initialDelaySeconds: 5
+ periodSeconds: 10
+ timeoutSeconds: 3
+ livenessProbe:
+ httpGet:
+ path: /version
+ port: 8080
+ initialDelaySeconds: 15
+ periodSeconds: 30
+ timeoutSeconds: 5
+ resources:
+ requests:
+ cpu: 100m
+ memory: 256Mi
+ limits:
+ cpu: "1"
+ memory: 1Gi
+ volumes:
+ - name: config
+ configMap:
+ name: aiops-config
+ - name: uploads
+ emptyDir: {}
+ - name: data
+ emptyDir: {}
+---
+apiVersion: v1
+kind: Service
+metadata:
+ name: aiops
+ namespace: devops
+ labels:
+ app.kubernetes.io/name: aiops
+spec:
+ type: NodePort
+ selector:
+ app.kubernetes.io/name: aiops
+ app.kubernetes.io/component: app
+ ports:
+ - name: http
+ port: 8080
+ targetPort: 8080
+ nodePort: 30080
+ protocol: TCP
diff --git a/deploy/k8s/ingress.yaml b/deploy/k8s/ingress.yaml
index 3a2511b..0f43a22 100644
--- a/deploy/k8s/ingress.yaml
+++ b/deploy/k8s/ingress.yaml
@@ -1,9 +1,9 @@
# ingress.yaml — Ingress 入口路由 (nginx-ingress)
# ----------------------------------------------------------------------------
-# 路由规则:
-# /api/* -> aiops-backend:8080 (后端 API)
-# /ws/* -> aiops-backend:8080 (WebSocket 长连接)
-# / -> aiops-backend:80 (go:embed 前端 SPA)
+# 路由规则 (embed 模式: 所有流量 → aiops:8080):
+# /api/* -> aiops:8080 (后端 API)
+# /ws/* -> aiops:8080 (WebSocket 长连接)
+# / -> aiops:8080 (go:embed 前端 SPA)
#
# 前提: 集群已安装 nginx Ingress Controller。
# 部署前请修改 host (aiops.local) 与 ingressClassName 为实际值。
@@ -18,42 +18,22 @@ metadata:
labels:
app.kubernetes.io/name: aiops
annotations:
- # WebSocket 长连接支持(/ws)
nginx.ingress.kubernetes.io/proxy-read-timeout: "3600"
nginx.ingress.kubernetes.io/proxy-send-timeout: "3600"
- # 文件上传大小(与后端 upload.max_size 配合)
nginx.ingress.kubernetes.io/proxy-body-size: "100m"
- # 显式转发 WebSocket 升级头
nginx.ingress.kubernetes.io/configuration-snippet: |
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
spec:
- ingressClassName: nginx # 按集群实际 IngressClass 调整
+ ingressClassName: nginx
rules:
- - host: aiops.local # TODO: 修改为实际访问域名
+ - host: aiops.local
http:
paths:
- # ---- API ----
- - path: /api
- pathType: Prefix
- backend:
- service:
- name: aiops-backend
- port:
- number: 8080
- # ---- WebSocket ----
- - path: /ws
- pathType: Prefix
- backend:
- service:
- name: aiops-backend
- port:
- number: 8080
- # ---- 前端 (go:embed, 默认兜底) ----
- path: /
pathType: Prefix
backend:
service:
- name: aiops-backend
+ name: aiops
port:
- number: 80
+ number: 8080
diff --git a/deploy/k8s/mysql.yaml b/deploy/k8s/mysql.yaml
index 6ba03d5..c91a815 100644
--- a/deploy/k8s/mysql.yaml
+++ b/deploy/k8s/mysql.yaml
@@ -1,62 +1,11 @@
-# mysql.yaml — MySQL 8.0 数据库 (StatefulSet + Service + 持久化卷)
-# ----------------------------------------------------------------------------
-# 组件:
-# - mysql-headless (Headless Service): 为 StatefulSet 提供稳定网络标识 mysql-0
-# - mysql (ClusterIP Service): 供后端以 mysql:3306 连接
-# - mysql (StatefulSet): 单实例 MySQL 8.0
-# 账号: 用户 aiops / 密码 aiops_pass_2026 / 数据库 aiops (密码来自 aiops-secret)
-# 持久化: volumeClaimTemplates 8Gi (依赖集群 StorageClass 动态供应)
-#
-# 部署: kubectl apply -f mysql.yaml
-# ----------------------------------------------------------------------------
----
-# Headless Service — StatefulSet 稳定标识
-apiVersion: v1
-kind: Service
-metadata:
- name: mysql-headless
- namespace: devops
- labels:
- app.kubernetes.io/name: mysql
- app.kubernetes.io/component: database
-spec:
- clusterIP: None
- selector:
- app.kubernetes.io/name: mysql
- ports:
- - name: mysql
- port: 3306
- targetPort: 3306
----
-# ClusterIP Service — 供应用连接
-apiVersion: v1
-kind: Service
-metadata:
- name: mysql
- namespace: devops
- labels:
- app.kubernetes.io/name: mysql
- app.kubernetes.io/component: database
-spec:
- type: ClusterIP
- selector:
- app.kubernetes.io/name: mysql
- ports:
- - name: mysql
- port: 3306
- targetPort: 3306
----
-# StatefulSet
apiVersion: apps/v1
-kind: StatefulSet
+kind: Deployment
metadata:
name: mysql
namespace: devops
labels:
app.kubernetes.io/name: mysql
- app.kubernetes.io/component: database
spec:
- serviceName: mysql-headless
replicas: 1
selector:
matchLabels:
@@ -70,67 +19,40 @@ spec:
- name: mysql
image: mysql:8.0
imagePullPolicy: IfNotPresent
+ ports:
+ - containerPort: 3306
env:
- name: MYSQL_ROOT_PASSWORD
- valueFrom:
- secretKeyRef:
- name: aiops-secret
- key: mysql-root-password
+ value: "aiops_root_2026"
- name: MYSQL_DATABASE
value: aiops
- - name: MYSQL_USER
- value: aiops
- - name: MYSQL_PASSWORD
- valueFrom:
- secretKeyRef:
- name: aiops-secret
- key: database-password
- name: TZ
value: Asia/Shanghai
args:
- # 字符集
+ - --default-authentication-plugin=mysql_native_password
- --character-set-server=utf8mb4
- --collation-server=utf8mb4_unicode_ci
- # 兼容旧客户端认证方式(8.0 默认 caching_sha2_password)
- - --default-authentication-plugin=mysql_native_password
- ports:
- - name: mysql
- containerPort: 3306
- livenessProbe:
- exec:
- command:
- - sh
- - -c
- - 'mysqladmin ping -h 127.0.0.1 -uroot -p"$MYSQL_ROOT_PASSWORD"'
- initialDelaySeconds: 30
- periodSeconds: 10
- timeoutSeconds: 5
+ volumeMounts:
+ - name: mysql-data
+ mountPath: /var/lib/mysql
readinessProbe:
exec:
- command:
- - sh
- - -c
- - 'mysqladmin ping -h 127.0.0.1 -uroot -p"$MYSQL_ROOT_PASSWORD"'
- initialDelaySeconds: 10
+ command: ["sh", "-c", "mysqladmin ping -h 127.0.0.1 -uroot -paiops_root_2026"]
+ initialDelaySeconds: 20
periodSeconds: 10
timeoutSeconds: 5
- resources:
- requests:
- cpu: 250m
- memory: 1Gi
- limits:
- cpu: "1"
- memory: 2Gi
- volumeMounts:
- - name: data
- mountPath: /var/lib/mysql
- # ---- 持久化卷 (8Gi) ----
- volumeClaimTemplates:
- - metadata:
- name: data
- spec:
- accessModes: ["ReadWriteOnce"]
- resources:
- requests:
- storage: 8Gi
- # storageClassName: standard # 按集群实际 StorageClass 调整,留空使用默认
+ volumes:
+ - name: mysql-data
+ emptyDir: {}
+---
+apiVersion: v1
+kind: Service
+metadata:
+ name: mysql
+ namespace: devops
+spec:
+ selector:
+ app.kubernetes.io/name: mysql
+ ports:
+ - port: 3306
+ targetPort: 3306
diff --git a/deploy/k8s/namespace.yaml b/deploy/k8s/namespace.yaml
index 5f32bd2..04080ba 100644
--- a/deploy/k8s/namespace.yaml
+++ b/deploy/k8s/namespace.yaml
@@ -1,12 +1,6 @@
-# namespace.yaml — 创建 devops 命名空间
-# 所有 AIOps 资源(MySQL / Redis / 后端 / Ingress)统一部署在该命名空间下。
-# 必须最先 apply,其它资源依赖该命名空间存在。
-# 部署: kubectl apply -f namespace.yaml
----
apiVersion: v1
kind: Namespace
metadata:
name: devops
labels:
app.kubernetes.io/name: aiops
- app.kubernetes.io/part-of: aiops
diff --git a/deploy/k8s/quick-deploy.yaml b/deploy/k8s/quick-deploy.yaml
new file mode 100644
index 0000000..4e53f7c
--- /dev/null
+++ b/deploy/k8s/quick-deploy.yaml
@@ -0,0 +1,27 @@
+apiVersion: v1
+kind: ConfigMap
+metadata:
+ name: aiops-config
+ namespace: devops
+data:
+ config.yaml: |
+ mode: release
+ port: "8080"
+ log_level: info
+ database:
+ driver: mysql
+ host: mysql
+ port: 3306
+ user: root
+ password: aiops_root_2026
+ dbname: aiops
+ charset: utf8mb4
+ redis:
+ driver: redis
+ host: redis
+ port: 6379
+ password: ""
+ db: 0
+ jwt:
+ secret: "aiops-jwt-secret-key-2026"
+ expire_hour: 24
diff --git a/deploy/k8s/redis.yaml b/deploy/k8s/redis.yaml
index eed2c81..f143401 100644
--- a/deploy/k8s/redis.yaml
+++ b/deploy/k8s/redis.yaml
@@ -1,61 +1,11 @@
-# redis.yaml — Redis 7 缓存 (StatefulSet + Service + 持久化卷)
-# ----------------------------------------------------------------------------
-# 组件:
-# - redis-headless (Headless Service): StatefulSet 稳定标识 redis-0
-# - redis (ClusterIP Service): 供后端以 redis:6379 连接
-# - redis (StatefulSet): 单实例 Redis 7,无密码,db 0
-# 持久化: volumeClaimTemplates 2Gi + AOF/RDB 落盘
-#
-# 部署: kubectl apply -f redis.yaml
-# ----------------------------------------------------------------------------
----
-# Headless Service — StatefulSet 稳定标识
-apiVersion: v1
-kind: Service
-metadata:
- name: redis-headless
- namespace: devops
- labels:
- app.kubernetes.io/name: redis
- app.kubernetes.io/component: cache
-spec:
- clusterIP: None
- selector:
- app.kubernetes.io/name: redis
- ports:
- - name: redis
- port: 6379
- targetPort: 6379
----
-# ClusterIP Service — 供应用连接
-apiVersion: v1
-kind: Service
-metadata:
- name: redis
- namespace: devops
- labels:
- app.kubernetes.io/name: redis
- app.kubernetes.io/component: cache
-spec:
- type: ClusterIP
- selector:
- app.kubernetes.io/name: redis
- ports:
- - name: redis
- port: 6379
- targetPort: 6379
----
-# StatefulSet
apiVersion: apps/v1
-kind: StatefulSet
+kind: Deployment
metadata:
name: redis
namespace: devops
labels:
app.kubernetes.io/name: redis
- app.kubernetes.io/component: cache
spec:
- serviceName: redis-headless
replicas: 1
selector:
matchLabels:
@@ -69,43 +19,22 @@ spec:
- name: redis
image: redis:7-alpine
imagePullPolicy: IfNotPresent
- command: ["redis-server"]
- args:
- - --appendonly yes # 开启 AOF
- - --save 60 1000 # RDB: 60s 内 1000 次变更快照
- - --databases 1 # 仅使用 db 0
- - --maxmemory 256mb
- - --maxmemory-policy allkeys-lru
ports:
- - name: redis
- containerPort: 6379
- livenessProbe:
- exec:
- command: ["redis-cli", "ping"]
- initialDelaySeconds: 15
- periodSeconds: 10
+ - containerPort: 6379
readinessProbe:
exec:
command: ["redis-cli", "ping"]
initialDelaySeconds: 5
- periodSeconds: 10
- resources:
- requests:
- cpu: 50m
- memory: 128Mi
- limits:
- cpu: 250m
- memory: 256Mi
- volumeMounts:
- - name: data
- mountPath: /data
- # ---- 持久化卷 (2Gi) ----
- volumeClaimTemplates:
- - metadata:
- name: data
- spec:
- accessModes: ["ReadWriteOnce"]
- resources:
- requests:
- storage: 2Gi
- # storageClassName: standard # 按集群实际 StorageClass 调整
+ periodSeconds: 5
+---
+apiVersion: v1
+kind: Service
+metadata:
+ name: redis
+ namespace: devops
+spec:
+ selector:
+ app.kubernetes.io/name: redis
+ ports:
+ - port: 6379
+ targetPort: 6379
diff --git a/deploy/k8s/secret.yaml b/deploy/k8s/secret.yaml
new file mode 100644
index 0000000..741aff4
--- /dev/null
+++ b/deploy/k8s/secret.yaml
@@ -0,0 +1,10 @@
+apiVersion: v1
+kind: Secret
+metadata:
+ name: aiops-secret
+ namespace: devops
+type: Opaque
+stringData:
+ mysql-root-password: "aiops_root_2026"
+ mysql-password: "aiops_pass_2026"
+ jwt-secret: "aiops-jwt-secret-key-2026"
diff --git a/frontend/index.html b/frontend/index.html
index 9a18e73..b391807 100644
--- a/frontend/index.html
+++ b/frontend/index.html
@@ -3,6 +3,9 @@
+
+
+
AIOps智能运维平台
diff --git a/frontend/public/favicon.svg b/frontend/public/favicon.svg
new file mode 100644
index 0000000..9b6334d
--- /dev/null
+++ b/frontend/public/favicon.svg
@@ -0,0 +1,21 @@
+
diff --git a/run.sh b/run.sh
index 485cea5..bd7c4e4 100755
--- a/run.sh
+++ b/run.sh
@@ -1,14 +1,24 @@
#!/bin/bash
#===========================================================================
# AIOps 智能运维平台 - 统一启动脚本
-# 支持三种运行模式:本地开发 / Docker容器部署 / 独立二进制
+# 支持三种部署方式 + 本地开发模式:
#
-# 用法:
-# ./run.sh dev 本地开发模式(前后端热重载)
-# ./run.sh docker up Docker Compose 容器部署
-# ./run.sh build 编译独立二进制 ./aiops
-# ./run.sh binary 编译并启动独立二进制
-# ./run.sh help 查看帮助
+# 方式 1 — Docker Compose:
+# ./run.sh docker up 构建+启动 (MySQL+Redis+Backend+Nginx)
+#
+# 方式 2 — Kubernetes (embed 单容器):
+# ./run.sh k8s deploy 构建镜像+全量部署 (embed 前端嵌入后端)
+# ./run.sh k8s upgrade 重新构建+滚动更新
+# ./run.sh k8s status 查看资源状态
+# ./run.sh k8s logs/exec 日志/进入容器
+#
+# 方式 3 — 独立二进制:
+# ./run.sh build 编译独立二进制 (embed 前端)
+# ./run.sh binary 编译并启动
+#
+# 本地开发:
+# ./run.sh dev 前后端热重载
+# ./run.sh help 查看完整帮助
#===========================================================================
set -e
@@ -35,6 +45,7 @@ GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
CYAN='\033[0;36m'
+MAGENTA='\033[0;35m'
NC='\033[0m'
info() { echo -e "${BLUE}[INFO]${NC} $1"; }
@@ -400,6 +411,22 @@ cmd_build() {
case "$target" in
binary|aiops)
banner "========== 编译独立二进制 =========="
+
+ # 构建前端 → 拷贝到 embed 目录(go:embed 需要 static/ 下有文件)
+ info "构建前端 → embed..."
+ cd "$SCRIPT_DIR/$FRONTEND_DIR"
+ if [ ! -d "node_modules" ]; then
+ npm install --registry=https://registry.npmmirror.com
+ fi
+ npm run build 2>&1 | tail -5
+
+ # 拷贝前端构建产物到后端 embed 目录
+ cd "$SCRIPT_DIR"
+ rm -rf "$BACKEND_DIR/cmd/static"/*
+ cp -r "$FRONTEND_DIR/dist/"* "$BACKEND_DIR/cmd/static/"
+ local fe_files=$(find "$BACKEND_DIR/cmd/static" -type f | wc -l)
+ success "前端已嵌入 ($fe_files 文件)"
+
cd "$SCRIPT_DIR/$BACKEND_DIR"
# 确保 config.yaml 存在(二进制模式默认 SQLite)
@@ -514,6 +541,257 @@ cmd_binary() {
./"$STANDALONE_BIN" "$@"
}
+# ============================================================
+# Kubernetes 部署 (embed 单容器模式)
+# ============================================================
+K8S_NAMESPACE="devops"
+K8S_MANIFEST_DIR="$SCRIPT_DIR/deploy/k8s"
+K8S_IMAGE="aiops:embed"
+K8S_NODE_PORT=30080
+
+# 检测 kubectl 和 Docker
+check_k8s_deps() {
+ if ! command -v kubectl &>/dev/null; then
+ error "kubectl 未安装,请先安装 kubectl"
+ exit 1
+ fi
+ if ! kubectl cluster-info &>/dev/null 2>&1; then
+ error "无法连接 Kubernetes 集群,请检查 kubeconfig"
+ kubectl cluster-info 2>&1 | tail -3
+ exit 1
+ fi
+}
+
+# 检测容器运行时并加载镜像到集群
+load_image_to_cluster() {
+ local image=$1
+ info "加载镜像到集群..."
+
+ # minikube
+ if command -v minikube &>/dev/null && minikube status &>/dev/null 2>&1; then
+ minikube image load "$image" 2>/dev/null && success "镜像已加载到 minikube" && return 0
+ fi
+
+ # kind
+ if command -v kind &>/dev/null && kind get clusters &>/dev/null 2>&1 | grep -q .; then
+ kind load docker-image "$image" 2>/dev/null && success "镜像已加载到 kind" && return 0
+ fi
+
+ # k3d
+ if command -v k3d &>/dev/null; then
+ k3d image import "$image" 2>/dev/null && success "镜像已加载到 k3d" && return 0
+ fi
+
+ # Docker Desktop K8s / 远程仓库 — 镜像通过 registry 共享
+ warning "未检测到 minikube/kind/k3d,假设镜像通过 registry 共享"
+ warning "如集群无法拉取镜像,请手动推送: docker push /$image"
+}
+
+# K8s 构建镜像
+cmd_k8s_build() {
+ if ! command -v docker &>/dev/null; then
+ error "Docker 未安装"
+ exit 1
+ fi
+
+ banner "========== 构建 embed 镜像 =========="
+ info "镜像: $K8S_IMAGE"
+
+ # 版本信息
+ local version=$(git describe --tags --abbrev=0 2>/dev/null || echo "dev")
+ local build_time=$(date -u +%Y-%m-%dT%H:%M:%SZ)
+
+ info "构建中(前端 + 后端 embed)..."
+ docker build -f Dockerfile.embed \
+ --build-arg VERSION="$version" \
+ --build-arg BUILD_TIME="$build_time" \
+ -t "$K8S_IMAGE" . || {
+ error "镜像构建失败"
+ exit 1
+ }
+
+ local img_size=$(docker images "$K8S_IMAGE" --format "{{.Size}}" | head -1)
+ success "镜像构建完成: $K8S_IMAGE ($img_size)"
+
+ # 加载到本地集群
+ load_image_to_cluster "$K8S_IMAGE"
+}
+
+# K8s 部署
+cmd_k8s() {
+ local action=${1:-help}
+
+ case "$action" in
+ deploy|up)
+ check_k8s_deps
+ banner "========== K8s 部署 ($K8S_NAMESPACE) =========="
+
+ # 1. 构建镜像
+ cmd_k8s_build
+
+ # 2. 部署基础设施
+ info "部署 Namespace + Secret + ConfigMap..."
+ kubectl apply -f "$K8S_MANIFEST_DIR/namespace.yaml"
+ kubectl apply -f "$K8S_MANIFEST_DIR/secret.yaml"
+ kubectl apply -f "$K8S_MANIFEST_DIR/configmap.yaml"
+
+ info "部署 MySQL + Redis..."
+ kubectl apply -f "$K8S_MANIFEST_DIR/mysql.yaml"
+ kubectl apply -f "$K8S_MANIFEST_DIR/redis.yaml"
+
+ # 3. 等待数据库就绪
+ info "等待 MySQL 就绪..."
+ kubectl -n "$K8S_NAMESPACE" rollout status statefulset/mysql --timeout=120s 2>/dev/null || {
+ warning "MySQL 启动较慢,继续部署应用(可在后台自行完成初始化)"
+ }
+ info "等待 Redis 就绪..."
+ kubectl -n "$K8S_NAMESPACE" rollout status statefulset/redis --timeout=60s 2>/dev/null || true
+
+ # 4. 部署应用
+ info "部署 AIOps 应用..."
+ kubectl apply -f "$K8S_MANIFEST_DIR/deployment.yaml"
+ kubectl -n "$K8S_NAMESPACE" rollout status deployment/aiops --timeout=180s 2>/dev/null || {
+ warning "应用正在启动,查看日志: ./run.sh k8s logs"
+ }
+
+ # 5. Ingress(可选)
+ if [ -f "$K8S_MANIFEST_DIR/ingress.yaml" ]; then
+ info "部署 Ingress(如集群无 Ingress Controller 会跳过)..."
+ kubectl apply -f "$K8S_MANIFEST_DIR/ingress.yaml" 2>/dev/null || true
+ fi
+
+ echo ""
+ success "部署完成!"
+ echo ""
+ kubectl -n "$K8S_NAMESPACE" get pods,svc
+ echo ""
+ local node_ip=$(kubectl get nodes -o jsonpath='{.items[0].status.addresses[?(@.type=="InternalIP")].address}' 2>/dev/null)
+ info "访问地址: http://${node_ip:-}:$K8S_NODE_PORT"
+ info "健康检查: http://${node_ip:-}:$K8S_NODE_PORT/health"
+ ;;
+
+ upgrade|update)
+ check_k8s_deps
+ banner "========== K8s 滚动更新 =========="
+ cmd_k8s_build
+ info "触发滚动更新..."
+ kubectl -n "$K8S_NAMESPACE" rollout restart deployment/aiops
+ kubectl -n "$K8S_NAMESPACE" rollout status deployment/aiops --timeout=180s
+ success "滚动更新完成"
+ ;;
+
+ delete|down|remove)
+ check_k8s_deps
+ banner "========== K8s 清理 ($K8S_NAMESPACE) =========="
+ warning "这将删除 $K8S_NAMESPACE 命名空间中的所有资源"
+ warning "MySQL/Redis 持久卷(PVC)需手动删除"
+ echo ""
+ read -p "确认删除?[y/N] " confirm
+ if [ "$confirm" = "y" ] || [ "$confirm" = "Y" ]; then
+ kubectl delete -f "$K8S_MANIFEST_DIR/" --ignore-not-found 2>/dev/null
+ echo ""
+ info "PVC 状态(需手动清理):"
+ kubectl -n "$K8S_NAMESPACE" get pvc 2>/dev/null || echo " 无 PVC"
+ success "清理完成"
+ else
+ info "已取消"
+ fi
+ ;;
+
+ build|image)
+ cmd_k8s_build
+ ;;
+
+ status|ps)
+ check_k8s_deps
+ banner "========== K8s 状态 ($K8S_NAMESPACE) =========="
+ echo ""
+ echo "--- Pods ---"
+ kubectl -n "$K8S_NAMESPACE" get pods -o wide 2>/dev/null
+ echo ""
+ echo "--- Services ---"
+ kubectl -n "$K8S_NAMESPACE" get svc 2>/dev/null
+ echo ""
+ echo "--- PVC ---"
+ kubectl -n "$K8S_NAMESPACE" get pvc 2>/dev/null
+ echo ""
+ echo "--- Ingress ---"
+ kubectl -n "$K8S_NAMESPACE" get ingress 2>/dev/null
+ ;;
+
+ logs|log)
+ check_k8s_deps
+ local follow=${2:--f}
+ info "应用日志 (Ctrl+C 退出):"
+ kubectl -n "$K8S_NAMESPACE" logs $follow deployment/aiops --tail=100
+ ;;
+
+ exec|shell|sh)
+ check_k8s_deps
+ info "进入容器 Shell..."
+ kubectl -n "$K8S_NAMESPACE" exec -it deployment/aiops -- bash || \
+ kubectl -n "$K8S_NAMESPACE" exec -it deployment/aiops -- sh
+ ;;
+
+ mysql|db)
+ check_k8s_deps
+ info "进入 MySQL..."
+ kubectl -n "$K8S_NAMESPACE" exec -it mysql-0 -- mysql -uaiops -paiops_pass_2026 aiops
+ ;;
+
+ redis)
+ check_k8s_deps
+ info "进入 Redis CLI..."
+ kubectl -n "$K8S_NAMESPACE" exec -it redis-0 -- redis-cli
+ ;;
+
+ scale)
+ check_k8s_deps
+ local replicas=${2:-1}
+ info "扩缩容到 $replicas 副本..."
+ kubectl -n "$K8S_NAMESPACE" scale deployment/aiops --replicas="$replicas"
+ ;;
+
+ port-forward|pf)
+ check_k8s_deps
+ local local_port=${2:-8080}
+ info "端口转发 localhost:$local_port → aiops:8080 (Ctrl+C 退出):"
+ kubectl -n "$K8S_NAMESPACE" port-forward svc/aiops "$local_port":8080
+ ;;
+
+ help|--help|-h)
+ echo "K8s 部署命令 (embed 单容器模式):"
+ echo ""
+ echo " ${GREEN}部署${NC}"
+ echo " ./run.sh k8s deploy 构建镜像 + 全量部署"
+ echo " ./run.sh k8s upgrade 重新构建镜像 + 滚动更新"
+ echo " ./run.sh k8s delete 删除所有 K8s 资源(保留 PVC)"
+ echo ""
+ echo " ${CYAN}运维${NC}"
+ echo " ./run.sh k8s status 查看资源状态"
+ echo " ./run.sh k8s logs 查看应用日志(默认 follow)"
+ echo " ./run.sh k8s exec 进入容器 Shell"
+ echo " ./run.sh k8s mysql 进入 MySQL CLI"
+ echo " ./run.sh k8s redis 进入 Redis CLI"
+ echo " ./run.sh k8s scale 3 扩缩容到 3 副本"
+ echo " ./run.sh k8s port-forward 端口转发到本机 8080"
+ echo ""
+ echo " ${YELLOW}镜像${NC}"
+ echo " ./run.sh k8s build 仅构建 embed 镜像"
+ echo ""
+ echo " 镜像: $K8S_IMAGE"
+ echo " 命名空间: $K8S_NAMESPACE"
+ echo " NodePort: $K8S_NODE_PORT"
+ ;;
+
+ *)
+ error "未知 k8s 子命令: $action"
+ cmd_k8s help
+ exit 1
+ ;;
+ esac
+}
+
# ============================================================
# 帮助
# ============================================================
@@ -536,6 +814,19 @@ cmd_help() {
echo " ./run.sh docker logs 查看容器日志"
echo " ./run.sh docker ps 查看容器状态"
echo ""
+ echo -e " ${MAGENTA}Kubernetes 部署 (embed 单容器)${NC}"
+ echo " ./run.sh k8s deploy 构建镜像 + 全量部署到 K8s"
+ echo " ./run.sh k8s upgrade 重新构建 + 滚动更新"
+ echo " ./run.sh k8s delete 删除所有 K8s 资源"
+ echo " ./run.sh k8s status 查看资源状态"
+ echo " ./run.sh k8s logs 查看应用日志"
+ echo " ./run.sh k8s exec 进入容器 Shell"
+ echo " ./run.sh k8s mysql 进入 MySQL CLI"
+ echo " ./run.sh k8s redis 进入 Redis CLI"
+ echo " ./run.sh k8s scale 3 扩缩容"
+ echo " ./run.sh k8s port-forward 端口转发到本机"
+ echo " ./run.sh k8s build 仅构建 embed 镜像"
+ echo ""
echo -e " ${YELLOW}编译 & 独立二进制${NC}"
echo " ./run.sh build 编译独立二进制 ./$BACKEND_DIR/$STANDALONE_BIN"
echo " ./run.sh binary 编译+启动独立二进制"
@@ -598,6 +889,12 @@ main() {
cmd_binary "$@"
;;
+ # Kubernetes 部署
+ k8s|kubernetes)
+ shift
+ cmd_k8s "$@"
+ ;;
+
# 帮助
help|--help|-h)
cmd_help