From 9a46a955d45ea91911540037c4b4e15095ba116b Mon Sep 17 00:00:00 2001 From: opsx Date: Mon, 26 Oct 2026 14:03:39 +0000 Subject: [PATCH] =?UTF-8?q?feat(k8s):=20=E4=B8=80=E9=94=AE=E9=83=A8?= =?UTF-8?q?=E7=BD=B2=E8=84=9A=E6=9C=AC=20+=20=E8=B7=A8=E4=BA=91=E9=80=82?= =?UTF-8?q?=E9=85=8D=20+=20=E4=BA=91=E7=9B=B8=E5=85=B3=E6=B8=85=E5=8D=95?= =?UTF-8?q?=E6=8B=86=E5=88=86=E5=88=B0=20examples?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增 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 数据运维:定位、热备份、跨节点迁移、卸载保留、常见坑 --- kubernetes/README.md | 364 ++++++++++++++++-- kubernetes/examples/ingress-tke.yaml | 33 ++ kubernetes/{ => examples}/ingress.yaml | 0 kubernetes/{ => examples}/secret.example.yaml | 0 kubernetes/install.sh | 333 ++++++++++++++++ 5 files changed, 704 insertions(+), 26 deletions(-) create mode 100644 kubernetes/examples/ingress-tke.yaml rename kubernetes/{ => examples}/ingress.yaml (100%) rename kubernetes/{ => examples}/secret.example.yaml (100%) create mode 100755 kubernetes/install.sh diff --git a/kubernetes/README.md b/kubernetes/README.md index 3a57e76..6afa0b2 100644 --- a/kubernetes/README.md +++ b/kubernetes/README.md @@ -1,41 +1,332 @@ # opsx-loki 部署到 Kubernetes -## 文件 +命名空间固定:**`log`**(日志相关资源统一放这里) -| 文件 | 说明 | -|---|---| -| `namespace.yaml` | `log` 命名空间(已有可跳过) | -| `secret.example.yaml` | JWT/ENCRYPTION 密钥模板(建议用 `kubectl create secret` 命令创建,不要提交真实值) | -| `pvc.yaml` | 平台数据卷(30Gi,默认 StorageClass) | -| `deployment.yaml` | 平台本体,**单副本 + Recreate** | -| `service.yaml` | ClusterIP `opsx-loki:8080` | -| `ingress.yaml` | 可选,按云厂商的 ingressClass 调整 | +--- -## 部署步骤 +## 一条命令部署(推荐) ```bash -# 1) 密钥(只生成一次,之后复用;丢失会导致已保存的凭证无法解密) +bash kubernetes/install.sh +``` + +脚本会依次完成: + +1. 创建 `log` 命名空间 +2. **生成随机密钥** `JWT_SECRET` / `ENCRYPTION_KEY` 写入 Secret `opsxloki-secrets` + (已存在则跳过,**不会覆盖**) +3. 创建 PVC `opsxloki-data`(30 GiB,使用集群默认 StorageClass) +4. 部署 `opsx-loki`(单副本 + Recreate) +5. 创建 Service `opsx-loki:8080`(ClusterIP) +6. 等待 Pod 就绪并输出访问方式 + +**需要 NodePort(配 ingress 时)**: + +```bash +WITH_NODEPORT=1 bash kubernetes/install.sh +``` + +**幂等性**:重复执行只做增量更新,不会覆盖密钥、不会重建数据卷。 + +--- + +## 全部用法速查 + +| 场景 | 命令 | +|------|------| +| **标准部署**(自动探测云平台 + StorageClass)| `bash kubernetes/install.sh` | +| 同时开 NodePort(配 ingress 用)| `WITH_NODEPORT=1 bash kubernetes/install.sh` | +| 指定 StorageClass | `STORAGE_CLASS=cbs bash kubernetes/install.sh` | +| 指定 SC(自建用 local-path)| `STORAGE_CLASS=local-path bash kubernetes/install.sh` | +| **自建集群强制用节点本地盘**(hostPath)| `LOCAL_STORAGE=force bash kubernetes/install.sh` | +| 只诊断不动手(无变更)| `LOCAL_STORAGE=off bash kubernetes/install.sh` | +| **指定本地存储的节点/路径/容量** | `NODE_NAME=node1 PV_PATH=/data/opsxloki PV_SIZE=50Gi bash kubernetes/install.sh` | +| 组合:自建 + 指定节点 + NodePort | `LOCAL_STORAGE=force NODE_NAME=node1 WITH_NODEPORT=1 bash kubernetes/install.sh` | + +**环境变量一览**: + +| 变量 | 默认值 | 说明 | +|------|--------|------| +| `WITH_NODEPORT` | `0` | `1` = 额外创建 NodePort Service(30080)| +| `STORAGE_CLASS` | 空(自动探测)| 显式指定 StorageClass 名称 | +| `LOCAL_STORAGE` | `auto` | `auto` 无 SC 时自动建本地 PV / `off` 只诊断 / `force` 强制本地盘 | +| `NODE_NAME` | 空(第一个 Ready 节点)| hostPath PV 绑定的节点 | +| `PV_PATH` | `/var/lib/opsxloki-data` | hostPath 在宿主机上的路径 | +| `PV_SIZE` | `30Gi` | PV 容量声明 | + +--- + +## 环境要求 + +| 项 | 要求 | +|----|------| +| Kubernetes | 1.20+ | +| StorageClass | **脚本自动探测**(default 注解 → 唯一 SC → 手动 `STORAGE_CLASS=`);自建集群无 SC 时会提前报错并给出解决方案,不会白等 | +| 镜像 | 节点可拉 `lghost/opsxloki:latest`(或改成自己的 Harbor/ACR/TCR/SWR) | +| 权限 | 有 `log` 命名空间的 create/apply 权限 | + +## 脚本自动处理什么 + +`install.sh` 不是无脑 apply,它会先探测环境再动手: + +| 场景 | 脚本行为 | +|------|---------| +| **云平台识别** | 读节点 `providerID`,识别出 腾讯云 TKE / AWS EKS / 阿里云 ACK / 华为云 CCE / GCP GKE / Azure AKS / K3s / 自建 | +| **StorageClass** | ① 显式 `STORAGE_CLASS=xxx` → ② `is-default-class` 注解 → ③ 集群唯一 SC → ④ **都没有则自动创建节点本地存储**(静态 local PV,零外部依赖) | +| **Secret** | 不存在则用 `openssl rand -hex 32` 生成;**已存在则跳过,绝不覆盖** | +| **PVC** | 把探测到的 SC 显式写进渲染后的 pvc.yaml(避免依赖隐式默认行为) | +| **就绪等待** | `rollout status` 最多 180s,超时后按**失败原因分类诊断** | +| **镜像拉不到** | 诊断为 `ImagePullBackOff` 时,按云给出对应仓库格式(TCR / ACR / SWR / Harbor) | +| **PVC 未绑定** | 诊断为 StorageClass 问题时,按云给出推荐 SC 名(cbs / gp3 / alicloud-disk-essd / csi-disk) | + +**脚本不做什么**(需要人工决策的部分): + +- ❌ 不自动改镜像仓库(不知道你的仓库地址,只给格式提示) +- ❌ 不覆盖已存在的 Secret / 已有数据卷 + +--- + +## 自建 K8s(kubeadm 裸装)的存储怎么办 + +自建集群最常见的坑就是**没有 StorageClass** → PVC 永久 Pending。脚本内置三种处理方式: + +### 默认行为(`LOCAL_STORAGE=auto`):自动创建节点本地存储 + +检测到集群完全没有 SC 时,脚本自动创建一个**静态 local PV**: + +```yaml +StorageClass: opsxloki-local + provisioner: kubernetes.io/no-provisioner + volumeBindingMode: WaitForFirstConsumer # 先定 Pod 再绑 PV + reclaimPolicy: Retain # 误删 PVC 不丢数据 + +PersistentVolume: opsxloki-data-pv + hostPath: { path: /var/lib/opsxloki-data, type: DirectoryOrCreate } + nodeAffinity: 锁定一台节点 +``` + +**特点**: + +| 优点 | 代价 | +|------|------| +| **零外部依赖**(不拉任何镜像,国内网络友好)| 数据固定在单节点,Pod 不会漂移到别处 | +| kubelet 自动建目录(`DirectoryOrCreate`)| 只能供给一个 PVC(单实例场景够用)| +| Pod 一定调度到数据所在节点(`nodeAffinity`)| 节点损毁需从备份恢复(见「持久化」的 CSI 快照)| + +**实测数据**:PVC 从 Pending 到 Bound **5 秒**,Pod 正常读写宿主机目录。 + +### 想要动态供给(支持多 PVC / 多实例) + +装 Rancher **local-path-provisioner**(自建集群事实标准): + +```bash +kubectl apply -f https://raw.githubusercontent.com/rancher/local-path-provisioner/master/deploy/local-path-storage.yaml +STORAGE_CLASS=local-path bash kubernetes/install.sh +``` + +### 想先看着不动手(`LOCAL_STORAGE=off`) + +```bash +LOCAL_STORAGE=off bash kubernetes/install.sh +``` + +只打印诊断和 4 种解法,不做任何变更。 + +### 自建 K8s 存储方案选型 + +| 方案 | 外部依赖 | 适用 | 备注 | +|------|---------|------|------| +| **静态 local PV**(本脚本默认)| **零** | 单副本、数据固定 | 最省事,国内网络友好 | +| **local-path-provisioner** | 2 个镜像 | 需要动态供给/多 PVC | 自建集群首选,社区标准 | +| **OpenEBS LocalPV** | Helm + 较多镜像 | 要 LVM/ZFS 精细控制 | 较重 | +| **Longhorn** | Helm,每节点装 | 需要副本/高可用 | 每节点约 1C2G 开销 | +| **NFS + nfs-subdir-provisioner** | 需现成 NFS 服务器 | 多节点共享 | 依赖外部 NFS | + +> 对 opsx-loki 这类**单副本 + JSON DB 单写者**的应用,**local PV 完全够用**, +> 不需要为了"高可用"上 Longhorn —— 副本存储反而会和"单写者"约束冲突。 + +--- + +## hostPath 数据的运维(自建集群必读) + +自建集群用 hostPath 时,**数据在宿主机目录里**,不在云盘上。下面这些操作要会。 + +### 数据在哪 + +```bash +# 1) 找到 PV 绑定的节点和路径 +kubectl get pv opsxloki-data-pv -o custom-columns=\ +'NAME:.metadata.name,PATH:.spec.hostPath.path,NODE:.spec.nodeAffinity.required.nodeSelectorTerms[0].matchExpressions[0].values[0]' + +# 2) 去那台节点上看 +NODE=<上面的 NODE 值> +ssh root@${NODE} 'ls -la /var/lib/opsxloki-data/' +# 应看到:opsxloki.json kubeconfigs/ known_hosts +``` + +### 备份(不用停机) + +```bash +NODE=<节点> +TS=$(date +%Y%m%d-%H%M%S) +ssh root@${NODE} "tar czf /tmp/opsxloki-backup-${TS}.tar.gz -C /var/lib opsxloki-data" +scp root@${NODE}:/tmp/opsxloki-backup-${TS}.tar.gz ./ +``` + +> `opsxloki.json` 写入是原子的(临时文件 + fsync + rename),**热备份安全**,不用停 Pod。 + +### 迁移到另一台节点 + +```bash +OLD=<旧节点> NEW=<新节点> + +# 1) 停止写入 +kubectl -n log scale deploy opsx-loki --replicas=0 + +# 2) 从旧节点拉取数据 +ssh root@${OLD} 'tar czf - -C /var/lib opsxloki-data' > /tmp/opsxloki-data.tar.gz + +# 3) 推到新节点并解包 +scp /tmp/opsxloki-data.tar.gz root@${NEW}:/tmp/ +ssh root@${NEW} 'mkdir -p /var/lib && tar xzf /tmp/opsxloki-data.tar.gz -C /var/lib && ls -la /var/lib/opsxloki-data' + +# 4) 改 PV 的 nodeAffinity 指向新节点 +kubectl patch pv opsxloki-data-pv --type=json -p \ + "[{\"op\":\"replace\",\"path\":\"/spec/nodeAffinity/required/nodeSelectorTerms/0/matchExpressions/0/values\",\"value\":[\"${NEW}\"]}]" + +# 5) 恢复 +kubectl -n log scale deploy opsx-loki --replicas=1 +kubectl -n log rollout status deploy/opsx-loki +``` + +### 卸载但保留数据 + +```bash +# 删工作负载和服务(数据卷保留) +kubectl -n log delete deploy opsx-loki svc opsx-loki + +# PVC 保留(Retain 策略,删了也不会丢 PV 里的数据) +# 想彻底清掉的话: +# kubectl delete pvc opsxloki-data +# kubectl delete pv opsxloki-data-pv +# 然后去节点上 rm -rf /var/lib/opsxloki-data + +# 如果要连脏数据一起重置重装: +kubectl -n log delete pvc opsxloki-data +kubectl delete pv opsxloki-data-pv +kubectl delete sc opsxloki-local +ssh root@${NODE} 'rm -rf /var/lib/opsxloki-data' +# 然后重新 bash install.sh +``` + +### 常见坑 + +| 现象 | 原因 | 处理 | +|------|------|------| +| Pod 一直 Pending | PV 的 nodeAffinity 指向的节点不可用 | `kubectl get nodes` 确认该节点 Ready,或按上面「迁移」流程改节点 | +| Pod Pending 且报 `node(s) didn't match node affinity` | Pod 被调度到其他节点 | 正常,scheduler 会重试;若持续,检查 PV 的 nodeAffinity 是否写错节点名 | +| 换节点后数据"消失" | 数据在新节点上不存在 | 走「迁移到另一台节点」流程,别只改 nodeAffinity | +| 磁盘写满 | hostPath 用节点本地盘 | `df -h /var/lib`,迁移到大盘或改 `PV_PATH` | + +--- + +## 目录结构 + +``` +kubernetes/ +├── install.sh # 一键部署脚本(推荐入口) +├── namespace.yaml # log 命名空间 +├── pvc.yaml # 数据卷 30Gi(storageClassName 留空 → 用默认 SC) +├── deployment.yaml # 平台本体(单副本 + Recreate,必须) +├── service.yaml # ClusterIP opsx-loki:8080 +├── service-nodeport.yaml # NodePort 30080(配 ingress 时用) +└── examples/ # 云相关配置,不参与默认部署 + ├── ingress.yaml # 阿里云 ACK(alb class) + ├── ingress-tke.yaml # 腾讯云 TKE(qcloud class) + └── secret.example.yaml # Secret 模板(仅供手工创建参考) +``` + +> ⚠️ `examples/` 里的文件**故意不放在 `kubernetes/` 根目录**: +> 它们是云厂商相关的(ingress class 各云不同),混在默认目录里会让 +> `kubectl apply -f kubernetes/` 在不同云上互相覆盖或报错。 + +--- + +## 手动部署(等价于 install.sh,供需要精细控制时用) + +```bash +# 1) 命名空间 +kubectl apply -f kubernetes/namespace.yaml + +# 2) 密钥(⚠️ 必须先做,否则 Pod 起不来) +# 密钥只生成一次,丢失会导致已保存的凭证无法解密 kubectl -n log create secret generic opsxloki-secrets \ --from-literal=JWT_SECRET="$(openssl rand -hex 32)" \ --from-literal=ENCRYPTION_KEY="$(openssl rand -hex 32)" -# 2) 数据卷 + 平台 -kubectl apply -f kubernetes/namespace.yaml +# 3) 数据卷 + 平台 + 服务 kubectl apply -f kubernetes/pvc.yaml -# 修改 deployment.yaml 里的 image 为实际仓库地址后: kubectl apply -f kubernetes/deployment.yaml kubectl apply -f kubernetes/service.yaml -# 3) 可选:Ingress(不需要就跳过,继续走外部 WAF 回源) -kubectl apply -f kubernetes/ingress.yaml +# 4) 等待就绪 +kubectl -n log rollout status deployment/opsx-loki --timeout=180s ``` +> ❗**不要直接 `kubectl apply -f kubernetes/`**:Secret 不在目录里(密钥不能进 Git), +> 缺 Secret 时 Pod 会一直 `CreateContainerConfigError`。 + +--- + +## 访问 + +**临时(本地端口转发)**: + +```bash +kubectl -n log port-forward svc/opsx-loki 8080:8080 +# 浏览器打开 http://localhost:8080 +``` + +**初始账号**:`admin` / `admin123` ← **首次登录请立即改密** + +--- + +## 配 Ingress(可选) + +以腾讯云 TKE 为例(其他云见 `examples/`): + +```bash +# 1) 先开 NodePort(TKE 的 qcloud ingress 后端要求 NodePort 类型 Service) +WITH_NODEPORT=1 bash kubernetes/install.sh + +# 2) 改域名后应用 +vi kubernetes/examples/ingress-tke.yaml # 改 host +kubectl apply -f kubernetes/examples/ingress-tke.yaml + +# 3) 查 CLB 分配的公网 IP,把域名解析过去 +kubectl -n log get ingress opsx-loki +``` + +| 云 | ingressClassName | 备注 | +|----|------------------|------| +| 腾讯云 TKE | `qcloud` | 后端 Service 必须 NodePort | +| 阿里云 ACK | `alb` | 用 `alb.ingress.kubernetes.io/*` 注解 | +| AWS EKS | `alb` 或 `nginx` | 需装 AWS Load Balancer Controller | +| 华为云 CCE | `nginx` 或 `cce` | — | +| 自建 | `nginx` | 需装 ingress-nginx | + +--- + ## 持久化(重要) - `/app/data` 挂 PVC,保存 `opsxloki.json`、`kubeconfigs/`、`known_hosts`。 -- **必须 `replicas: 1` + `strategy: Recreate`**:JSON DB 是单写者;滚动更新时两个 Pod 同时挂同一个 RWO PVC 会互相覆盖。 +- **必须 `replicas: 1` + `strategy: Recreate`**:JSON DB 是单写者;滚动更新时两个 Pod + 同时挂同一个 RWO PVC 会互相覆盖。 - DB 写入本身是原子的(临时文件 + fsync + rename),容器被杀不会写坏文件。 -- 备份建议用 CSI VolumeSnapshot(AWS EBS / 腾讯 CBS / 华为 EVS / 阿里云 ESSD 都支持同一套 `snapshot.storage.k8s.io` API): + +### 备份:CSI VolumeSnapshot + +各云都支持同一套 `snapshot.storage.k8s.io` API(AWS EBS / 腾讯 CBS / 华为 EVS / 阿里云 ESSD): ```yaml apiVersion: snapshot.storage.k8s.io/v1 @@ -49,31 +340,52 @@ spec: persistentVolumeClaimName: opsxloki-data ``` +--- + ## 多云兼容 | 项 | 做法 | |---|---| -| 存储类 | `pvc.yaml` 不写 `storageClassName`,用集群默认 SC;没有默认 SC 时填对应云的 SC 名(AWS `gp3` / 腾讯 `cbs` / 华为 `csi-disk` / 阿里云 `alicloud-disk-essd`) | -| 镜像仓库 | `deployment.yaml` 的 `image` 填目标云可拉的仓库(Harbor / ACR / TCR / SWR) | -| Loki 存储 | 平台已支持 S3 / OSS / COS / OBS,均为 S3 兼容,无需云厂商 SDK | +| 存储类 | `pvc.yaml` **不写** `storageClassName`,用集群默认 SC;没有默认 SC 时填对应云的名字(AWS `gp3` / 腾讯 `cbs` / 华为 `csi-disk` / 阿里云 `alicloud-disk-essd`)| +| 镜像仓库 | `deployment.yaml` 的 `image` 填目标云可拉的仓库(Harbor / ACR / TCR / SWR)| +| Loki 存储 | 平台已支持 S3 / OSS / COS / OBS,均 S3 兼容,无需云厂商 SDK | | 查询链路 | 平台与 Loki 同集群时,数据源选「集群服务」(namespace `log`、service `loki`、port `3100`),无需 Ingress/WAF | +| Namespace | 固定 `log`,跨云一致 | + +--- ## 与外部实例的关系 平台的 JSON DB 单写者,**不要两个实例共用同一份数据卷**。要么: + - 集群内新实例 + 迁移旧 `opsxloki.json`(停旧实例后拷贝);或 -- 保留外部 229 实例做带外管理,集群内实例使用独立数据卷(两套数据)。 +- 保留外部实例做带外管理,集群内实例使用独立数据卷(两套数据)。 + +--- ## 日志 -- 平台**只写 stdout**(Go/GIN 默认),不写日志文件;`kubectl logs` / `docker logs` 可直接看,Pod 部署无需额外配置。 -- Docker Compose:已配置 `json-file` 轮转 `max-size=50m`、`max-file=5`。 -- Kubernetes:由 kubelet 统一轮转,节点级配置建议: +- 平台**只写 stdout**(Go/GIN 默认),不写日志文件;`kubectl logs` 可直接看。 +- Kubernetes 由 kubelet 统一轮转,节点级配置建议: + ```yaml # /var/lib/kubelet/config.yaml containerLogMaxSize: 50Mi containerLogMaxFiles: 5 ``` - 也可以用 kubelet 启动参数 `--container-log-max-size=50Mi --container-log-max-files=5`。 + - Loki / Promtail 同样输出到 stdout,由容器运行时轮转;不要在容器内写业务日志文件。 - 应用自身的部署日志写在 DB(`deployment_logs`),可在部署详情里查看,不占容器日志空间。 + +--- + +## 常见问题 + +| 现象 | 原因 | 解决 | +|------|------|------| +| Pod `CreateContainerConfigError` | 缺 Secret `opsxloki-secrets` | 跑 `install.sh`,或手动 `kubectl create secret` | +| Pod `Pending`,PVC `Pending` | 集群没有默认 StorageClass | `pvc.yaml` 里填 `storageClassName` | +| Pod `ImagePullBackOff` | 节点拉不到 `lghost/opsxloki:latest` | 换镜像地址(ACR/TCR/SWR/Harbor) | +| `kubectl apply -f kubernetes/` 报 ingress 错误 | 误把 examples 里的 ingress 一起 apply 了 | 只 apply 根目录的 yaml,ingress 单独处理 | +| 重启后数据丢失 | `/app/data` 没挂 PVC | 确认 `deployment.yaml` 的 volumeMounts | +| 两个 Pod 同时写 DB | 用了 RollingUpdate 或多副本 | 必须 `replicas: 1` + `strategy: Recreate` | diff --git a/kubernetes/examples/ingress-tke.yaml b/kubernetes/examples/ingress-tke.yaml new file mode 100644 index 0000000..6920e29 --- /dev/null +++ b/kubernetes/examples/ingress-tke.yaml @@ -0,0 +1,33 @@ +# 腾讯云 TKE 专用 Ingress 模板 +# +# 与官方 ingress.yaml 的区别: +# 官方用 alb(阿里云)→ TKE 不支持 +# TKE 用 ingressClassName: qcloud,且后端 Service 必须是 NodePort 类型 +# +# 使用前: +# 1) 先创建 NodePort Service:kubectl apply -f kubernetes/service-nodeport.yaml +# 2) 把下面 host 改成你要用的域名 +# 3) kubectl apply -f kubernetes/ingress-tke.yaml +# 4) 在腾讯云控制台/DNS 把该域名解析到 CLB 的公网 IP +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: opsx-loki + namespace: log + annotations: + # CLB 健康检查(可选,TKE 默认会用 TCP 检查) + cloud.tencent.com/health-check-path: /api/v1/health + cloud.tencent.com/health-check-interval: "10" +spec: + ingressClassName: qcloud + rules: + - host: loki.astx.club # ← 改成你的域名 + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: opsx-loki-nodeport # ← 必须是 NodePort 类型 Service + port: + number: 8080 diff --git a/kubernetes/ingress.yaml b/kubernetes/examples/ingress.yaml similarity index 100% rename from kubernetes/ingress.yaml rename to kubernetes/examples/ingress.yaml diff --git a/kubernetes/secret.example.yaml b/kubernetes/examples/secret.example.yaml similarity index 100% rename from kubernetes/secret.example.yaml rename to kubernetes/examples/secret.example.yaml diff --git a/kubernetes/install.sh b/kubernetes/install.sh new file mode 100755 index 0000000..c1b9b28 --- /dev/null +++ b/kubernetes/install.sh @@ -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 </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: 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= 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 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//opsxloki:latest" + echo " 阿里云 ACR : registry.cn-hangzhou.aliyuncs.com//opsxloki:latest" + echo " 华为云 SWR : swr.cn-north-4.myhuaweicloud.com//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 "════════════════════════════════════════════════════════"