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:
1 parent
9f7c522157
commit
9a46a955d4
5 files changed
+704
-26
No files matched your search
+338
-26
@@ -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` |
|
||||
@@ -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
|
||||
File renamed without changes.
File renamed without changes.
Executable
+333
@@ -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 "════════════════════════════════════════════════════════"
|
||||
Reference in new issue
Block a user