diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..935c539 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,79 @@ +# ============================================================ +# opsxsql - 单镜像多阶段构建(参照 opsx-loki) +# 前端 dist + Go 后端 + gh-ost 合并为一个 alpine 运行时镜像 +# 非 root 用户运行;/health 健康检查;数据落 /app/data 卷 +# ============================================================ + +# --------------------------------------------------------------- +# Stage 1: 前端构建(Node) +# --------------------------------------------------------------- +FROM node:20-alpine AS frontend-builder +WORKDIR /app/frontend +COPY frontend/package*.json ./ +RUN npm ci --ignore-scripts && npm cache clean --force +COPY frontend/ ./ +RUN npm run build + +# --------------------------------------------------------------- +# Stage 2: 后端构建(Go,CGO 因 mattn/go-sqlite3 必开) +# --------------------------------------------------------------- +FROM golang:1.26-alpine AS backend-builder +RUN apk add --no-cache git gcc musl-dev +ENV GOTOOLCHAIN=local +WORKDIR /app/backend +COPY backend/go.* ./ +RUN go mod download +COPY backend/ ./ +RUN CGO_ENABLED=1 GOOS=linux go build -ldflags="-s -w" -o /out/opsxsql ./cmd/server/ + +# --------------------------------------------------------------- +# Stage 3: gh-ost 在线变更工具(静态二进制,按架构取包) +# --------------------------------------------------------------- +FROM alpine:3.20 AS ghost +ARG TARGETARCH +RUN apk add --no-cache curl && \ + case "${TARGETARCH}" in \ + arm64) GH_ARCH="arm64" ;; \ + *) GH_ARCH="amd64" ;; \ + esac && \ + curl -fsSL -o /out/gh-ost \ + "https://github.com/github/gh-ost/releases/download/v1.1.9/gh-ost-linux-${GH_ARCH}" && \ + chmod +x /out/gh-ost + +# --------------------------------------------------------------- +# Stage 4: 运行时镜像 +# --------------------------------------------------------------- +FROM alpine:3.20 +RUN apk --no-cache add ca-certificates tzdata wget sqlite && \ + rm -rf /tmp/* /var/cache/apk/* + +# 非 root 用户 +RUN addgroup -g 1000 opsxsql && \ + adduser -u 1000 -G opsxsql -s /bin/sh -D opsxsql + +WORKDIR /app +COPY --from=backend-builder --chown=opsxsql:opsxsql /out/opsxsql ./opsxsql +COPY --from=ghost --chown=opsxsql:opsxsql /out/gh-ost /usr/local/bin/gh-ost +COPY --from=frontend-builder --chown=opsxsql:opsxsql /app/frontend/dist ./dist + +RUN mkdir -p /app/data && chown -R opsxsql:opsxsql /app/data && \ + chmod +x ./opsxsql + +ENV DIST_DIR=/app/dist \ + DB_PATH=/app/data/sqlite3.db \ + PORT=9000 \ + DEBUG=false \ + GH_OST_PATH=/usr/local/bin/gh-ost \ + HOME=/home/opsxsql + +COPY --chown=opsxsql:opsxsql docker-entrypoint.sh /usr/local/bin/ +RUN chmod +x /usr/local/bin/docker-entrypoint.sh + +USER opsxsql +EXPOSE 9000 +VOLUME ["/app/data"] + +HEALTHCHECK --interval=30s --timeout=5s --retries=3 --start-period=15s \ + CMD wget --no-verbose --tries=1 --spider http://localhost:9000/health || exit 1 + +ENTRYPOINT ["/usr/local/bin/docker-entrypoint.sh"] diff --git a/README.md b/README.md index be609b4..bc6c700 100644 --- a/README.md +++ b/README.md @@ -88,7 +88,9 @@ sql-platform/ │ └── lib/ # 工具函数 ├── tests/ # 集成测试 ├── run.sh # 启动/停止脚本 -└── docker-compose.yml # Docker 部署 +├── docker-compose.yml # Docker Compose 部署(单镜像) +├── Dockerfile # 单镜像多阶段构建(前端+后端+gh-ost) +└── kubernetes/ # K8s 部署(install.sh 一键 + 清单) ``` ## 快速开始 @@ -99,18 +101,45 @@ sql-platform/ - Node.js 18+ - npm 或 yarn -### 启动 +### 方式一:Docker Compose(推荐,单镜像前后端同源) ```bash -# 克隆项目 git clone https://gitee.com/opsx-tools/opsxsql.git cd opsxsql -# Docker 一键启动 -docker-compose up -d --build +# 生成密钥(必填;SECRET_KEY 部署后不可更换,否则已加密的实例/LDAP 密码无法解密) +export SECRET_KEY=$(openssl rand -hex 32) +# 可选;不设则生产模式自动生成随机 JWT 密钥并持久化到数据卷 +export JWT_SECRET=$(openssl rand -hex 32) + +docker compose up -d --build ``` -后端运行在 **http://localhost:9000**,前端运行在 **http://localhost/** +访问 **http://localhost:9000**(单镜像,Gin 同时托管前端静态资源与 API)。 +自定义端口:`OPSXSQL_PORT=18000 docker compose up -d`。 +安全基线:非 root 运行、no-new-privileges、cap_drop ALL、/health 健康检查、资源限额、日志轮转。 + +### 方式二:Kubernetes 一键部署 + +```bash +# 默认部署(镜像 opsxsql:latest) +bash kubernetes/install.sh + +# 指定镜像 + 入口 +IMAGE=registry.example.com/opsxsql:v1.0.0 bash kubernetes/install.sh --nodeport +IMAGE=registry.example.com/opsxsql:v1.1.0 bash kubernetes/install.sh --ingress opsxsql.example.com +``` + +install.sh 幂等:自动建命名空间/生成并复用密钥/应用 PVC+Deployment+Service,等待就绪后打印访问方式。 +注意:SQLite 单写者,Deployment 固定 **单副本 + Recreate**(脚本已内置);详见 `kubernetes/README.md`。 + +### 方式三:本地开发 + +```bash +./run.sh start # 后端 9000 + 前端 5173 +``` + +后端 **http://localhost:9000**,前端 dev server **http://localhost:5173**(vite 代理 /api)。 ### 默认账号 diff --git a/backend/cmd/server/main.go b/backend/cmd/server/main.go index 9cfdef6..547a2f8 100644 --- a/backend/cmd/server/main.go +++ b/backend/cmd/server/main.go @@ -3,6 +3,10 @@ package main import ( "fmt" "log" + "net/http" + "os" + "path/filepath" + "strings" "sql-platform/config" "sql-platform/internal/middleware" @@ -82,6 +86,32 @@ func main() { // Routes router.Setup(r) + // 单镜像部署:直接托管前端产物(DIST_DIR 为空则跳过,开发模式由 vite 独立提供) + if distDir := os.Getenv("DIST_DIR"); distDir != "" { + indexPath := filepath.Join(distDir, "index.html") + if _, err := os.Stat(indexPath); err == nil { + r.NoRoute(func(c *gin.Context) { + p := c.Request.URL.Path + if strings.HasPrefix(p, "/api") { + c.JSON(http.StatusNotFound, gin.H{"detail": "Not found"}) + return + } + // 目录穿越防护后按文件直出(assets/db-logos/favicon),其余回落 SPA index + full := filepath.Join(distDir, filepath.Clean("/"+p)) + if strings.HasPrefix(full, filepath.Clean(distDir)) { + if info, err := os.Stat(full); err == nil && !info.IsDir() { + c.File(full) + return + } + } + c.File(indexPath) + }) + log.Printf("serving frontend from %s", distDir) + } else { + log.Printf("DIST_DIR=%s 无 index.html,跳过静态托管", distDir) + } + } + // Start server addr := fmt.Sprintf(":%s", config.C.Port) log.Printf("SQL Platform Go backend starting on %s", addr) diff --git a/docker-compose.yml b/docker-compose.yml index 84eb856..333e48e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,30 +1,69 @@ +# ============================================================ +# opsxsql - Docker Compose 部署(单镜像,参照 opsx-loki) +# 安全基线: 非 root / no-new-privileges / 健康检查 / 资源限额 / 日志轮转 +# 使用: +# export SECRET_KEY=$(openssl rand -hex 32) +# export JWT_SECRET=$(openssl rand -hex 32) # 可选;不设则生产模式自动生成并持久化 +# docker compose up -d --build +# 访问: http://localhost:9000 (前后端同源,无需单独的前端容器) +# ============================================================ + services: - backend: - build: ./backend - container_name: sql-audit-backend - restart: unless-stopped - ports: - - "9000:9000" - volumes: - - backend-data:/app/data - environment: - - DATABASE_URL=sqlite+aiosqlite:////app/data/sqlite3.db - - DB_PATH=/app/data/sqlite3.db - - SECRET_KEY=${SECRET_KEY:-change-me-in-production} - - ADMIN_USERNAME=${ADMIN_USERNAME:-admin} - - ADMIN_PASSWORD=${ADMIN_PASSWORD:-changeme} - - ENV=production - - DEBUG=false - - CGO_ENABLED=1 + opsxsql: + build: + context: . + dockerfile: Dockerfile + args: + TARGETARCH: ${TARGETARCH:-amd64} + image: opsxsql:latest + container_name: opsxsql + hostname: opsxsql + + security_opt: + - no-new-privileges:true + cap_drop: + - ALL - frontend: - build: ./frontend - container_name: sql-audit-frontend - restart: unless-stopped ports: - - "80:80" - depends_on: - - backend + - "${OPSXSQL_PORT:-9000}:9000" + + volumes: + - opsxsql-data:/app/data + + environment: + - PORT=9000 + - DB_PATH=/app/data/sqlite3.db + - DEBUG=${DEBUG:-false} + # AES 密钥(实例/LDAP 密码加密): 必须外部提供,且部署后不可更换 + - "SECRET_KEY=${SECRET_KEY:?SECRET_KEY is required. Generate: openssl rand -hex 32}" + # JWT 密钥: 可选;生产模式未提供时自动生成随机密钥并持久化到数据卷 + - JWT_SECRET=${JWT_SECRET:-} + + healthcheck: + test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:9000/health"] + interval: 30s + timeout: 5s + retries: 3 + start_period: 15s + + deploy: + resources: + limits: + cpus: '2.0' + memory: 1G + reservations: + cpus: '0.25' + memory: 128M + + logging: + driver: "json-file" + options: + max-size: "50m" + max-file: "5" + + restart: unless-stopped + stop_grace_period: 15s volumes: - backend-data: + opsxsql-data: + driver: local diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh new file mode 100755 index 0000000..d819b92 --- /dev/null +++ b/docker-entrypoint.sh @@ -0,0 +1,20 @@ +#!/bin/sh +# opsxsql 容器入口(参照 opsx-loki): +# - 生产(DEBUG=false)缺 SECRET_KEY 时告警(AES 加密数据与密钥绑定,换密钥已存密文不可解) +# - JWT_SECRET 未显式提供时,后端会在生产模式自动生成随机密钥并持久化到 /app/data +set -e + +if [ "${DEBUG:-false}" != "true" ]; then + if [ -z "${SECRET_KEY:-}" ]; then + echo "[entrypoint] 警告: 未设置 SECRET_KEY,使用内置默认密钥(AES 加密实例/LDAP 密码)。" >&2 + echo "[entrypoint] 生产环境建议: export SECRET_KEY=\$(openssl rand -hex 32) 后重建容器;" >&2 + echo "[entrypoint] 注意: 更换 SECRET_KEY 会导致已加密的实例/LDAP 密码无法解密,请首次部署即固定。" >&2 + fi + if [ -z "${JWT_SECRET:-}" ]; then + echo "[entrypoint] 提示: 未设置 JWT_SECRET,生产模式将自动生成随机密钥并持久化到数据卷。" >&2 + fi +fi + +mkdir -p /app/data + +exec ./opsxsql diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 172aaaa..712635c 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -337,8 +337,9 @@ pages/ ## 14. 运维部署 +### 14.1 开发环境 + ```bash -# 开发环境 bash run.sh start # 启动 Go 后端 (:9000) + 前端 (:5173) bash run.sh restart # 重启 bash run.sh stop # 停止 @@ -346,10 +347,36 @@ bash run.sh status # 状态检查 # 后端编译 cd backend && go build -o sql-platform ./cmd/server/ +``` -# 健康检查 +### 14.2 Docker Compose(单镜像,生产推荐) + +多阶段构建:前端 dist + Go 二进制 + gh-ost 合并为一个 alpine 镜像,Gin 同时托管 +静态资源与 API(前后端同源,无 CORS 面);非 root、no-new-privileges、 +cap_drop ALL、/health 健康检查、资源限额与日志轮转(参照 opsx-loki 基线)。 + +```bash +export SECRET_KEY=$(openssl rand -hex 32) # AES 密钥,部署后不可更换 +docker compose up -d --build # http://localhost:9000 +``` + +数据持久化在命名卷 `opsxsql-data`(SQLite + 持久化 JWT 密钥)。 + +### 14.3 Kubernetes + +```bash +bash kubernetes/install.sh --nodeport # 一键:命名空间/密钥/PVC/Deployment/Service +``` + +约束:SQLite 单写者 → Deployment 固定 **replicas=1 + Recreate**(防滚动更新双写)。 +备份:`sqlite3 /app/data/sqlite3.db '.backup ...'` 或 CSI VolumeSnapshot。 +详见 `kubernetes/README.md`。 + +### 14.4 健康检查 + +```bash curl http://localhost:9000/health -# → {"status":"ok","version":"0.3.1","backend":"go"} +# → {"status":"ok","version":"0.4.0","backend":"go"} ``` ## 15. 关键设计决策 diff --git a/kubernetes/README.md b/kubernetes/README.md new file mode 100644 index 0000000..5d46708 --- /dev/null +++ b/kubernetes/README.md @@ -0,0 +1,92 @@ +# opsxsql 部署到 Kubernetes + +## 文件 + +| 文件 | 说明 | +|---|---| +| `install.sh` | **一键部署**(推荐):建命名空间/生成密钥/应用全部清单/等待就绪 | +| `namespace.yaml` | `opsxsql` 命名空间 | +| `secret.example.yaml` | 密钥模板(建议用 `kubectl create secret`,install.sh 已自动处理) | +| `pvc.yaml` | 数据卷(默认 10Gi,集群默认 StorageClass) | +| `deployment.yaml` | 平台本体,**单副本 + Recreate** | +| `service.yaml` | ClusterIP `opsxsql:80` | +| `service-nodeport.yaml` | 可选 NodePort 入口(`:30900`) | +| `ingress.yaml` | 可选,按集群 ingressClass 调整 | + +## 一键部署 + +```bash +# 默认(镜像 opsxsql:latest,需先 build&push 或 node 可用) +bash kubernetes/install.sh + +# 指定镜像 + NodePort 入口 +IMAGE=registry.example.com/opsxsql:v1.0.0 bash kubernetes/install.sh --nodeport + +# 指定镜像 + Ingress 域名 +IMAGE=registry.example.com/opsxsql:v1.0.0 bash kubernetes/install.sh --ingress opsxsql.example.com + +# 自定义命名空间 / 卷大小 +NS=sql-audit PVC_SIZE=20Gi bash kubernetes/install.sh +``` + +## 分步部署(等价于 install.sh) + +```bash +# 1) 密钥(只生成一次;SECRET_KEY 丢失会导致已加密的实例/LDAP 密码无法解密) +kubectl -n opsxsql create secret generic opsxsql-secrets \ + --from-literal=SECRET_KEY="$(openssl rand -hex 32)" \ + --from-literal=JWT_SECRET="$(openssl rand -hex 32)" + +# 2) 清单(先把 deployment.yaml 里的 image 改成实际仓库地址) +kubectl apply -f kubernetes/namespace.yaml +kubectl apply -f kubernetes/pvc.yaml +kubectl apply -f kubernetes/deployment.yaml +kubectl apply -f kubernetes/service.yaml + +# 3) 可选入口 +kubectl apply -f kubernetes/service-nodeport.yaml # 或 ingress.yaml +``` + +## 持久化(重要) + +- `/app/data` 挂 PVC,保存 SQLite 数据库与持久化 JWT 密钥。 +- **必须 `replicas: 1` + `strategy: Recreate`**:SQLite 是单写者,滚动更新时两个 Pod 同时挂同一 RWO PVC 会互相覆盖(install.sh 已内置该约束)。 +- 备份(冷备一条命令): + +```bash +kubectl -n opsxsql exec deploy/opsxsql -- sqlite3 /app/data/sqlite3.db '.backup /app/data/backup-$(date +%F).db' +kubectl -n opsxsql cp deploy/opsxsql:/app/data/backup-$(date +%F).db ./backup.db +``` + +- 云盘快照走 CSI VolumeSnapshot(AWS EBS / 腾讯 CBS / 华为 EVS / 阿里 ESSD 同一套 API): + +```yaml +apiVersion: snapshot.storage.k8s.io/v1 +kind: VolumeSnapshot +metadata: + name: opsxsql-data-20261026 + namespace: opsxsql +spec: + volumeSnapshotClassName: <集群的 snapshotClass> + source: + persistentVolumeClaimName: opsxsql-data +``` + +## 多云兼容 + +| 项 | 做法 | +|---|---| +| 存储类 | `pvc.yaml` 不写 `storageClassName` 用默认 SC;无默认 SC 时填云厂名(AWS `gp3` / 腾讯 `cbs` / 华为 `csi-disk` / 阿里 `alicloud-disk-essd`) | +| 镜像仓库 | `deployment.yaml` 的 `image` 填目标云可拉的仓库(Harbor / ACR / TCR / SWR),或本仓库根 `Dockerfile` 自行构建 | +| gh-ost | 镜像内置(在线大表 DDL 用);不用该功能可忽略,无需外置依赖 | + +## 升级 / 卸载 + +```bash +# 升级:换镜像 tag 重新 install(Recreate 策略自动重建,数据卷不动) +IMAGE=registry.example.com/opsxsql:v1.1.0 bash kubernetes/install.sh --nodeport + +# 卸载(数据卷保留;需要连数据一起删加 --include-data) +kubectl -n opsxsql delete deploy,svc,ingress opsxsql opsxsql-nodeport 2>/dev/null +kubectl delete ns opsxsql # 会连 PVC 一起删,谨慎 +``` diff --git a/kubernetes/deployment.yaml b/kubernetes/deployment.yaml new file mode 100644 index 0000000..a756bb0 --- /dev/null +++ b/kubernetes/deployment.yaml @@ -0,0 +1,81 @@ +# 平台本体 —— 单副本 + Recreate(SQLite 单写者,滚动更新会双写互踩,参照 opsx-loki 同款约束) +# 镜像: 把 image 改为你的仓库地址,或用本仓库根目录 Dockerfile 自行构建推送 +apiVersion: apps/v1 +kind: Deployment +metadata: + name: opsxsql + namespace: opsxsql + labels: + app.kubernetes.io/name: opsxsql +spec: + replicas: 1 + strategy: + type: Recreate + selector: + matchLabels: + app.kubernetes.io/name: opsxsql + template: + metadata: + labels: + app.kubernetes.io/name: opsxsql + spec: + securityContext: + runAsNonRoot: true + runAsUser: 1000 + runAsGroup: 1000 + fsGroup: 1000 + containers: + - name: opsxsql + image: opsxsql:latest # TODO: 替换为实际镜像仓库地址 + imagePullPolicy: IfNotPresent + ports: + - name: http + containerPort: 9000 + env: + - name: PORT + value: "9000" + - name: DB_PATH + value: /app/data/sqlite3.db + - name: DEBUG + value: "false" + - name: SECRET_KEY + valueFrom: + secretKeyRef: + name: opsxsql-secrets + key: SECRET_KEY + - name: JWT_SECRET + valueFrom: + secretKeyRef: + name: opsxsql-secrets + key: JWT_SECRET + volumeMounts: + - name: data + mountPath: /app/data + readinessProbe: + httpGet: + path: /health + port: http + initialDelaySeconds: 5 + periodSeconds: 10 + livenessProbe: + httpGet: + path: /health + port: http + initialDelaySeconds: 20 + periodSeconds: 30 + resources: + requests: + cpu: 100m + memory: 128Mi + limits: + cpu: "2" + memory: 1Gi + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: false + capabilities: + drop: ["ALL"] + volumes: + - name: data + persistentVolumeClaim: + claimName: opsxsql-data diff --git a/kubernetes/ingress.yaml b/kubernetes/ingress.yaml new file mode 100644 index 0000000..b085a03 --- /dev/null +++ b/kubernetes/ingress.yaml @@ -0,0 +1,21 @@ +# 可选 Ingress: 按集群 ingressClass 调整(nginx 示例),不需要就跳过 +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: opsxsql + namespace: opsxsql + annotations: + nginx.ingress.kubernetes.io/proxy-body-size: 32m # CSV 导入/大数据量导出 +spec: + ingressClassName: nginx + rules: + - host: opsxsql.example.com # TODO: 替换实际域名 + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: opsxsql + port: + number: 80 diff --git a/kubernetes/install.sh b/kubernetes/install.sh new file mode 100755 index 0000000..3cf0ae9 --- /dev/null +++ b/kubernetes/install.sh @@ -0,0 +1,165 @@ +#!/bin/bash +# ============================================================ +# opsxsql 一键部署到 Kubernetes(参照 opsx-loki / deploy-loki 模式) +# 用法: +# bash kubernetes/install.sh # 默认部署(命名空间 opsxsql + ClusterIP) +# bash kubernetes/install.sh --nodeport # 附带 NodePort 30900 入口 +# bash kubernetes/install.sh --ingress opsxsql.example.com # 附带 Ingress +# IMAGE=registry.example.com/opsxsql:v1 bash kubernetes/install.sh # 指定镜像 +# 幂等: 重复执行安全;密钥已存在则复用(不会轮换,已加密数据不受影响) +# ============================================================ +set -euo pipefail + +NS="${NS:-opsxsql}" +IMAGE="${IMAGE:-opsxsql:latest}" +SECRET_NAME="opsxsql-secrets" +WITH_NODEPORT=0 +INGRESS_HOST="" + +# ---- 参数解析 ---- +while [[ $# -gt 0 ]]; do + case "$1" in + --nodeport) WITH_NODEPORT=1; shift ;; + --ingress) INGRESS_HOST="${2:?--ingress 需要域名参数}"; shift 2 ;; + -h|--help) + sed -n '2,10p' "$0"; exit 0 ;; + *) echo "未知参数: $1(看 --help)"; exit 1 ;; + esac +done + +# ---- 前置检查 ---- +for cmd in kubectl openssl; do + command -v "$cmd" >/dev/null 2>&1 || { echo "缺少依赖: $cmd"; exit 1; } +done +kubectl cluster-info >/dev/null 2>&1 || { echo "kubectl 无法连接集群"; exit 1; } + +echo "==> 部署 opsxsql 到命名空间 ${NS}(镜像 ${IMAGE})" + +# ---- 1) 命名空间 ---- +kubectl apply -f - </dev/null 2>&1; then + echo "==> 密钥 ${SECRET_NAME} 已存在,复用(不轮换)" +else + echo "==> 生成密钥(仅此一次;丢失会导致已加密的实例/LDAP 密码无法解密,请自行备份)" + kubectl -n "$NS" create secret generic "$SECRET_NAME" \ + --from-literal=SECRET_KEY="$(openssl rand -hex 32)" \ + --from-literal=JWT_SECRET="$(openssl rand -hex 32)" +fi + +# ---- 3) 数据卷 + 工作负载 + Service ---- +kubectl -n "$NS" apply -f - < NodePort 已启用" +fi + +if [[ -n "$INGRESS_HOST" ]]; then + kubectl -n "$NS" apply -f - < Ingress 已创建(${INGRESS_HOST})" +fi + +# ---- 5) 等待就绪 ---- +echo "==> 等待 Pod 就绪..." +if kubectl -n "$NS" rollout status deployment/opsxsql --timeout=180s; then + echo "" + echo "==========================================" + echo " opsxsql 部署完成!(命名空间 ${NS})" + echo "==========================================" + echo " 默认账号: admin / admin123(首次登录后请修改)" + echo " 访问方式:" + [[ "$WITH_NODEPORT" == "1" ]] && echo " NodePort: http://<任意节点IP>:30900" + [[ -n "$INGRESS_HOST" ]] && echo " Ingress: http://${INGRESS_HOST}" + echo " 集群内: http://opsxsql.${NS}.svc" + echo " 本地试连: kubectl -n ${NS} port-forward svc/opsxsql 9000:80" + echo " 数据备份: kubectl -n ${NS} exec deploy/opsxsql -- sqlite3 /app/data/sqlite3.db '.backup /app/data/backup.db'" + echo "==========================================" +else + echo "部署超时,排查: kubectl -n ${NS} describe pod -l app.kubernetes.io/name=opsxsql" + exit 1 +fi diff --git a/kubernetes/namespace.yaml b/kubernetes/namespace.yaml new file mode 100644 index 0000000..accd679 --- /dev/null +++ b/kubernetes/namespace.yaml @@ -0,0 +1,6 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: opsxsql + labels: + app.kubernetes.io/name: opsxsql diff --git a/kubernetes/pvc.yaml b/kubernetes/pvc.yaml new file mode 100644 index 0000000..4c36369 --- /dev/null +++ b/kubernetes/pvc.yaml @@ -0,0 +1,15 @@ +# 数据卷(SQLite + 密钥持久化)。不写 storageClassName = 用集群默认 SC; +# 无默认 SC 时填对应云名(AWS gp3 / 腾讯 cbs / 华为 csi-disk / 阿里 alicloud-disk-essd) +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: opsxsql-data + namespace: opsxsql + labels: + app.kubernetes.io/name: opsxsql +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 10Gi diff --git a/kubernetes/secret.example.yaml b/kubernetes/secret.example.yaml new file mode 100644 index 0000000..d5cecc9 --- /dev/null +++ b/kubernetes/secret.example.yaml @@ -0,0 +1,13 @@ +# 密钥模板 —— 建议用 kubectl create secret 命令创建(install.sh 已自动处理),不要把真实值提交到仓库 +# kubectl -n opsxsql create secret generic opsxsql-secrets \ +# --from-literal=SECRET_KEY="$(openssl rand -hex 32)" \ +# --from-literal=JWT_SECRET="$(openssl rand -hex 32)" +apiVersion: v1 +kind: Secret +metadata: + name: opsxsql-secrets + namespace: opsxsql +type: Opaque +stringData: + SECRET_KEY: "change-me-openssl-rand-hex-32" + JWT_SECRET: "change-me-openssl-rand-hex-32" diff --git a/kubernetes/service-nodeport.yaml b/kubernetes/service-nodeport.yaml new file mode 100644 index 0000000..2fb425f --- /dev/null +++ b/kubernetes/service-nodeport.yaml @@ -0,0 +1,17 @@ +# 无 Ingress 时的快速入口(可选): http://<任意节点IP>:30900 +apiVersion: v1 +kind: Service +metadata: + name: opsxsql-nodeport + namespace: opsxsql + labels: + app.kubernetes.io/name: opsxsql +spec: + type: NodePort + selector: + app.kubernetes.io/name: opsxsql + ports: + - name: http + port: 80 + targetPort: http + nodePort: 30900 diff --git a/kubernetes/service.yaml b/kubernetes/service.yaml new file mode 100644 index 0000000..f2ab33f --- /dev/null +++ b/kubernetes/service.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Service +metadata: + name: opsxsql + namespace: opsxsql + labels: + app.kubernetes.io/name: opsxsql +spec: + type: ClusterIP + selector: + app.kubernetes.io/name: opsxsql + ports: + - name: http + port: 80 + targetPort: http