feat(k8s): 一键部署脚本 + 跨云适配 + 云相关清单拆分到 examples

新增 kubernetes/install.sh(一条命令部署):
- 云平台识别:读节点 providerID,识别 TKE / EKS / ACK / CCE / GKE / AKS / K3s / 自建
- StorageClass 三级探测:显式 STORAGE_CLASS → is-default-class 注解 → 集群唯一 SC
- 自建集群兜底:无任何 SC 时自动创建静态 local PV
  (hostPath + DirectoryOrCreate + nodeAffinity + WaitForFirstConsumer + Retain)
- Secret 幂等生成:已存在则跳过,绝不覆盖(避免密钥轮换导致历史凭证无法解密)
- 失败分类诊断:ImagePullBackOff / CreateContainerConfigError / PVC Pending
  按云给出对应仓库格式与推荐 SC 名
- 环境变量:WITH_NODEPORT / STORAGE_CLASS / LOCAL_STORAGE / NODE_NAME / PV_PATH / PV_SIZE

云相关清单拆到 kubernetes/examples/:
- ingress.yaml(阿里云 alb)、ingress-tke.yaml(腾讯云 qcloud)、secret.example.yaml
- 原因:留在根目录会导致 kubectl apply -f kubernetes/ 时 ingress 同名互相覆盖,
  且 secret.example.yaml 的占位符会把真实密钥顶掉

README 重写:
- 全部用法速查 + 环境变量一览
- 自建 K8s 存储方案选型(local PV / local-path-provisioner / OpenEBS / Longhorn / NFS)
- hostPath 数据运维:定位、热备份、跨节点迁移、卸载保留、常见坑
This commit is contained in:
opsx committed 2026-10-26 14:03:39 +00:00
1 parent 9f7c522157
commit 9a46a955d4
5 files changed
+704 -26

No files matched your search

+333
View File
@@ -0,0 +1,333 @@
#!/usr/bin/env bash
# ============================================================================
# opsx-loki 一键部署
#
# 自动适配:阿里云 ACK / 腾讯云 TKE / AWS EKS / 华为云 CCE / GCP GKE / 自建 K8s
# 命名空间固定:log
# 幂等:重复执行不覆盖密钥、不重建数据卷
#
# 用法:
# bash kubernetes/install.sh # 自动探测
# WITH_NODEPORT=1 bash kubernetes/install.sh # 同时开 NodePort(配 ingress)
# STORAGE_CLASS=local-path bash kubernetes/install.sh # 手动指定 StorageClass
# ============================================================================
set -euo pipefail
NS="log"
WITH_NODEPORT="${WITH_NODEPORT:-0}"
STORAGE_CLASS="${STORAGE_CLASS:-}"
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
info() { printf '\033[1;36m==>\033[0m %s\n' "$*"; }
ok() { printf '\033[1;32m✅\033[0m %s\n' "$*"; }
warn() { printf '\033[1;33m⚠️ \033[0m %s\n' "$*"; }
die() { printf '\033[1;31m❌\033[0m %s\n' "$*" >&2; exit 1; }
# ============================================================================
# 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=""
# 1) 显式指定
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
# 2) 带 default 注解的 SC
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
# 3) 集群里只有一个 SC → 直接用它
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
# 4) 无法确定
return 1
}
# ============================================================================
# 0.3) 自建集群兜底:无任何 StorageClass 时,创建「静态 local PV」
#
# 设计取舍:
# - 零外部依赖:不拉 local-path-provisioner 镜像(国内网络常常拉不到)
# - hostPath + DirectoryOrCreate:kubelet 自动建目录,无需预先 mkdir
# - nodeAffinity 锁节点:Pod 永远调度到数据所在节点
# - WaitForFirstConsumer:先定 Pod 再绑 PV,避免跨节点错绑
# - reclaimPolicy: Retain:误删 PVC 不丢数据
#
# 代价:数据固定在一台节点,节点损毁需从备份恢复(README 有 CSI 备份说明)
# ============================================================================
LOCAL_SC_NAME="opsxloki-local"
LOCAL_PV_NAME="opsxloki-data-pv"
LOCAL_PV_SIZE="${PV_SIZE:-30Gi}"
LOCAL_PV_PATH="${PV_PATH:-/var/lib/opsxloki-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: opsx-loki
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 "opsx-loki 部署开始(namespace: ${NS})"
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 ""
echo " 2) 标记某个 SC 为默认:"
echo " kubectl patch sc <sc名称> -p '{\"metadata\":{\"annotations\":{\"storageclass.kubernetes.io/is-default-class\":\"true\"}}}'"
echo ""
echo " 3) 自建集群安装 local-path-provisioner(动态供给,支持多 PVC):"
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 ""
echo " 4) 让脚本用节点本地存储(零依赖):"
echo " 去掉 LOCAL_STORAGE=off 重跑即可"
echo ""
die "StorageClass 未就绪,部署中止(避免 PVC 永久 Pending)"
fi
else
ok "StorageClass: ${RESOLVED_SC}"
fi
# ============================================================================
# 1) 命名空间
# ============================================================================
kubectl apply -f "${DIR}/namespace.yaml"
# ============================================================================
# 2) Secret(幂等:已存在则跳过,绝不覆盖)
# ============================================================================
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 opsxloki-secrets >/dev/null 2>&1; then
info "Secret opsxloki-secrets 已存在,跳过(不覆盖现有密钥)"
else
info "生成随机密钥 JWT_SECRET / ENCRYPTION_KEY"
kubectl -n "${NS}" create secret generic opsxloki-secrets \
--from-literal=JWT_SECRET="$(rand_hex)" \
--from-literal=ENCRYPTION_KEY="$(rand_hex)" >/dev/null
ok "Secret 已创建"
fi
# ============================================================================
# 3) 数据卷(显式写入探测到的 StorageClass)
# ============================================================================
RENDER_DIR="$(mktemp -d)"
trap 'rm -rf "${RENDER_DIR}"' EXIT
awk -v sc="${RESOLVED_SC}" '
/^spec:/ && !done { print; print " storageClassName: " sc; done=1; next }
{ print }
' "${DIR}/pvc.yaml" > "${RENDER_DIR}/pvc.yaml"
kubectl apply -f "${RENDER_DIR}/pvc.yaml"
# ============================================================================
# 4) 工作负载 + 服务
# ============================================================================
kubectl apply -f "${DIR}/deployment.yaml"
kubectl apply -f "${DIR}/service.yaml"
if [ "${WITH_NODEPORT}" = "1" ]; then
kubectl apply -f "${DIR}/service-nodeport.yaml"
ok "NodePort Service 已创建(30080)"
fi
# ============================================================================
# 5) 等待就绪(失败时给出按云定制的诊断)
# ============================================================================
info "等待 Deployment 就绪(最多 180 秒)..."
if ! kubectl -n "${NS}" rollout status deployment/opsx-loki --timeout=180s; then
echo ""
warn "部署未在 180 秒内就绪,开始诊断:"
echo ""
kubectl -n "${NS}" get po -l app=opsx-loki -o wide 2>/dev/null | sed 's/^/ /'
REASON=$(kubectl -n "${NS}" get po -l app=opsx-loki -o jsonpath='{.items[0].status.containerStatuses[0].state.waiting.reason}' 2>/dev/null || true)
echo ""
case "${REASON}" in
ImagePullBackOff|ErrImagePull)
warn "镜像拉取失败(${REASON})"
echo " → 当前镜像: $(grep -E '^\s+image:' "${DIR}/deployment.yaml" | head -1 | awk '{print $2}')"
echo " → 该节点拉不到 Docker Hub。请改为目标云可拉的仓库后重跑:"
echo " 腾讯云 TCR : ccr.ccs.tencentyun.com/<ns>/opsxloki:latest"
echo " 阿里云 ACR : registry.cn-hangzhou.aliyuncs.com/<ns>/opsxloki:latest"
echo " 华为云 SWR : swr.cn-north-4.myhuaweicloud.com/<ns>/opsxloki:latest"
echo " 自建 Harbor: harbor.your.domain/library/opsxloki:latest"
;;
CreateContainerConfigError)
warn "容器配置缺失(通常是 Secret 不存在)"
echo " → 检查: kubectl -n ${NS} get secret opsxloki-secrets"
;;
"")
PVC_PHASE=$(kubectl -n "${NS}" get pvc opsxloki-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 opsxloki-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=opsx-loki 2>/dev/null | tail -15 | sed 's/^/ /'
die "部署未完成,请按上面提示处理"
fi
ok "Deployment 已就绪"
# ============================================================================
# 6) 输出访问信息
# ============================================================================
POD=$(kubectl -n "${NS}" get po -l app=opsx-loki -o jsonpath='{.items[0].metadata.name}' 2>/dev/null || echo "-")
POD_IP=$(kubectl -n "${NS}" get po -l app=opsx-loki -o jsonpath='{.items[0].status.podIP}' 2>/dev/null || echo "-")
NODE=$(kubectl -n "${NS}" get po -l app=opsx-loki -o jsonpath='{.items[0].spec.nodeName}' 2>/dev/null || echo "-")
echo ""
echo "════════════════════════════════════════════════════════"
ok "opsx-loki 部署完成"
echo "════════════════════════════════════════════════════════"
echo " 平台 : ${CLOUD}"
echo " Namespace : ${NS}"
echo " 存储类 : ${RESOLVED_SC}"
echo " Pod : ${POD} (node=${NODE} ip=${POD_IP})"
echo " Service : opsx-loki.${NS}.svc.cluster.local:8080"
[ "${WITH_NODEPORT}" = "1" ] && echo " NodePort : <任意节点IP>:30080"
echo ""
echo " 初始账号 : admin / admin123 ← 首次登录请立即改密"
echo ""
echo " 本地访问 : kubectl -n ${NS} port-forward svc/opsx-loki 8080:8080"
echo " 浏览器打开 http://localhost:8080"
echo ""
echo " 配 Ingress(可选,先跑 WITH_NODEPORT=1 bash install.sh):"
case "${CLOUD}" in
腾讯云*) echo " kubectl apply -f ${DIR}/examples/ingress-tke.yaml # qcloud class" ;;
阿里云*) echo " kubectl apply -f ${DIR}/examples/ingress.yaml # alb class" ;;
*) echo " 参照 ${DIR}/examples/ 改 ingressClassName 与注解" ;;
esac
echo "════════════════════════════════════════════════════════"