Files
LokiStack/README.md
T
opsx d0b9b6b71d fix(ui): 前端用户可见文案 opsx-loki → lokistack
修正用户能看到/会复制走的旧名(你反馈的 K8s 示例 token 命令在内):

1. 界面标题(4 处)
   App.tsx            浏览器标题栏
   Layout.tsx         侧边栏 logo / 页面标题 fallback / 移动端导航
   login/index.tsx    登录页大标题
   settings/index.tsx 「关于 lokistack」

2. 集群接入示例(用户直接复制到集群执行,5 处)
   ClusterFormDialog.tsx:
     Role / ServiceAccount 名: opsx-loki       → lokistack
     token Secret 名:         opsx-loki-token  → lokistack-token
     取 token 命令:           get secret opsx-loki-token → lokistack-token

3. 文档
   docs/deployment/README.md, host-nginx.md  旧名全量修正
   README.md 迁移章节之外的示例命令修正;
             迁移章节(从 opsx-loki 升级)保留旧名作历史对照

【刻意不改】
  a) localStorage key(9 个)
     opsxloki.auth.{token,refresh-token,user}
     opsxloki.theme.mode / layout.collapsed / chunk-reloaded
     opsxloki.logs.{query-history,range-history}
     原因:改了所有在线用户立即掉登录,主题/折叠/查询历史全丢。
     要改必须配套迁移(参考 main.tsx 中 lokideploy.* → opsxloki.* 的写法)。

  b) 数据文件名 opsxloki.json(后端硬编码,改了读不到 DB)

  c) Go module 路径 opsxloki/...(159 处 import)
     属编译期内部标识,最终用户不可见;改名需全仓重构 + go.mod 变更。
2026-10-26 17:30:30 +00:00

604 lines
18 KiB
Markdown
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.
# lokistack v2 — 安全的 K8s Loki 部署平台
一个轻量级的 **Gin + React + JSON 文件存储** 平台,用于通过 SSH 或 kubeconfig 把 Grafana Loki
日志聚合系统部署到 Kubernetes 集群或裸金属主机上。
> **v2 安全增强**:非 root 容器、运行时强制校验密钥、健康检查、资源限制、日志轮转、优雅关停。
>
> **面向 AI 编码代理**:本仓库的 `CLAUDE.md` 与 `AGENTS.md` 记录了架构约定与常用命令,供 Claude Code、
> Cursor、Aider 等工具参考。两份文件内容一致,修改其一请同步另一份。
## 功能特性
- **K8s 集群管理** — 通过 kubeconfig 添加集群,自动校验连通性
- **SSH 主机管理** — 支持密码或密钥认证
- **存储配置** — 支持 S3、OSS、MinIO、RustFS 等后端
- **一键部署 Loki** — Helm Chart / YAML 清单 / 二进制 三种模式
- **部署日志** — 实时日志流
- **Web 终端** — 在浏览器中执行远程命令
- **单体二进制** — 约 13MB,零外部依赖
- **安全** — Argon2 密码哈希、AES 凭证加密、JWT 鉴权、非 root 容器
---
---
## 从 opsx-loki 升级(资源更名说明)
本版本把所有资源名从 `opsx-loki` / `opsxloki` 统一改为 **`lokistack`**,包括镜像、Deployment、
Service、PVC、Secret、docker-compose 卷名。
### Kubernetes 用户
资源名已变,**不能直接 `kubectl apply` 覆盖**(会新建空 PVC)。按下面顺序迁移,**零数据丢失**:
```bash
# 0) 先备份(数据只有几十 KB)
kubectl -n log exec deploy/opsx-loki -- tar czf - -C /app/data . > /tmp/data.tar.gz
kubectl -n log get secret opsxloki-secrets -o yaml > /tmp/secret-old.yaml
kubectl -n log get pvc opsxloki-data -o yaml > /tmp/pvc-old.yaml
# 1) 停旧服务(Recreate + RWO,必须停)
kubectl -n log scale deploy opsx-loki --replicas=0
kubectl -n log wait --for=delete pod -l app=opsx-loki --timeout=120s
# 2) 复制 Secret(值不变,密钥才不会失效)
kubectl -n log get secret opsxloki-secrets -o yaml \
| sed 's/name: opsxloki-secrets/name: lokistack-secrets/' \
| kubectl apply -f -
# 3) 只建新 PVC
kubectl apply -f kubernetes/pvc.yaml
kubectl -n log wait --for=jsonpath='{.status.phase}'=Bound pvc/lokistack-data --timeout=180s
# 4) 恢复数据到新 PVC(起临时 Pod 挂载)
kubectl -n log run restore --image=alpine:3.20 --restart=Never \
--overrides='{"spec":{"containers":[{"name":"r","image":"alpine:3.20","command":["sleep","600"],"volumeMounts":[{"name":"d","mountPath":"/data"}]}],"volumes":[{"name":"d","persistentVolumeClaim":{"claimName":"lokistack-data"}}]}}'
kubectl -n log wait --for=condition=Ready pod/restore --timeout=120s
kubectl -n log cp /tmp/data.tar.gz restore:/tmp/
kubectl -n log exec restore -- sh -c 'cd /data && tar xzf /tmp/data.tar.gz && chown -R 1000:1000 /data'
kubectl -n log delete po restore
# 5) 启动新服务
kubectl apply -f kubernetes/deployment.yaml -f kubernetes/service.yaml -f kubernetes/service-nodeport.yaml
kubectl -n log rollout status deploy/lokistack --timeout=240s
# 6) 若有 Ingress,改后端(TKE qcloud 要 NodePort)
kubectl -n log patch ingress <你的ingress名> --type=json \
-p='[{"op":"replace","path":"/spec/rules/0/http/paths/0/backend/service/name","value":"lokistack-nodeport"}]'
# 7) 确认服务正常后再删旧资源
kubectl -n log delete deploy opsx-loki
kubectl -n log delete svc opsx-loki opsx-loki-nodeport
# 旧 PVC / Secret 建议保留 7 天做回滚兜底
# kubectl -n log delete pvc opsxloki-data
# kubectl -n log delete secret opsxloki-secrets
```
### Docker Compose 用户
卷名 `opsxloki-data` → `lokistack-data`。**旧数据不会自动迁移**,需要手动拷:
```bash
# 停服务
docker compose down
# 创建新卷并迁移数据
docker volume create lokistack-data
docker run --rm -v opsxloki-data:/from -v lokistack-data:/to alpine \
sh -c 'cp -av /from/. /to/ && chown -R 1000:1000 /to'
# 重新启动(会用新卷)
docker compose up -d
# 验证数据在
docker compose exec lokistack cat /app/data/opsxloki.json | head -c 100
```
> **注意**:数据文件名仍叫 `opsxloki.json`(代码里硬编码,不改),只是目录/卷名变了。
---
## 快速开始
### 前置条件
- Docker Engine 24.0+ 与 Docker Compose v2+
- 宿主机上装有 `kubectl` 和 `helm`(用于向外部集群部署 K8s 资源)
- `openssl`(用于生成密钥)
### 第 1 步:生成密钥(必需)
启动前必须生成所需的安全密钥:
```bash
# 必需:JWT 签名密钥(建议 >= 32 字符)
export JWT_SECRET=$(openssl rand -hex 32)
# 必需:用于凭证存储的 AES 加密密钥(必须 >= 32 字节)
export ENCRYPTION_KEY=$(openssl rand -hex 32)
# 可选:跨会话持久化
echo "export JWT_SECRET=$JWT_SECRET" >> ~/.bashrc
echo "export ENCRYPTION_KEY=$ENCRYPTION_KEY" >> ~/.bashrc
```
> **警告**:如果未设置 `JWT_SECRET` 或 `ENCRYPTION_KEY`,容器将 **拒绝启动**,以避免不安全的默认部署。
### 第 2 步:用 Docker Compose 启动(推荐)
```bash
git clone https://github.com/your/lokistack.git
cd lokistack
# 设置密钥(见第 1 步)
export JWT_SECRET=$(openssl rand -hex 32)
export ENCRYPTION_KEY=$(openssl rand -hex 32)
# 启动
docker compose up -d
# 查看健康状态
docker compose ps
docker compose logs -f
# 打开 http://localhost:8080
# 管理员密码见「第 4 步」(首次启动随机生成,不再是固定 admin123)
```
### 第 3 步:验证健康状态
```bash
# 容器级健康检查(内部)
docker inspect --format='{{.State.Health.Status}}' lokistack
# 期望输出:"healthy"
# 手动检查
curl http://localhost:8080/index.html
```
### 第 4 步:获取管理员初始密码
首次启动会生成**随机管理员密码**(不再固定 `admin123`),同时:
- 打印到启动日志(**仅首次**)
- 写入数据卷 `/app/data/initial-admin-password.txt`(权限 `0600`)
**任选一种方式获取**(推荐方式 1 —— 日志会轮转丢失):
```bash
# 方式 1(推荐):从容器内读文件 —— 日志轮转后依然可查
docker compose exec lokistack cat /app/data/initial-admin-password.txt
# 方式 2:首次启动日志(只打印一次,重启后不会再有)
docker compose logs lokistack | grep -A6 "已生成管理员账号"
# 方式 3:容器已停止时也能用(不要求 running)
docker cp lokistack:/app/data/initial-admin-password.txt ./
# 方式 4:直接读 named volume(宿主机 root)
sudo cat /var/lib/docker/volumes/lokistack_lokistack-data/_data/initial-admin-password.txt
```
> ⚠️ **为什么不能只靠日志**:`docker logs` 只在**首次启动**打印一次;日志轮转或容器重启后,
> 数据库里已有用户,**不会再打印**。所以**文件才是可靠来源**。
>
> 💡 容器名说明:本 compose 指定了 `container_name: lokistack`,所以可直接用 `lokistack`。
> 若你的部署没有指定,用服务名形式更通用:`docker compose exec lokistack cat ...`
**Kubernetes 环境(同一个文件)**:
```bash
# 读文件(推荐)
kubectl -n log exec deploy/lokistack -- cat /app/data/initial-admin-password.txt
# 首次启动日志
kubectl -n log logs deploy/lokistack | grep -A6 "已生成管理员账号"
# 拷到本地
kubectl -n log cp <pod-name>:/app/data/initial-admin-password.txt ./
```
**想自己指定密码(跳过随机生成)**:
```bash
# Docker Compose:部署前设置环境变量
export ADMIN_INIT_PASSWORD='your-strong-password'
docker compose up -d
# Kubernetes:在 deployment 的 env 中加入 ADMIN_INIT_PASSWORD
```
**安全说明**:
| 项 | 说明 |
|----|------|
| 密码文件权限 | `0600`,属主 `lokistack` |
| 自动清理 | **admin 首次修改密码后,该文件自动删除**(见 `seed.CleanupInitPassword`) |
| 日志内容 | 仅在首次启动打印,且容器日志由运行时轮转 |
| 忘了密码 | 数据卷里文件仍在;若文件已删,用 `ADMIN_INIT_PASSWORD` 重新初始化(需清空数据卷) |
| 环境变量优先 | 设置了 `ADMIN_INIT_PASSWORD` 时不生成随机密码、不写文件 |
---
## 生产部署
### 方式 A:带密钥文件的 Docker Compose(推荐)
```bash
# 创建密钥文件
mkdir -p secrets
openssl rand -hex 32 > secrets/jwt_secret.txt
openssl rand -hex 32 > secrets/encryption_key.txt
chmod 600 secrets/*.txt
# 使用 docker-compose.prod.yml
docker compose -f docker-compose.prod.yml up -d
```
### 方式 B:Docker Swarm
```bash
# 初始化 swarm(如果尚未初始化)
docker swarm init
# 创建 secrets
echo "$(openssl rand -hex 32)" | docker secret create lokistack_jwt -
echo "$(openssl rand -hex 32)" | docker secret create lokistack_key -
# 部署
docker stack deploy -c docker-compose.prod.yml lokistack
```
### 安全清单
- [ ] `JWT_SECRET` 至少 32 字符且为密码学随机
- [ ] `ENCRYPTION_KEY` 至少 32 字节且为密码学随机
- [ ] 密钥存放在 Docker Secrets 或外部保险柜中(而非 env 文件)
- [ ] `.env` 与 `secrets/` 已加入 `.gitignore`
- [ ] 启用了 `no-new-privileges:true`(docker-compose 默认启用)
- [ ] kubeconfig 以只读方式挂载(`:ro`)
- [ ] SSH 密钥以只读方式挂载(`:ro`)
- [ ] 已合理配置资源限制
- [ ] 健康检查通过
- [ ] 已配置日志轮转
---
## 从源码构建
```bash
# 构建前端 + 后端
bash build.sh
# 用本地脚本启动(开发)
./run.sh
```
### 开发模式(热重载)
```bash
# 终端 1 — 后端
cd backend
go run . # 监听 :8080
# 终端 2 — 前端
cd frontend
npm install
npm run dev # 打开 http://localhost:3000,API 代理到 localhost:8080
```
> 直接 `go run` 启动后端前,必须先 `export JWT_SECRET` 与 `ENCRYPTION_KEY`,否则
> `backend/config/config.go` 会 `log.Fatal` 拒绝启动。`./run.sh` 会自动注入开发用默认值。
### 测试与 lint
```bash
cd backend && go test ./... # 所有 Go 测试(utils、repository)
cd backend && go test ./utils/ -run TestName -v # 运行单个测试
cd frontend && npm run lint # ESLint
```
---
## 架构
```
前端(React + TypeScript + Tailwind CSS) [开发 :3000 / 生产 :8080]
|
| HTTP/REST + JWT Bearer Token
v
后端(Go + Gin) [:8080]
|
+---> JSON 文件数据库(data/opsxloki.json(兼容加载旧 lokideploy.json))
+---> K8s CLI(kubectl / helm)
+---> SSH 客户端(golang.org/x/crypto/ssh)
+---> Loki 部署器(Helm Chart / YAML / Binary)
```
### 后端分层
请求流转:**router → middleware(JWT)→ controller → service → repository(JsonDB)**
- `config/` — `GetConfig()` 单例,读取环境变量,密钥缺失或长度不足时直接 `log.Fatal`。
- `routers/router.go` — 所有路由注册在 `/api/v1`;注册 SPA 静态资源并用 `NoRoute` 回退到
`index.html` 支持前端路由。
- `controllers/` — 薄 Gin handler,每资源一个文件;绑定/校验入参、调用 service、用
`models.SanitizedJSON(...)` 包装返回。
- `services/` — 业务逻辑。部署流水线:
`DeploymentService.Deploy()` → `LokiDeployer.Deploy()`,按模式(`k8s-helm` / `k8s-yaml` /
`host-binary`)分发到 `kubectl`/`helm` 或 SSH。另有 `K8sClient`、`ssh`、`storage`、
`cluster_info` 与较新的 **collector(日志采集器)** 模块。
- `middleware/jwt.go` — `JWTAuth()`、`AdminAuth()`。
- `utils/` — `response.go`(响应封装)、`crypto.go`(Argon2 + AES-GCM)、`jwt.go`。
- `seed/` — 数据库为空时写入默认 admin 用户。
### 数据层 — JSON 文件数据库
持久化是 **单个 JSON 文件** `$DATA_DIR/opsxloki.json(兼容加载旧 lokideploy.json)`(默认 `./data/`),**没有 SQL 数据库**。
`repository.JsonDB` 是与文件对应的内存结构:
- 写操作由 `Lock()/RLock()` 保护,`RequestSave()` 触发 **防抖(500ms)原子写**(临时文件 →
`fsync` → rename),由单一串行化的 `saveWorker` goroutine 执行。常规写入用 `RequestSave()`,
不要在 worker 可能运行时直接 `Save()`。
- 优雅关停顺序很关键:停防抖定时器 → 关闭 worker 并等待 → 最后一次 `doSave`。顺序颠倒会丢更新。
- 因此应用 **只能单实例运行**,不可水平扩展。
- `NextID(table)` 为各集合分配单调递增的整数 ID。
> 注:`repository/db.go` 是当前在用的实现;`models/db.go` 是正在迁移、尚未清理的旧副本(仍会编译,
> 但已无人调用)。新增数据库代码请写到 `repository`。
### 响应封装与脱敏
- 所有 API 响应为 `{ code, message, data }`,`code == 0` 表示成功。
- **不要直接返回原始 model**,必须用 `models.SanitizedJSON(...)` 包装以剔除密码、kubeconfig、
私钥、token 等敏感字段。
### 凭证安全
- 密码用 **Argon2** 哈希;凭证用 **AES-GCM** 加密,AES 密钥由 `ENCRYPTION_KEY` 经 **HKDF** 派生。
- `ENCRYPTION_KEY` 必须 **>= 32 字节**(早期文档写 16,现在会导致启动失败)。
### 容器安全模型
```
+-------------------+ +---------------------+
| 宿主内核 | | 宿主文件系统 |
| | | ~/.kube (只读) |
| +-------------+ | | ~/.ssh (只读) |
| | 容器 | | | ./data (读写) |
| | (lokistack | | +---------------------+
| | UID:1000) | |
| | no-new-privs| |
| | cap_drop:ALL| |
| +-------------+ |
+-------------------+
```
---
## API 接口
Base URL:`/api/v1`
| 方法 | 接口 | 鉴权 | 说明 |
|------|------|------|------|
| POST | `/auth/login` | 否 | 登录,获取 JWT token |
| GET | `/auth/user` | 是 | 当前用户信息 |
| POST | `/auth/password` | 是 | 修改密码 |
| GET/POST | `/clusters` | 是 | 集群增删改查 |
| POST | `/clusters/:id/validate` | 是 | 校验集群连通性 |
| GET/POST | `/hosts` | 是 | 主机增删改查 |
| POST | `/hosts/:id/execute` | 是 | 远程执行命令 |
| GET/POST | `/storages` | 是 | 存储配置增删改查 |
| GET/POST | `/deployments` | 是 | 部署增删改查 |
| POST | `/deployments/:id/deploy` | 是 | 启动部署 |
| POST | `/deployments/:id/stop` | 是 | 停止部署 |
| GET | `/deployments/:id/logs` | 是 | 部署日志 |
| GET | `/deployments/stats` | 是 | 统计信息 |
### 健康检查
容器对静态文件路径暴露健康检查:
```bash
# 容器级(内部)
wget --spider http://localhost:8080/index.html
# 宿主级
curl http://localhost:8080/index.html
# Docker inspect
docker inspect --format='{{.State.Health}}' lokistack
```
健康检查参数:
- **间隔**:30 秒
- **超时**:5 秒
- **启动宽限期**:15 秒(允许启动)
- **重试次数**:3
---
## 部署模式
| 模式 | 目标 | 说明 |
|------|------|------|
| `k8s-helm` | K8s 集群 | 官方 Grafana Helm Chart |
| `k8s-yaml` | K8s 集群 | 原生 Kubernetes 清单 |
| `host-binary` | Linux 主机 | 通过 SSH 部署二进制 + systemd |
## 存储后端
| 类型 | Endpoint 示例 |
|------|---------------|
| S3 | `s3.amazonaws.com` |
| OSS | `oss-cn-hangzhou.aliyuncs.com` |
| MinIO | `http://minio:9000` |
| RustFS | `http://rustfs:8080` |
---
## 环境变量
### 必需
| 变量 | 示例 | 说明 |
|------|------|------|
| `JWT_SECRET` | `$(openssl rand -hex 32)` | JWT 签名密钥(**必需**,>= 32 字符) |
| `ENCRYPTION_KEY` | `$(openssl rand -hex 32)` | AES 凭证加密密钥(**必需**,>= 32 字节) |
### 可选
| 变量 | 默认值 | 说明 |
|------|--------|------|
| `SERVER_PORT` | `8080` | HTTP 服务端口 |
| `DATA_DIR` | `/app/data` | 数据存储目录 |
| `KUBECONFIG` | `/home/lokideploy/.kube/config` | kubeconfig 文件路径 |
| `KUBECONFIG_PATH` | `~/.kube` | 宿主机上 kubeconfig 的挂载源路径 |
| `SSH_KEY_PATH` | `~/.ssh` | 宿主机上 SSH 密钥的挂载源路径 |
### Docker Secrets 支持
entrypoint 支持基于文件的 Docker Secrets 注入:
| 变量 | 文件变量 | 说明 |
|------|----------|------|
| `JWT_SECRET` | `JWT_SECRET_FILE` | 含 JWT 密钥的 secret 文件路径 |
| `ENCRYPTION_KEY` | `ENCRYPTION_KEY_FILE` | 含加密密钥的 secret 文件路径 |
配合 Docker Compose secrets 的示例:
```yaml
services:
lokideploy:
environment:
JWT_SECRET_FILE: /run/secrets/jwt_secret
ENCRYPTION_KEY_FILE: /run/secrets/encryption_key
secrets:
- jwt_secret
- encryption_key
secrets:
jwt_secret:
file: ./secrets/jwt_secret.txt
encryption_key:
file: ./secrets/encryption_key.txt
```
---
## 安全
### 鉴权与授权
- **Argon2** 密码哈希
- **AES-GCM** 凭证加密(密钥由 `ENCRYPTION_KEY` 派生)
- **JWT** token 鉴权
- **RBAC** 基于角色的访问控制
- API 响应经过脱敏(不暴露密码、kubeconfig、密钥)
### 容器安全
- **非 root 用户**:`lokideploy:1000`
- **禁止提权**:`no-new-privileges:true`
- **丢弃能力**:除必需外丢弃所有 capabilities
- **只读挂载**:kubeconfig 与 SSH 密钥只读挂载
- **资源限制**:CPU 与内存受限
- **日志轮转**:防止磁盘写满
### 密钥管理最佳实践
1. **绝不** 把密钥提交到版本控制
2. 生产环境使用 **Docker Secrets**(Swarm/Kubernetes)
3. 定期 **轮换密钥**:`openssl rand -hex 32`
4. 企业部署可使用密钥保险柜(HashiCorp Vault、AWS Secrets Manager 等)
5. 设置文件权限:对密钥文件执行 `chmod 600`
---
## 资源配置
### 默认限制(docker-compose.yml)
| 资源 | 上限 | 预留 |
|------|------|------|
| CPU | 2.0 核 | 0.25 核 |
| 内存 | 512 MB | 64 MB |
| PIDs | 100 | - |
### 按规模调整
```yaml
# 更大规模部署(10+ 集群)
deploy:
resources:
limits:
cpus: '4.0'
memory: 1G
reservations:
cpus: '1.0'
memory: 256M
```
---
## 故障排查
### 容器立即退出
```bash
# 检查密钥是否已设置
docker compose logs
# 期望错误信息:"Missing required security environment variables"
# 修复:
export JWT_SECRET=$(openssl rand -hex 32)
export ENCRYPTION_KEY=$(openssl rand -hex 32)
docker compose up -d
```
### 健康检查失败
```bash
# 查看容器状态
docker inspect --format='{{.State.Health}}' lokistack
# 查看日志
docker compose logs --tail=50
```
### kubeconfig 不可访问
```bash
# 确保宿主侧权限正确
chmod 600 ~/.kube/config
# 核对挂载路径
docker exec lokideploy ls -la /home/lokideploy/.kube/
```
---
## 许可证
MIT License
---
## 更新日志
### v2.0.0
- **安全**:非 root 容器用户(`lokideploy:1000`)
- **安全**:运行时强制校验 `JWT_SECRET`/`ENCRYPTION_KEY`
- **安全**:`no-new-privileges` 与 capabilities 丢弃
- **安全**:kubectl/helm 的 SHA256 校验和验证
- **运维**:健康检查端点
- **运维**:资源限制与预留
- **运维**:日志轮转(json-file 驱动)
- **运维**:优雅关停(SIGTERM)
- **运维**:Docker Secrets 支持
- **运维**:带 nginx 负载均衡的生产 compose 文件