Files
opsxsql/kubernetes/install.sh
T
Logan.Li 738498fea3 feat(k8s): align install.sh with latest opsx-loki version
- cloud auto-detection (ACK/TKE/EKS/CCE/GKE/AKS/K3s/self-hosted) via
  node providerID, used for deployment banner and failure diagnostics
- storage class resolution chain: explicit STORAGE_CLASS > default
  annotation > sole SC; self-hosted clusters without any SC now get a
  zero-dependency static local PV (hostPath DirectoryOrCreate +
  nodeAffinity pin + WaitForFirstConsumer + Retain), LOCAL_STORAGE
  auto/off/force tri-state
- PVC rendered with resolved storageClassName + PVC_SIZE; image
  rendered into deployment via IMAGE env
- rollout failure diagnostics per waiting-reason: ImagePullBackOff
  lists per-cloud registry suggestions, CreateContainerConfigError
  points to secret, PVC Pending suggests per-cloud SC
- colored info/ok/warn/die helpers, /dev/urandom key fallback,
  completion banner with pod/node/ip, port-forward and backup hints
- kubernetes/examples/: alibaba ALB + tencent qcloud (CLB health
  check annotations, NodePort backend) ingress templates, secret
  example moved here (opsx-loki layout)
- README updated with env-var table and local-storage notes
2026-10-26 22:09:33 +08:00

343 lines
15 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env bash
# ============================================================================
# opsxsql 一键部署(参照 opsx-loki 最新 install.sh)
#
# 自动适配:阿里云 ACK / 腾讯云 TKE / AWS EKS / 华为云 CCE / GCP GKE / Azure AKS / K3s / 自建
# 幂等:重复执行不覆盖密钥、不重建数据卷
#
# 用法:
# bash kubernetes/install.sh # 自动探测,默认镜像 opsxsql:latest
# IMAGE=registry.example.com/opsxsql:v1 bash kubernetes/install.sh
# WITH_NODEPORT=1 bash kubernetes/install.sh # 同时开 NodePort(:30900)
# bash kubernetes/install.sh --nodeport # 等价于上一行
# bash kubernetes/install.sh --ingress opsxsql.example.com # 通用 nginx Ingress
# STORAGE_CLASS=local-path NS=opsxsql PVC_SIZE=20Gi bash kubernetes/install.sh
# ============================================================================
set -euo pipefail
NS="${NS:-opsxsql}"
IMAGE="${IMAGE:-opsxsql:latest}"
SECRET_NAME="opsxsql-secrets"
WITH_NODEPORT="${WITH_NODEPORT:-0}"
INGRESS_HOST=""
STORAGE_CLASS="${STORAGE_CLASS:-}"
PVC_SIZE="${PVC_SIZE:-10Gi}"
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
info() { printf '\033[1;36m==>\033[0m %s\n' "$*"; }
ok() { printf '\033[1;32m✅\33[0m %s\n' "$*"; }
warn() { printf '\033[1;33m⚠️ \33[0m %s\n' "$*"; }
die() { printf '\033[1;31m❌\33[0m %s\n' "$*" >&2; exit 1; }
# ---- 参数解析(兼容 flags 与环境变量) ----
while [[ $# -gt 0 ]]; do
case "$1" in
--nodeport) WITH_NODEPORT=1; shift ;;
--ingress) INGRESS_HOST="${2:?--ingress 需要域名参数}"; shift 2 ;;
-h|--help) sed -n '2,17p' "$0"; exit 0 ;;
*) die "未知参数: $1(看 --help)" ;;
esac
done
# ============================================================================
# 0) 前置检查
# ============================================================================
command -v kubectl >/dev/null 2>&1 || die "未找到 kubectl,请先安装"
kubectl version --request-timeout=5s >/dev/null 2>&1 || die "kubectl 无法连接集群,请检查 kubeconfig"
# ============================================================================
# 0.1) 云平台识别(信息展示 + 失败诊断用)
# ============================================================================
detect_cloud() {
local pid=""
pid=$(kubectl get nodes -o jsonpath='{.items[0].spec.providerID}' 2>/dev/null || true)
case "${pid}" in
qcloud://*) echo "腾讯云 TKE" ;;
aws://*) echo "AWS EKS" ;;
aliyun://*) echo "阿里云 ACK" ;;
huaweicloud*|huawei*) echo "华为云 CCE" ;;
gce://*) echo "GCP GKE" ;;
azure://*) echo "Azure AKS" ;;
k3s://*) echo "K3s(自建)" ;;
*) echo "自建 / 其他 K8s" ;;
esac
}
# ============================================================================
# 0.2) StorageClass 探测
# 优先级:显式 STORAGE_CLASS > default 注解 > 唯一 SC > 失败
# 结果写入全局 RESOLVED_SC / SC_ERROR(不要用 $() 调用,子 shell 赋值不回传)
# ============================================================================
RESOLVED_SC=""
SC_ERROR=""
resolve_storage_class() {
local sc="" all="" count=0
RESOLVED_SC=""; SC_ERROR=""
if [ -n "${STORAGE_CLASS}" ]; then
if ! kubectl get sc "${STORAGE_CLASS}" >/dev/null 2>&1; then
SC_ERROR="指定的 StorageClass '${STORAGE_CLASS}' 不存在。现有:$(kubectl get sc -o jsonpath='{range .items[*]}{.metadata.name}{" "}{end}' 2>/dev/null)"
return 1
fi
RESOLVED_SC="${STORAGE_CLASS}"; return 0
fi
sc=$(kubectl get sc -o jsonpath='{range .items[?(@.metadata.annotations.storageclass\.kubernetes\.io/is-default-class=="true")]}{.metadata.name}{"\n"}{end}' 2>/dev/null | head -1 || true)
if [ -n "${sc}" ]; then RESOLVED_SC="${sc}"; return 0; fi
all=$(kubectl get sc -o jsonpath='{range .items[*]}{.metadata.name}{"\n"}{end}' 2>/dev/null || true)
count=$(printf '%s' "${all}" | grep -c . || true)
if [ "${count}" = "1" ]; then RESOLVED_SC="${all}"; return 0; fi
return 1
}
# ============================================================================
# 0.3) 自建集群兜底:无任何 StorageClass 时创建「静态 local PV」(零外部依赖)
# hostPath + DirectoryOrCreate + nodeAffinity 锁节点 + WaitForFirstConsumer + Retain
# ============================================================================
LOCAL_SC_NAME="opsxsql-local"
LOCAL_PV_NAME="opsxsql-data-pv"
LOCAL_PV_SIZE="${PVC_SIZE}"
LOCAL_PV_PATH="${PV_PATH:-/var/lib/opsxsql-data}"
pick_local_node() {
if [ -n "${NODE_NAME:-}" ]; then echo "${NODE_NAME}"; return 0; fi
kubectl get nodes --no-headers \
-o custom-columns=NAME:.metadata.name,READY:.status.conditions[-1].status 2>/dev/null \
| awk '$2=="True"{print $1; exit}'
}
create_local_storage() {
local node
node="$(pick_local_node)"
[ -n "${node}" ] || { warn "找不到 Ready 节点,无法创建本地存储"; return 1; }
info "创建节点本地存储:节点=${node} 路径=${LOCAL_PV_PATH} 容量=${LOCAL_PV_SIZE}"
cat <<EOF | kubectl apply -f - >/dev/null
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: ${LOCAL_SC_NAME}
provisioner: kubernetes.io/no-provisioner
volumeBindingMode: WaitForFirstConsumer
reclaimPolicy: Retain
EOF
cat <<EOF | kubectl apply -f - >/dev/null
apiVersion: v1
kind: PersistentVolume
metadata:
name: ${LOCAL_PV_NAME}
labels:
app: opsxsql
spec:
capacity:
storage: ${LOCAL_PV_SIZE}
accessModes:
- ReadWriteOnce
persistentVolumeReclaimPolicy: Retain
storageClassName: ${LOCAL_SC_NAME}
hostPath:
path: ${LOCAL_PV_PATH}
type: DirectoryOrCreate
nodeAffinity:
required:
nodeSelectorTerms:
- matchExpressions:
- key: kubernetes.io/hostname
operator: In
values:
- ${node}
EOF
ok "已创建 StorageClass ${LOCAL_SC_NAME} + PV ${LOCAL_PV_NAME}(节点 ${node})"
warn "注意:数据固定在该节点 ${LOCAL_PV_PATH},Pod 不会漂移到其他节点"
RESOLVED_SC="${LOCAL_SC_NAME}"
return 0
}
CLOUD="$(detect_cloud)"
info "opsxsql 部署开始(namespace: ${NS},镜像 ${IMAGE})"
info "集群: $(kubectl config current-context 2>/dev/null || echo unknown)"
info "平台: ${CLOUD}"
# LOCAL_STORAGE 三态:auto(默认,无 SC 时自动建) / off(只提示) / force(强制建)
LOCAL_STORAGE="${LOCAL_STORAGE:-auto}"
if [ "${LOCAL_STORAGE}" = "force" ]; then
warn "LOCAL_STORAGE=force:强制使用节点本地存储"
create_local_storage || die "创建节点本地存储失败"
elif ! resolve_storage_class; then
if [ -n "${SC_ERROR}" ]; then
die "${SC_ERROR}"
fi
echo ""
if [ "${LOCAL_STORAGE}" = "auto" ]; then
warn "集群没有可用 StorageClass(自建集群常见)→ 自动启用节点本地存储"
create_local_storage || die "创建节点本地存储失败"
else
warn "未找到可用的 StorageClass,且 LOCAL_STORAGE=off"
echo ""
echo " 现有 StorageClass:"
kubectl get sc 2>/dev/null | sed 's/^/ /' || echo " (无)"
echo ""
echo " 解决办法(选一个):"
echo " 1) 手动指定(推荐,若已有可用 SC):"
echo " STORAGE_CLASS=<sc名称> bash kubernetes/install.sh"
echo " 2) 标记某个 SC 为默认:"
echo " kubectl patch sc <sc名称> -p '{\"metadata\":{\"annotations\":{\"storageclass.kubernetes.io/is-default-class\":\"true\"}}}'"
echo " 3) 自建集群安装 local-path-provisioner:"
echo " kubectl apply -f https://raw.githubusercontent.com/rancher/local-path-provisioner/master/deploy/local-path-storage.yaml"
echo " STORAGE_CLASS=local-path bash kubernetes/install.sh"
echo ""
die "StorageClass 未就绪,部署中止(避免 PVC 永久 Pending)"
fi
else
ok "StorageClass: ${RESOLVED_SC}"
fi
# ============================================================================
# 1) 命名空间
# ============================================================================
cat <<EOF | kubectl apply -f - >/dev/null
apiVersion: v1
kind: Namespace
metadata:
name: ${NS}
EOF
# ============================================================================
# 2) Secret(幂等:已存在则跳过,绝不覆盖 —— SECRET_KEY 换了已加密数据不可解)
# ============================================================================
rand_hex() {
if command -v openssl >/dev/null 2>&1; then
openssl rand -hex 32
elif [ -c /dev/urandom ]; then
head -c 32 /dev/urandom | od -An -tx1 | tr -d ' \n'
else
die "无法生成随机密钥(缺 openssl 且无 /dev/urandom)"
fi
}
if kubectl -n "${NS}" get secret "${SECRET_NAME}" >/dev/null 2>&1; then
info "Secret ${SECRET_NAME} 已存在,跳过(不覆盖现有密钥)"
else
info "生成随机密钥 SECRET_KEY / JWT_SECRET(丢失则已加密的实例/LDAP 密码无法解密,请自行备份)"
kubectl -n "${NS}" create secret generic "${SECRET_NAME}" \
--from-literal=SECRET_KEY="$(rand_hex)" \
--from-literal=JWT_SECRET="$(rand_hex)" >/dev/null
ok "Secret 已创建"
fi
# ============================================================================
# 3) 数据卷(渲染注入探测到的 StorageClass 与容量)
# ============================================================================
RENDER_DIR="$(mktemp -d)"
trap 'rm -rf "${RENDER_DIR}"' EXIT
awk -v sc="${RESOLVED_SC}" -v size="${PVC_SIZE}" '
/^spec:/ && !done { print; print " storageClassName: " sc; done=1; next }
/storage:/ && !sized { $0=" storage: " size; sized=1 }
{ print }
' "${DIR}/pvc.yaml" > "${RENDER_DIR}/pvc.yaml"
kubectl apply -f "${RENDER_DIR}/pvc.yaml"
# ============================================================================
# 4) 工作负载 + Service(镜像渲染进 deployment)
# ============================================================================
sed "s|image: opsxsql:latest.*|image: ${IMAGE}|" "${DIR}/deployment.yaml" > "${RENDER_DIR}/deployment.yaml"
kubectl apply -f "${RENDER_DIR}/deployment.yaml"
kubectl apply -f "${DIR}/service.yaml"
if [ "${WITH_NODEPORT}" = "1" ]; then
kubectl apply -f "${DIR}/service-nodeport.yaml"
ok "NodePort Service 已创建(:30900)"
fi
if [ -n "${INGRESS_HOST}" ]; then
sed "s|host: opsxsql.example.com.*|host: ${INGRESS_HOST}|" "${DIR}/ingress.yaml" > "${RENDER_DIR}/ingress.yaml"
kubectl apply -f "${RENDER_DIR}/ingress.yaml"
ok "Ingress 已创建(${INGRESS_HOST},nginx class;其他云参照 ${DIR}/examples/)"
fi
# ============================================================================
# 5) 等待就绪(失败时按云诊断)
# ============================================================================
info "等待 Deployment 就绪(最多 180 秒)..."
if ! kubectl -n "${NS}" rollout status deployment/opsxsql --timeout=180s; then
echo ""
warn "部署未在 180 秒内就绪,开始诊断:"
echo ""
kubectl -n "${NS}" get po -l app.kubernetes.io/name=opsxsql -o wide 2>/dev/null | sed 's/^/ /'
REASON=$(kubectl -n "${NS}" get po -l app.kubernetes.io/name=opsxsql -o jsonpath='{.items[0].status.containerStatuses[0].state.waiting.reason}' 2>/dev/null || true)
echo ""
case "${REASON}" in
ImagePullBackOff|ErrImagePull)
warn "镜像拉取失败(${REASON})"
echo " → 当前镜像: ${IMAGE}"
echo " → 该节点拉不到该仓库。改为目标云可拉的仓库后重跑:"
echo " 腾讯云 TCR : ccr.ccs.tencentyun.com/<ns>/opsxsql:latest"
echo " 阿里云 ACR : registry.cn-hangzhou.aliyuncs.com/<ns>/opsxsql:latest"
echo " 华为云 SWR : swr.cn-north-4.myhuaweicloud.com/<ns>/opsxsql:latest"
echo " 自建 Harbor: harbor.your.domain/library/opsxsql:latest"
echo " 示例: IMAGE=registry.cn-hangzhou.aliyuncs.com/<ns>/opsxsql:latest bash kubernetes/install.sh"
;;
CreateContainerConfigError)
warn "容器配置缺失(通常是 Secret 不存在)"
echo " → 检查: kubectl -n ${NS} get secret ${SECRET_NAME}"
;;
"")
PVC_PHASE=$(kubectl -n "${NS}" get pvc opsxsql-data -o jsonpath='{.status.phase}' 2>/dev/null || echo unknown)
if [ "${PVC_PHASE}" != "Bound" ]; then
warn "PVC 未绑定(phase=${PVC_PHASE})"
echo " → 检查 StorageClass: kubectl -n ${NS} describe pvc opsxsql-data"
echo " → ${CLOUD} 建议的 SC:"
case "${CLOUD}" in
腾讯云*) echo " cbs" ;;
AWS*) echo " gp3 / gp2" ;;
阿里云*) echo " alicloud-disk-essd" ;;
华为云*) echo " csi-disk" ;;
*) echo " local-path(需装 local-path-provisioner)" ;;
esac
fi
;;
esac
echo ""
kubectl -n "${NS}" describe po -l app.kubernetes.io/name=opsxsql 2>/dev/null | tail -15 | sed 's/^/ /'
die "部署未完成,请按上面提示处理"
fi
ok "Deployment 已就绪"
# ============================================================================
# 6) 输出访问信息
# ============================================================================
POD=$(kubectl -n "${NS}" get po -l app.kubernetes.io/name=opsxsql -o jsonpath='{.items[0].metadata.name}' 2>/dev/null || echo "-")
POD_IP=$(kubectl -n "${NS}" get po -l app.kubernetes.io/name=opsxsql -o jsonpath='{.items[0].status.podIP}' 2>/dev/null || echo "-")
NODE=$(kubectl -n "${NS}" get po -l app.kubernetes.io/name=opsxsql -o jsonpath='{.items[0].spec.nodeName}' 2>/dev/null || echo "-")
echo ""
echo "════════════════════════════════════════════════════════"
ok "opsxsql 部署完成"
echo "════════════════════════════════════════════════════════"
echo " 平台 : ${CLOUD}"
echo " Namespace : ${NS}"
echo " 镜像 : ${IMAGE}"
echo " 存储类 : ${RESOLVED_SC}"
echo " Pod : ${POD} (node=${NODE} ip=${POD_IP})"
echo " Service : opsxsql.${NS}.svc.cluster.local:80"
[ "${WITH_NODEPORT}" = "1" ] && echo " NodePort : <任意节点IP>:30900"
[ -n "${INGRESS_HOST}" ] && echo " Ingress : http://${INGRESS_HOST}"
echo ""
echo " 初始账号 : admin / admin123 ← 首次登录请立即改密"
echo ""
echo " 本地访问 : kubectl -n ${NS} port-forward svc/opsxsql 9000:80"
echo " 浏览器打开 http://localhost:9000"
echo ""
echo " 数据备份 : kubectl -n ${NS} exec deploy/opsxsql -- sqlite3 /app/data/sqlite3.db '.backup /app/data/backup.db'"
echo " 配 Ingress(可选):参照 ${DIR}/examples/ 按云选模板"
echo "════════════════════════════════════════════════════════"