diff --git a/.gitignore b/.gitignore index 8819289..860e8c3 100644 --- a/.gitignore +++ b/.gitignore @@ -53,3 +53,6 @@ docker-data/ *.pid agent/aiops-agent agent/bin/ + +# embed 模式临时目录(前端构建产物,编译后清理) +backend/cmd/static/ diff --git a/Makefile b/Makefile index 6dcdcc9..4841b14 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ # AIOps 智能运维平台 - Makefile # 使用方式: make help -.PHONY: help install dev build build-ui build-api status stop restart logs lint clean \ - docker-up docker-down docker-build docker-logs +.PHONY: help install dev build build-ui build-api build-binary status stop restart logs lint clean \ + docker-up docker-down docker-build docker-logs docker-embed help: @echo "AIOps 智能运维平台 - 可用命令:" @@ -24,7 +24,10 @@ help: @echo " make docker-down - 停止并移除所有容器" @echo " make docker-build - 重新构建镜像(不启动)" @echo " make docker-logs - 查看容器日志" + @echo " make docker-embed - 单容器部署(embed 模式,前后端合一)" @echo "" + @echo " 二进制部署:" + @echo " make build-binary - 编译单二进制(前端嵌入后端)" # ==================== 本地开发 ==================== install: @@ -71,7 +74,7 @@ clean: rm -f backend/logs/*.log frontend/logs/*.log @echo "清理完成" -# ==================== Docker 部署 ==================== +# ==================== Docker 部署(双容器:前端 nginx + 后端) ==================== docker-up: docker-compose up -d --build @@ -83,3 +86,17 @@ docker-build: docker-logs: docker-compose logs -f + +# ==================== Docker 部署(单容器:embed 模式) ==================== +docker-embed: + docker-compose -f docker-compose.embed.yml up -d --build + +# ==================== 二进制部署(embed 模式) ==================== +build-binary: build-ui + @echo "构建单二进制(前端嵌入后端)..." + @mkdir -p backend/cmd/static + @cp -r frontend/dist/* backend/cmd/static/ + cd backend && CGO_ENABLED=0 go build -ldflags="-s -w" -o ../aiops ./cmd/ + @rm -rf backend/cmd/static + @echo "✓ 二进制已生成: ./aiops" + @echo " 部署: scp aiops deploy/binary/install.sh root@:/tmp/ && ssh root@ 'cd /tmp && bash install.sh install'" diff --git a/backend/Dockerfile.embed b/backend/Dockerfile.embed new file mode 100644 index 0000000..e14702a --- /dev/null +++ b/backend/Dockerfile.embed @@ -0,0 +1,91 @@ +# ============================================================ +# AIOps 单容器 Dockerfile(embed 模式) +# 前端嵌入后端二进制,一个容器搞定前后端 +# 用法: docker build -f backend/Dockerfile.embed -t aiops-embed . +# 署名 Logan.Li +# ============================================================ + +ARG VERSION=dev +ARG BUILD_TIME=unknown + +# ==================== Stage 1: Frontend Builder ==================== +FROM node:20-alpine AS frontend-builder + +WORKDIR /app +ENV NODE_OPTIONS=--max-old-space-size=4096 + +# 使用国内 npm 镜像 +RUN npm config set registry https://registry.npmmirror.com + +COPY frontend/package.json frontend/package-lock.json ./ +RUN npm ci --prefer-offline 2>/dev/null || npm install + +COPY frontend/ ./ +RUN npm run build + +# ==================== Stage 2: Backend Builder ==================== +FROM golang:1.24-alpine AS backend-builder + +WORKDIR /app + +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 GOPROXY=https://goproxy.cn,direct +ENV GOSUMDB=off + +COPY backend/go.mod backend/go.sum ./ +RUN go mod download + +COPY backend/ ./ + +# 复制前端构建产物到 static 目录(供 go:embed 嵌入) +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 + +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" -o bin/aiops-agent-linux-amd64 . && \ + CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -ldflags="-s -w" -o bin/aiops-agent-linux-arm64 . && \ + CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 go build -ldflags="-s -w" -o bin/aiops-agent-darwin-arm64 . + +# ==================== Stage 4: Runtime ==================== +FROM alpine:latest + +RUN sed -i 's|dl-cdn.alpinelinux.org|mirrors.aliyun.com|g' /etc/apk/repositories +RUN apk --no-cache add ca-certificates tzdata curl + +ENV TZ=Asia/Shanghai +WORKDIR /app + +# 单个二进制(前端已嵌入) +COPY --from=backend-builder /app/aiops . + +# Agent 二进制 + 安装脚本 +RUN mkdir -p /app/agent/bin /app/agent/deploy +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/ +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 new file mode 100644 index 0000000..84505bf --- /dev/null +++ b/backend/cmd/embed.go @@ -0,0 +1,68 @@ +package main + +import ( + "embed" + "io/fs" + "net/http" + "strings" + + "github.com/gin-gonic/gin" +) + +//go:embed all:static +var frontendFS embed.FS + +// setupFrontend 配置前端静态文件服务(go:embed 模式) // Logan.Li +// 前端构建产物放在 backend/cmd/static/ 目录,编译时嵌入二进制 +func setupFrontend(r *gin.Engine) { + // 获取 static 子目录的 fs.FS + staticFS, err := fs.Sub(frontendFS, "static") + if err != nil { + // static 目录不存在(开发模式或未构建前端),跳过 + return + } + httpFS := http.FS(staticFS) + + // 静态资源文件(js/css/图片等)直接返回 + r.Use(func(c *gin.Context) { + // API 和 WebSocket 路径不走静态文件 + path := c.Request.URL.Path + if strings.HasPrefix(path, "/api/") || + strings.HasPrefix(path, "/ws/") || + strings.HasPrefix(path, "/agent/") || + strings.HasPrefix(path, "/health") || + strings.HasPrefix(path, "/version") { + c.Next() + return + } + c.Next() + }) + + // NoRoute: SPA fallback — 非 API 请求尝试静态文件,找不到则返回 index.html + r.NoRoute(func(c *gin.Context) { + path := c.Request.URL.Path + + // API/WS 路径返回 JSON 404 + if strings.HasPrefix(path, "/api/") || strings.HasPrefix(path, "/ws/") || strings.HasPrefix(path, "/agent/") { + c.JSON(http.StatusNotFound, gin.H{"code": 404, "message": "接口不存在"}) + return + } + + // 尝试打开静态文件 + cleanPath := strings.TrimPrefix(path, "/") + if cleanPath == "" { + cleanPath = "index.html" + } + + f, err := staticFS.Open(cleanPath) + if err == nil { + f.Close() + c.Header("Cache-Control", "public, max-age=31536000") + c.FileFromFS(path, httpFS) + return + } + + // SPA fallback → index.html(React Router history 模式) + c.FileFromFS("/index.html", httpFS) + }) +} diff --git a/backend/cmd/main.go b/backend/cmd/main.go index 941d7fe..14dcdc4 100644 --- a/backend/cmd/main.go +++ b/backend/cmd/main.go @@ -162,6 +162,9 @@ func main() { // 注册路由 routes.Register(r, db, log, cfg, cache) + // 前端静态文件服务(go:embed 模式,二进制部署用) // Logan.Li + setupFrontend(r) + // 创建HTTP服务器 srv := &http.Server{ Addr: ":" + cfg.Port, diff --git a/deploy/README.md b/deploy/README.md new file mode 100644 index 0000000..663d45d --- /dev/null +++ b/deploy/README.md @@ -0,0 +1,107 @@ +# AIOps 三种部署方式 + +## 部署方式对比 + +| 方式 | 适用场景 | 优点 | 缺点 | +|------|---------|------|------| +| **Docker Compose(双容器)** | 生产环境 | 前后端分离,nginx 高性能 | 需要 4 个容器 | +| **Docker Compose(embed 单容器)** | 快速部署 | 1 个容器搞定,无需 nginx | 静态文件性能稍弱 | +| **二进制部署** | 无 Docker 环境 | 一个文件 + systemd | 需要自行管理 MySQL/Redis | +| **Kubernetes** | K8s 集群 | 标准化编排,弹性伸缩 | 需要 K8s 集群 | + +--- + +## 方式一:Docker Compose(双容器,当前默认) + +```bash +# 开发机(75) +cd /srv/aiops +make docker-up # 构建并启动 + +# 生产机(150) +cd /srv/aiops && git pull +make docker-up +``` + +架构:nginx(:80) → backend(:8080) + MySQL + Redis + +--- + +## 方式二:Docker Compose embed 单容器 + +前端通过 Go `//go:embed` 嵌入后端二进制,不需要 nginx 容器。 + +```bash +cd /srv/aiops +docker-compose -f docker-compose.embed.yml up -d --build +# 或 +make docker-embed +``` + +架构:aiops-app(:80→:8080) + MySQL + Redis(3 个容器) + +--- + +## 方式三:二进制部署(无 Docker) + +### 在开发机构建 + +```bash +cd /srv/aiops +make build-binary +# 生成 ./aiops 单二进制(~30MB,前端已嵌入) +``` + +### 在目标服务器部署 + +```bash +# 传两个文件到目标服务器 +scp aiops deploy/binary/install.sh root@:/tmp/ + +# 安装(自动创建 systemd 服务 + 默认配置) +ssh root@ +cd /tmp && bash install.sh install + +# 修改数据库配置 +vim /opt/aiops/config.yaml + +# 启动 +systemctl start aiops +``` + +### 管理 + +```bash +systemctl start aiops # 启动 +systemctl stop aiops # 停止 +systemctl restart aiops # 重启 +systemctl status aiops # 状态 +journalctl -u aiops -f # 日志 +``` + +架构:aiops 二进制 + systemd(端口 8080 同时服务 API 和前端) + +> 前提:目标服务器需已安装 MySQL 8.0 + Redis 7 + +--- + +## 方式四:Kubernetes 部署 + +```bash +cd /srv/aiops +kubectl apply -f deploy/k8s/ +``` + +详见 [deploy/k8s/README.md](../k8s/README.md) + +--- + +## 快速选择 + +| 环境 | 推荐方式 | +|------|---------| +| 开发测试(75) | 方式一 docker-compose | +| 生产环境(150) | 方式一 docker-compose | +| 客户现场快速演示 | 方式二 embed 单容器 | +| 内网无 Docker 服务器 | 方式三 二进制部署 | +| K8s 集群 | 方式四 Kubernetes | diff --git a/deploy/binary/install.sh b/deploy/binary/install.sh new file mode 100755 index 0000000..aa9eb08 --- /dev/null +++ b/deploy/binary/install.sh @@ -0,0 +1,165 @@ +#!/bin/bash +# ============================================================ +# AIOps 二进制部署脚本(embed 模式) +# 一个二进制 = 前端 + 后端,无需 Docker/Nginx +# 署名 Logan.Li +# ============================================================ +set -e + +# 颜色 +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' + +info() { echo -e "${GREEN}[INFO]${NC} $1"; } +warn() { echo -e "${YELLOW}[WARN]${NC} $1"; } +error() { echo -e "${RED}[ERROR]${NC} $1"; } + +# 默认路径 +INSTALL_DIR="/opt/aiops" +CONFIG_FILE="$INSTALL_DIR/config.yaml" +SERVICE_FILE="/etc/systemd/system/aiops.service" +BINARY_NAME="aiops" +PID_FILE="/var/run/aiops.pid" + +# ============================================================ +# 命令处理 +# ============================================================ +case "$1" in + install) + info "AIOps 二进制安装" + echo "" + + if [ "$(id -u)" -ne 0 ]; then + error "请使用 root 或 sudo 执行" + exit 1 + fi + + # 检查二进制 + if [ ! -f "./${BINARY_NAME}" ]; then + error "当前目录找不到 ./${BINARY_NAME} 二进制文件" + echo " 请先在开发机执行: make build-binary" + exit 1 + fi + + # 创建安装目录 + mkdir -p "$INSTALL_DIR/logs" "$INSTALL_DIR/uploads" + + # 复制二进制 + cp "./${BINARY_NAME}" "$INSTALL_DIR/" + chmod +x "$INSTALL_DIR/${BINARY_NAME}" + info "二进制已安装到 $INSTALL_DIR/${BINARY_NAME}" + + # 生成默认配置(如果不存在) + if [ ! -f "$CONFIG_FILE" ]; then + cat > "$CONFIG_FILE" << 'EOF' +mode: release +port: "8080" +log_level: info + +database: + driver: mysql + host: 127.0.0.1 + port: 3306 + user: aiops + password: aiops_pass_2026 + dbname: aiops + charset: utf8mb4 + max_conn: 100 + max_idle: 10 + +redis: + driver: redis + host: 127.0.0.1 + port: 6379 + password: "" + db: 0 + +jwt: + secret: ezops-aiops-jwt-secret-2026 + expire_hour: 24 +EOF + info "默认配置已生成: $CONFIG_FILE" + warn "请根据实际环境修改数据库/Redis 配置" + fi + + # 创建 systemd 服务 + cat > "$SERVICE_FILE" << EOF +[Unit] +Description=AIOps Platform (Embed Mode) +After=network-online.target mysql.service redis.service +Wants=network-online.target + +[Service] +Type=simple +WorkingDirectory=${INSTALL_DIR} +ExecStart=${INSTALL_DIR}/${BINARY_NAME} +Restart=always +RestartSec=5 +LimitNOFILE=65536 +Environment=GIN_MODE=release + +[Install] +WantedBy=multi-user.target +EOF + + systemctl daemon-reload + systemctl enable aiops + info "systemd 服务已创建并启用开机自启" + + echo "" + info "安装完成!" + echo "" + echo " 启动: systemctl start aiops" + echo " 状态: systemctl status aiops" + echo " 日志: journalctl -u aiops -f" + echo " 配置: vim $CONFIG_FILE" + echo " 访问: http://:8080" + echo "" + ;; + + start) + systemctl start aiops + info "AIOps 已启动" + ;; + + stop) + systemctl stop aiops + info "AIOps 已停止" + ;; + + restart) + systemctl restart aiops + info "AIOps 已重启" + ;; + + status) + systemctl status aiops --no-pager + ;; + + uninstall) + warn "即将卸载 AIOps(保留数据/配置)" + systemctl stop aiops 2>/dev/null + systemctl disable aiops + rm -f "$SERVICE_FILE" + rm -f "$INSTALL_DIR/${BINARY_NAME}" + systemctl daemon-reload + info "已卸载(配置/日志/数据保留在 $INSTALL_DIR)" + ;; + + *) + echo "AIOps 二进制部署工具" + echo "" + echo "用法: $0 {install|start|stop|restart|status|uninstall}" + echo "" + echo " install - 安装二进制 + 生成配置 + 创建 systemd 服务" + echo " start - 启动服务" + echo " stop - 停止服务" + echo " restart - 重启服务" + echo " status - 查看服务状态" + echo " uninstall - 卸载二进制(保留数据)" + echo "" + exit 1 + ;; +esac diff --git a/deploy/k8s/README.md b/deploy/k8s/README.md new file mode 100644 index 0000000..4ca06af --- /dev/null +++ b/deploy/k8s/README.md @@ -0,0 +1,158 @@ +# AIOps Kubernetes 部署 + +AIOps 平台在 Kubernetes 上的部署清单(Go/Gin 后端 + go:embed 前端 + MySQL 8.0 + Redis 7)。 + +所有资源部署在 **`devops`** 命名空间。 + +## 目录结构 + +| 文件 | 说明 | +|------|------| +| `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 | + +## 架构与端口 + +``` + ┌──────────────── 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 +``` + +- **8080**:API + WebSocket +- **80**:前端(go:embed 嵌入后端二进制,由后端进程提供静态资源) +- 配置文件 `config.yaml` 中 DB/Redis 连接均以环境变量 `${VAR}` 占位,由 Deployment 的 `env` 注入;密码走 Secret。 + +## 前置条件 + +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`)。 + +## 部署步骤 + +按顺序 apply(资源间存在依赖:Secret → 数据库 → 后端 → Ingress): + +```bash +cd deploy/k8s + +# 1. 命名空间(必须最先) +kubectl apply -f namespace.yaml + +# 2. 配置 + 密码(ConfigMap / Secret) +kubectl apply -f configmap.yaml + +# 3. MySQL(启动后自动建库 aiops 与用户 aiops) +kubectl apply -f mysql.yaml + +# 4. Redis +kubectl apply -f redis.yaml + +# 5. 等待数据库就绪 +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 + +# 7. Ingress(部署前修改 host 与 ingressClassName) +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 +# 查看所有资源 +kubectl -n devops get all,configmap,secret,ingress,pvc + +# 查看后端日志 +kubectl -n devops logs -f deployment/aiops-backend + +# 进入 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 exec -it deployment/aiops-backend -- sh + +# 查看 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`(生产请替换为强随机值) | +| Redis | 无密码 | db `0` | + +> ⚠️ 生产环境请勿使用仓库内默认密码。修改 `configmap.yaml` 中 Secret 的值后重新 apply, +> 或使用 `kubectl create secret` 覆盖。 + +## 资源限额 + +| 组件 | CPU 请求/上限 | 内存请求/上限 | +|------|---------------|---------------| +| backend | 100m / 500m | 512Mi / 1Gi | +| mysql | 250m / 1 | 1Gi / 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 +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 new file mode 100644 index 0000000..17832ba --- /dev/null +++ b/deploy/k8s/backend.yaml @@ -0,0 +1,116 @@ +# 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 new file mode 100644 index 0000000..6ab3520 --- /dev/null +++ b/deploy/k8s/configmap.yaml @@ -0,0 +1,78 @@ +# 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) + port: 3306 + user: "${DB_USER}" # -> aiops + password: "${DB_PASSWORD}" # <- Secret aiops-secret (database-password) + dbname: "${DB_NAME}" # -> aiops + charset: utf8mb4 + max_conn: 100 + max_idle: 10 + + # ---- Redis ---- + redis: + driver: redis # redka(内嵌) / redis(外部) + host: "${REDIS_HOST}" # -> redis (k8s Service) + port: 6379 + password: "${REDIS_PASSWORD}" # 本环境 Redis 无密码,留空 + db: 0 + + # ---- JWT ---- + jwt: + secret: "${JWT_SECRET}" # <- Secret aiops-secret (jwt-secret) + 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/ingress.yaml b/deploy/k8s/ingress.yaml new file mode 100644 index 0000000..3a2511b --- /dev/null +++ b/deploy/k8s/ingress.yaml @@ -0,0 +1,59 @@ +# ingress.yaml — Ingress 入口路由 (nginx-ingress) +# ---------------------------------------------------------------------------- +# 路由规则: +# /api/* -> aiops-backend:8080 (后端 API) +# /ws/* -> aiops-backend:8080 (WebSocket 长连接) +# / -> aiops-backend:80 (go:embed 前端 SPA) +# +# 前提: 集群已安装 nginx Ingress Controller。 +# 部署前请修改 host (aiops.local) 与 ingressClassName 为实际值。 +# 部署: kubectl apply -f ingress.yaml +# ---------------------------------------------------------------------------- +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: aiops-ingress + namespace: devops + 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 调整 + rules: + - host: aiops.local # TODO: 修改为实际访问域名 + 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 + port: + number: 80 diff --git a/deploy/k8s/mysql.yaml b/deploy/k8s/mysql.yaml new file mode 100644 index 0000000..6ba03d5 --- /dev/null +++ b/deploy/k8s/mysql.yaml @@ -0,0 +1,136 @@ +# 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 +metadata: + name: mysql + namespace: devops + labels: + app.kubernetes.io/name: mysql + app.kubernetes.io/component: database +spec: + serviceName: mysql-headless + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: mysql + template: + metadata: + labels: + app.kubernetes.io/name: mysql + spec: + containers: + - name: mysql + image: mysql:8.0 + imagePullPolicy: IfNotPresent + env: + - name: MYSQL_ROOT_PASSWORD + valueFrom: + secretKeyRef: + name: aiops-secret + key: mysql-root-password + - 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: + # 字符集 + - --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 + readinessProbe: + exec: + command: + - sh + - -c + - 'mysqladmin ping -h 127.0.0.1 -uroot -p"$MYSQL_ROOT_PASSWORD"' + initialDelaySeconds: 10 + 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 调整,留空使用默认 diff --git a/deploy/k8s/namespace.yaml b/deploy/k8s/namespace.yaml new file mode 100644 index 0000000..5f32bd2 --- /dev/null +++ b/deploy/k8s/namespace.yaml @@ -0,0 +1,12 @@ +# 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/redis.yaml b/deploy/k8s/redis.yaml new file mode 100644 index 0000000..eed2c81 --- /dev/null +++ b/deploy/k8s/redis.yaml @@ -0,0 +1,111 @@ +# 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 +metadata: + name: redis + namespace: devops + labels: + app.kubernetes.io/name: redis + app.kubernetes.io/component: cache +spec: + serviceName: redis-headless + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: redis + template: + metadata: + labels: + app.kubernetes.io/name: redis + spec: + containers: + - 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 + 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 调整 diff --git a/docker-compose.embed.yml b/docker-compose.embed.yml new file mode 100644 index 0000000..3f3feea --- /dev/null +++ b/docker-compose.embed.yml @@ -0,0 +1,91 @@ +# ============================================================ +# AIOps 单容器 docker-compose(embed 模式) +# 前端嵌入后端二进制,不需要独立 nginx 容器 +# 用法: docker-compose -f docker-compose.embed.yml up -d --build +# 署名 Logan.Li +# ============================================================ +version: '3.8' + +services: + mysql: + image: mysql:8.0 + container_name: aiops-mysql + restart: unless-stopped + environment: + MYSQL_ROOT_PASSWORD: rootpassword + MYSQL_DATABASE: aiops + MYSQL_USER: aiops + MYSQL_PASSWORD: aiops_pass_2026 + TZ: Asia/Shanghai + ports: + - "3306:3306" + volumes: + - mysql_data:/var/lib/mysql + command: > + --default-authentication-plugin=mysql_native_password + --character-set-server=utf8mb4 + --collation-server=utf8mb4_unicode_ci + healthcheck: + test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-uroot", "-p$$MYSQL_ROOT_PASSWORD"] + interval: 10s + timeout: 5s + retries: 10 + start_period: 30s + networks: + - aiops-network + + redis: + image: redis:7-alpine + container_name: aiops-redis + restart: unless-stopped + ports: + - "6379:6379" + volumes: + - redis_data:/data + command: redis-server --appendonly yes + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 10s + timeout: 5s + retries: 5 + networks: + - aiops-network + + # 单容器:后端 API (:8080) + 嵌入前端 (:8080) + # nginx 不再需要,gin 直接服务静态文件 + aiops: + build: + context: . + dockerfile: backend/Dockerfile.embed + container_name: aiops-app + restart: unless-stopped + ports: + - "80:8080" # 前端 + API 都走 8080 + - "8080:8080" # 也可以直接访问 8080 + volumes: + - ./backend/config.yaml:/app/config.yaml + - app_logs:/app/logs + - app_uploads:/app/uploads + depends_on: + mysql: + condition: service_healthy + redis: + condition: service_healthy + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8080/health"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s + networks: + - aiops-network + +volumes: + mysql_data: + redis_data: + app_logs: + app_uploads: + +networks: + aiops-network: + driver: bridge