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:
1 parent
13fb3aceb3
commit
0b16488029
15 files changed
+683
-34
No files matched your search
@@ -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 一起删,谨慎
|
||||
```
|
||||
@@ -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
|
||||
@@ -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
|
||||
Executable
+165
@@ -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
|
||||
@@ -0,0 +1,6 @@
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: opsxsql
|
||||
labels:
|
||||
app.kubernetes.io/name: opsxsql
|
||||
@@ -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
|
||||
@@ -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"
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in new issue
Block a user