#!/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) 前置检查 # # 分三级:kubectl 是否存在 → 能否连上集群 → 有没有部署权限。 # 设计原则:每级失败都要说清「是哪一类问题 + 下一步具体做什么」。 # 只丢一句「请检查 kubeconfig」等于没说 —— 新人根本不知道该查什么, # 也不知道是没装工具、网络不通、凭据过期、还是 RBAC 没权限。 # ============================================================================ KUBECTL_TIMEOUT="${KUBECTL_TIMEOUT:-8s}" # 当前 context / server / user:失败时一起打印,一眼看出连的是哪个集群 k8s_env_info() { local ctx server user kc ctx=$(kubectl config current-context 2>/dev/null || echo "(无)") server=$(kubectl config view --minify -o jsonpath='{.clusters[0].cluster.server}' 2>/dev/null || true) user=$(kubectl config view --minify -o jsonpath='{.users[0].name}' 2>/dev/null || true) kc="${KUBECONFIG:-$HOME/.kube/config}" printf ' kubeconfig : %s\n' "$kc" >&2 printf ' 当前 context : %s\n' "$ctx" >&2 printf ' API Server : %s\n' "${server:-(未知)}" >&2 printf ' 当前用户 : %s\n' "${user:-(未知)}" >&2 } # 统一失败出口:一句话结论 + 环境信息 + 可执行的处理步骤 k8s_die() { printf '\n\033[1;31m❌ %s\033[0m\n\n' "$1" >&2 k8s_env_info printf '\n%b\n\n' "$2" >&2 exit 1 } # ── 0.1 kubectl 是否存在 ────────────────────────────────────────────── if ! command -v kubectl >/dev/null 2>&1; then printf '\n\033[1;31m❌ 找不到 kubectl 命令\033[0m\n\n' >&2 cat >&2 <<'KUBECTL_INSTALL_HELP' 本脚本通过 kubectl 操作 K8s 集群,需要先安装这个命令行工具。 ── 安装方式 ─────────────────────────────────────────────────────── 通用 Linux(官方二进制,最省事): curl -LO "https://dl.k8s.io/release/$(curl -Ls https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl" sudo install -o root -g root -m 0755 kubectl /usr/local/bin/kubectl CentOS / TencentOS / RHEL : sudo yum install -y kubectl Ubuntu / Debian : sudo apt-get update && sudo apt-get install -y kubectl macOS : brew install kubectl ── 装完自检 ─────────────────────────────────────────────────────── kubectl version --client ── 这台机器没有外网怎么办 ───────────────────────────────────────── · 云控制台「集群 → 连接信息」一般可直接下载 kubectl 与 kubeconfig · 或者在内网跳板机 / 能访问集群的机器上执行本脚本 · 只需把 kubectl 二进制 scp 过来即可,不依赖 Docker / Jenkins KUBECTL_INSTALL_HELP exit 1 fi # ── 0.2 kubeconfig 文件是否存在 ──────────────────────────────────────── # 必须先单独判一次:KUBECONFIG 指向不存在的文件时,kubectl 会忽略它并 # 退回默认规则,最终报出的往往是 "Authentication required"/"connection refused" # 这类**误导性**错误,让人以为是凭据或网络问题,实际只是配置没放对位置。 _KC_PATHS="${KUBECONFIG:-$HOME/.kube/config}" # KUBECONFIG 允许用 ':' 分隔多个路径,任意一个存在即算通过。 # 注意不要写成 while read 循环:printf 输出没有结尾换行时,read 会 # 带着数据返回非 0(EOF),`while read` 的循环体一次都不会执行 —— 踩过。 _kc_found=0 _OLD_IFS="${IFS}" IFS=':' for _p in ${_KC_PATHS}; do if [ -n "${_p}" ] && [ -f "${_p}" ]; then _kc_found=1 break fi done IFS="${_OLD_IFS}" if [ "${_kc_found}" -eq 0 ]; then k8s_die "找不到 kubeconfig 文件(不知道该连哪个集群)" \ " 查找位置: ${_KC_PATHS} (KUBECONFIG 环境变量未设置时,默认只找 ~/.kube/config) ── 怎么修 ───────────────────────────────────────────────────────── 1) 从云控制台下载 kubeconfig: 腾讯云 TKE : 集群 → 基本信息 → 连接信息 → 下载 kubeconfig 阿里云 ACK : 集群 → 集群信息 → 连接信息 → 复制/下载 kubeconfig AWS EKS : aws eks update-kubeconfig --name <集群名> --region <区域> 2) 放到默认位置(推荐): mkdir -p ~/.kube && mv <下载的文件> ~/.kube/config && chmod 600 ~/.kube/config 3) 或者用环境变量指向任意路径: export KUBECONFIG=/path/to/kubeconfig 4) 确认生效: kubectl config get-contexts 5) 重新执行本脚本" fi # ── 0.3 能否连上集群(探测一次,按错误类型分别给建议)──────────────── if ! PROBE_OUT="$(kubectl version --request-timeout="${KUBECTL_TIMEOUT}" 2>&1)"; then case "${PROBE_OUT}" in *"does not exist"*|*"no context exists"*|*"current-context is not set"*|*"context was not found"*) k8s_die "kubeconfig 里没有可用的 context(集群配置不对)" \ " ── 怎么修 ───────────────────────────────────────────────────────── 1) 确认 kubeconfig 在哪: echo \$KUBECONFIG ls -l ~/.kube/config 2) 看有哪些 context: kubectl config get-contexts 3) 切到正确的那一个: kubectl config use-context <名字> 4) 完全没有配置就下载: 腾讯云 → 集群 → 基本信息 → 连接信息 → 下载 kubeconfig 放到 ~/.kube/config,或 export KUBECONFIG=/path/to/config 5) 重新执行本脚本" ;; *"Unauthorized"*|*"Authentication required"*|*"credentials"*|*"invalid bearer token"*) k8s_die "集群认证失败(凭据无效或已过期)" \ " ── 怎么修 ───────────────────────────────────────────────────────── · 云厂商 kubeconfig 里一般是**临时 token**,有效期几小时,过期就 401 → 回控制台重新下载 kubeconfig,覆盖 ~/.kube/config · 自建集群看客户端证书是否过期: kubectl config view --minify --raw | grep client-certificate-data · 验证当前身份: kubectl auth whoami (旧版用 kubectl get ns 试)" ;; *"Forbidden"*|*"(403)"*) k8s_die "已经连上集群,但当前账号没有权限(RBAC 拒绝)" \ " ── 怎么修 ───────────────────────────────────────────────────────── · 确认身份: kubectl auth whoami · 看能干什么: kubectl auth can-i --list -n ${NS} · 部署本平台需要在 ${NS} 命名空间具备: deployments.apps / services / persistentvolumeclaims / configmaps / secrets 的 create、get 若该命名空间还不存在,还需要集群级 create namespaces · 找集群管理员绑定具备上述权限的 Role,或直接用管理员 kubeconfig 执行本脚本" ;; *"x509"*|*"certificate"*|*"tls:"*|*"TLS"*) k8s_die "集群证书校验失败(证书过期 / 域名不匹配 / 自签证书)" \ " ── 怎么修 ───────────────────────────────────────────────────────── · 云托管集群极少出现,多见于自建集群;先确认 API Server 证书是否过期 · 可信内网内临时跳过校验(仅用于排障,不要长期使用): kubectl --insecure-skip-tls-verify=true get ns · 检查 kubeconfig 里 server 的地址/域名是否在证书 SAN 内" ;; *"connection refused"*|*"no such host"*|*"i/o timeout"*|*"context deadline exceeded"*|*"network is unreachable"*|*"Unable to connect to the server"*|*"timeout"*) k8s_die "连不上集群 API Server(网络不通)" \ " 实际报错: $(printf '%s' "${PROBE_OUT}" | tail -1) ── 按顺序排查 ─────────────────────────────────────────────────────── 1) 地址对不对?用上面打印的 API Server 测一下: curl -k --max-time 5 /version 2) 要不要 VPN / 专线?云上 API Server 常是**内网地址**(10.x / 172.16~31.x / 192.168.x), 本机不在同一 VPC 时,必须先连 VPN、走专线,或把本机 IP 加进安全组白名单 3) 安全组 / 防火墙:云厂商默认只放行运维网段访问 6443 端口 4) 集群本身是否正常:控制台看集群状态是否「运行中」 5) 换台机器试:在能访问集群的跳板机上执行本脚本,可排除本机网络问题" ;; *) k8s_die "kubectl 无法连接集群" \ " 实际报错: $(printf '%s\n' "${PROBE_OUT}" | sed 's/^/ /') ── 通用排查 ───────────────────────────────────────────────────────── · kubectl config get-contexts # 当前用的是哪个 context · kubectl cluster-info # API Server 是否可达 · echo \$KUBECONFIG # 确认读到的是哪个 kubeconfig · 容器/精简镜像里执行时,注意镜像内可能没有 ca-certificates 与 kubectl" ;; esac fi # ── 0.4 部署权限预检(只告警,不阻断)──────────────────────────────── # 说明:第一次部署时目标命名空间可能还不存在,can-i 对命名空间级资源会返回 no, # 为避免误拦正常流程这里只提示;真正的拒绝会在 apply 阶段以 Forbidden 暴露。 if [ "$(kubectl auth can-i create deployments.apps -n "${NS}" --request-timeout="${KUBECTL_TIMEOUT}" 2>/dev/null || echo unknown)" != "yes" ]; then warn "当前账号在命名空间 ${NS} 未确认具备部署权限(kubectl auth can-i 非 yes)" warn "若随后 apply 报 Forbidden:请改用管理员 kubeconfig,或让管理员绑定 Role 后再执行" fi # ============================================================================ # 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 </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 </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= bash kubernetes/install.sh" echo "" echo " 2) 标记某个 SC 为默认:" echo " kubectl patch 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//lokistack:latest" echo " 阿里云 ACR : registry.cn-hangzhou.aliyuncs.com//lokistack:latest" echo " 华为云 SWR : swr.cn-north-4.myhuaweicloud.com//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 "════════════════════════════════════════════════════════"