Files
LokiStack/kubernetes/install.sh
T
opsx 3c9388658b refactor: 资源名 opsx-loki/opsxloki → lokistack(线上已迁移完成)
【已在线完成,零数据丢失】
K8s ns=log 资源全部改名:
  Deployment  opsx-loki          → lokistack
  Service     opsx-loki          → lokistack
  Service     opsx-loki-nodeport → lokistack-nodeport
  PVC         opsxloki-data      → lokistack-data
  Secret      opsxloki-secrets   → lokistack-secrets
  Label       app: opsx-loki     → app: lokistack
  Ingress     log.astx.club 后端  → lokistack-nodeport:8080
  镜像        lghost/opsxloki     → lghost/lokistack
  docker-compose 卷/容器名同步更名

【保留不变(改了会出事)】
  namespace: log
  数据文件名 opsxloki.json(代码硬编码)
  容器内路径 /app/data

【迁移过程】
  1. 备份 PVC 数据(32KB) + Secret 原值 + 旧 yaml → /root/lokistack-migration-backup-20261026-171141
  2. scale 旧 Deployment → 0,等 Pod 消失
  3. 复制 Secret(JWT sha256 校验一致)
  4. 新建 PVC lokistack-data(Bound 30Gi cbs)
  5. 临时 Pod 恢复数据(chown 1000:1000,opsxloki.json 保留 0600)
  6. apply 新 Deployment → 1/1 Running
  7. 修复 Ingress 后端(TKE qcloud 要求 NodePort)→ CLB 恢复 200
  8. 删除旧 Deployment / 失效 Service

【验证】
  lokistack 1/1,0 重启,镜像 lghost/lokistack:latest(statically linked)
  /api/v1/health → {"status":"ok","version":"2.0.0"}
  /app/data/opsxloki.json 610B 完整(admin 用户 + 配置)
  http://log.astx.club/ → HTTP 200
  镜像加固:admin123=0、/app/backend/=0、initial-admin-password.txt 存在

【回滚兜底(建议 7 天后删)】
  pvc/opsxloki-data、secret/opsxloki-secrets

【文档】
  README 新增「从 opsx-loki 升级」章节:
  K8s 七步迁移脚本 + docker-compose 卷迁移命令
2026-10-26 17:16:15 +00:00

334 lines
14 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
# ============================================================================
# lokistack 一键部署
#
# 自动适配:阿里云 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="lokistack-local"
LOCAL_PV_NAME="lokistack-data-pv"
LOCAL_PV_SIZE="${PV_SIZE:-30Gi}"
LOCAL_PV_PATH="${PV_PATH:-/var/lib/lokistack-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: lokistack
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 "lokistack 部署开始(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 lokistack-secrets >/dev/null 2>&1; then
info "Secret lokistack-secrets 已存在,跳过(不覆盖现有密钥)"
else
info "生成随机密钥 JWT_SECRET / ENCRYPTION_KEY"
kubectl -n "${NS}" create secret generic lokistack-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/lokistack --timeout=180s; then
echo ""
warn "部署未在 180 秒内就绪,开始诊断:"
echo ""
kubectl -n "${NS}" get po -l app=lokistack -o wide 2>/dev/null | sed 's/^/ /'
REASON=$(kubectl -n "${NS}" get po -l app=lokistack -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>/lokistack:latest"
echo " 阿里云 ACR : registry.cn-hangzhou.aliyuncs.com/<ns>/lokistack:latest"
echo " 华为云 SWR : swr.cn-north-4.myhuaweicloud.com/<ns>/lokistack:latest"
echo " 自建 Harbor: harbor.your.domain/library/lokistack:latest"
;;
CreateContainerConfigError)
warn "容器配置缺失(通常是 Secret 不存在)"
echo " → 检查: kubectl -n ${NS} get secret lokistack-secrets"
;;
"")
PVC_PHASE=$(kubectl -n "${NS}" get pvc lokistack-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 lokistack-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=lokistack 2>/dev/null | tail -15 | sed 's/^/ /'
die "部署未完成,请按上面提示处理"
fi
ok "Deployment 已就绪"
# ============================================================================
# 6) 输出访问信息
# ============================================================================
POD=$(kubectl -n "${NS}" get po -l app=lokistack -o jsonpath='{.items[0].metadata.name}' 2>/dev/null || echo "-")
POD_IP=$(kubectl -n "${NS}" get po -l app=lokistack -o jsonpath='{.items[0].status.podIP}' 2>/dev/null || echo "-")
NODE=$(kubectl -n "${NS}" get po -l app=lokistack -o jsonpath='{.items[0].spec.nodeName}' 2>/dev/null || echo "-")
echo ""
echo "════════════════════════════════════════════════════════"
ok "lokistack 部署完成"
echo "════════════════════════════════════════════════════════"
echo " 平台 : ${CLOUD}"
echo " Namespace : ${NS}"
echo " 存储类 : ${RESOLVED_SC}"
echo " Pod : ${POD} (node=${NODE} ip=${POD_IP})"
echo " Service : lokistack.${NS}.svc.cluster.local:8080"
[ "${WITH_NODEPORT}" = "1" ] && echo " NodePort : <任意节点IP>:30080"
echo ""
echo " 初始账号 : admin / admin123 ← 首次登录请立即改密"
echo ""
echo " 本地访问 : kubectl -n ${NS} port-forward svc/lokistack 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 "════════════════════════════════════════════════════════"