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