新增 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 数据运维:定位、热备份、跨节点迁移、卸载保留、常见坑
392 lines
15 KiB
Markdown
392 lines
15 KiB
Markdown
# opsx-loki 部署到 Kubernetes
|
||
|
||
命名空间固定:**`log`**(日志相关资源统一放这里)
|
||
|
||
---
|
||
|
||
## 一条命令部署(推荐)
|
||
|
||
```bash
|
||
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)"
|
||
|
||
# 3) 数据卷 + 平台 + 服务
|
||
kubectl apply -f kubernetes/pvc.yaml
|
||
kubectl apply -f kubernetes/deployment.yaml
|
||
kubectl apply -f kubernetes/service.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 会互相覆盖。
|
||
- DB 写入本身是原子的(临时文件 + fsync + rename),容器被杀不会写坏文件。
|
||
|
||
### 备份:CSI VolumeSnapshot
|
||
|
||
各云都支持同一套 `snapshot.storage.k8s.io` API(AWS EBS / 腾讯 CBS / 华为 EVS / 阿里云 ESSD):
|
||
|
||
```yaml
|
||
apiVersion: snapshot.storage.k8s.io/v1
|
||
kind: VolumeSnapshot
|
||
metadata:
|
||
name: opsxloki-data-20261025
|
||
namespace: log
|
||
spec:
|
||
volumeSnapshotClassName: <集群的 snapshotClass>
|
||
source:
|
||
persistentVolumeClaimName: opsxloki-data
|
||
```
|
||
|
||
---
|
||
|
||
## 多云兼容
|
||
|
||
| 项 | 做法 |
|
||
|---|---|
|
||
| 存储类 | `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`(停旧实例后拷贝);或
|
||
- 保留外部实例做带外管理,集群内实例使用独立数据卷(两套数据)。
|
||
|
||
---
|
||
|
||
## 日志
|
||
|
||
- 平台**只写 stdout**(Go/GIN 默认),不写日志文件;`kubectl logs` 可直接看。
|
||
- Kubernetes 由 kubelet 统一轮转,节点级配置建议:
|
||
|
||
```yaml
|
||
# /var/lib/kubelet/config.yaml
|
||
containerLogMaxSize: 50Mi
|
||
containerLogMaxFiles: 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` |
|