feat(deploy): single-image docker-compose and kubernetes install.sh

- multi-stage root Dockerfile (opsx-loki pattern): frontend dist +
  go binary + gh-ost in one alpine image, non-root user, HEALTHCHECK,
  entrypoint warning for missing SECRET_KEY; gin now serves the SPA
  from DIST_DIR (asset/db-logos/favicon passthrough, /api returns JSON
  404, SPA fallback to index.html) - one origin, no CORS surface
- docker-compose.yml rewritten: single service, named data volume,
  no-new-privileges + cap_drop ALL, /health healthcheck, resource
  limits, log rotation; SECRET_KEY required (openssl rand -hex 32),
  JWT_SECRET optional (auto-generated and persisted in prod)
- kubernetes/: install.sh one-command deploy (idempotent secret
  reuse, --nodeport/--ingress flags, rollout wait, backup tips) plus
  namespace/secret/pvc/deployment(single replica + Recreate, probes,
  resource limits, non-root)/service/nodeport/ingress manifests and
  README (sqlite single-writer constraints, snapshot backup)
- docs: README quick-start now has 3 deployment paths; ARCHITECTURE
  ch.14 expanded

single-image runtime verified locally (SPA/assets/api/login coexist
on :9002); full image build left to server (docker hub unreachable
from this machine)
This commit is contained in:
Logan.Li committed 2026-10-26 20:33:26 +08:00
1 parent 13fb3aceb3
commit 0b16488029
15 files changed
+683 -34

No files matched your search

+79
View File
@@ -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"]
+35 -6
View File
@@ -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)。
### 默认账号
+30
View File
@@ -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)
+64 -25
View File
@@ -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
+20
View File
@@ -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
+30 -3
View File
@@ -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. 关键设计决策
+92
View File
@@ -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 一起删,谨慎
```
+81
View File
@@ -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
+21
View File
@@ -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
+165
View File
@@ -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 - <<EOF
apiVersion: v1
kind: Namespace
metadata:
name: ${NS}
EOF
# ---- 2) 密钥(已存在则复用) ----
if kubectl -n "$NS" get secret "$SECRET_NAME" >/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 - <<EOF
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: opsxsql-data
spec:
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: ${PVC_SIZE:-10Gi}
EOF
kubectl -n "$NS" apply -f - <<EOF
apiVersion: apps/v1
kind: Deployment
metadata:
name: opsxsql
labels: { app.kubernetes.io/name: opsxsql }
spec:
replicas: 1 # SQLite 单写者,禁止扩副本
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: ${IMAGE}
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: ${SECRET_NAME}, key: SECRET_KEY } }
- name: JWT_SECRET
valueFrom: { secretKeyRef: { name: ${SECRET_NAME}, 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, capabilities: { drop: ["ALL"] } }
volumes:
- name: data
persistentVolumeClaim: { claimName: opsxsql-data }
EOF
kubectl -n "$NS" apply -f - <<EOF
apiVersion: v1
kind: Service
metadata:
name: opsxsql
spec:
type: ClusterIP
selector: { app.kubernetes.io/name: opsxsql }
ports: [{ name: http, port: 80, targetPort: http }]
EOF
# ---- 4) 可选入口 ----
if [[ "$WITH_NODEPORT" == "1" ]]; then
kubectl -n "$NS" apply -f "$(dirname "$0")/service-nodeport.yaml"
echo "==> NodePort 已启用"
fi
if [[ -n "$INGRESS_HOST" ]]; then
kubectl -n "$NS" apply -f - <<EOF
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: opsxsql
annotations: { nginx.ingress.kubernetes.io/proxy-body-size: 32m }
spec:
ingressClassName: nginx
rules:
- host: ${INGRESS_HOST}
http:
paths:
- path: /
pathType: Prefix
backend: { service: { name: opsxsql, port: { number: 80 } } }
EOF
echo "==> 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
+6
View File
@@ -0,0 +1,6 @@
apiVersion: v1
kind: Namespace
metadata:
name: opsxsql
labels:
app.kubernetes.io/name: opsxsql
+15
View File
@@ -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
+13
View File
@@ -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"
+17
View File
@@ -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
+15
View File
@@ -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