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
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
|
||||
Reference in new issue
Block a user