【已在线完成,零数据丢失】
K8s ns=log 资源全部改名:
Deployment opsx-loki → lokistack
Service opsx-loki → lokistack
Service opsx-loki-nodeport → lokistack-nodeport
PVC opsxloki-data → lokistack-data
Secret opsxloki-secrets → lokistack-secrets
Label app: opsx-loki → app: lokistack
Ingress log.astx.club 后端 → lokistack-nodeport:8080
镜像 lghost/opsxloki → lghost/lokistack
docker-compose 卷/容器名同步更名
【保留不变(改了会出事)】
namespace: log
数据文件名 opsxloki.json(代码硬编码)
容器内路径 /app/data
【迁移过程】
1. 备份 PVC 数据(32KB) + Secret 原值 + 旧 yaml → /root/lokistack-migration-backup-20261026-171141
2. scale 旧 Deployment → 0,等 Pod 消失
3. 复制 Secret(JWT sha256 校验一致)
4. 新建 PVC lokistack-data(Bound 30Gi cbs)
5. 临时 Pod 恢复数据(chown 1000:1000,opsxloki.json 保留 0600)
6. apply 新 Deployment → 1/1 Running
7. 修复 Ingress 后端(TKE qcloud 要求 NodePort)→ CLB 恢复 200
8. 删除旧 Deployment / 失效 Service
【验证】
lokistack 1/1,0 重启,镜像 lghost/lokistack:latest(statically linked)
/api/v1/health → {"status":"ok","version":"2.0.0"}
/app/data/opsxloki.json 610B 完整(admin 用户 + 配置)
http://log.astx.club/ → HTTP 200
镜像加固:admin123=0、/app/backend/=0、initial-admin-password.txt 存在
【回滚兜底(建议 7 天后删)】
pvc/opsxloki-data、secret/opsxloki-secrets
【文档】
README 新增「从 opsx-loki 升级」章节:
K8s 七步迁移脚本 + docker-compose 卷迁移命令
18 KiB
opsx-loki 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)。按下面顺序迁移,零数据丢失:
# 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。旧数据不会自动迁移,需要手动拷:
# 停服务
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 步:生成密钥(必需)
启动前必须生成所需的安全密钥:
# 必需: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 启动(推荐)
git clone https://github.com/your/opsx-loki.git
cd opsx-loki
# 设置密钥(见第 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 步:验证健康状态
# 容器级健康检查(内部)
docker inspect --format='{{.State.Health.Status}}' opsxloki
# 期望输出:"healthy"
# 手动检查
curl http://localhost:8080/index.html
第 4 步:获取管理员初始密码
首次启动会生成随机管理员密码(不再固定 admin123),同时:
- 打印到启动日志(仅首次)
- 写入数据卷
/app/data/initial-admin-password.txt(权限0600)
任选一种方式获取(推荐方式 1 —— 日志会轮转丢失):
# 方式 1(推荐):从容器内读文件 —— 日志轮转后依然可查
docker compose exec opsxloki cat /app/data/initial-admin-password.txt
# 方式 2:首次启动日志(只打印一次,重启后不会再有)
docker compose logs opsxloki | grep -A6 "已生成管理员账号"
# 方式 3:容器已停止时也能用(不要求 running)
docker cp opsxloki:/app/data/initial-admin-password.txt ./
# 方式 4:直接读 named volume(宿主机 root)
sudo cat /var/lib/docker/volumes/opsx-loki_opsxloki-data/_data/initial-admin-password.txt
⚠️ 为什么不能只靠日志:
docker logs只在首次启动打印一次;日志轮转或容器重启后, 数据库里已有用户,不会再打印。所以文件才是可靠来源。💡 容器名说明:本 compose 指定了
container_name: opsxloki,所以可直接用opsxloki。 若你的部署没有指定,用服务名形式更通用:docker compose exec opsxloki cat ...
Kubernetes 环境(同一个文件):
# 读文件(推荐)
kubectl -n log exec deploy/opsx-loki -- cat /app/data/initial-admin-password.txt
# 首次启动日志
kubectl -n log logs deploy/opsx-loki | grep -A6 "已生成管理员账号"
# 拷到本地
kubectl -n log cp <pod-name>:/app/data/initial-admin-password.txt ./
想自己指定密码(跳过随机生成):
# Docker Compose:部署前设置环境变量
export ADMIN_INIT_PASSWORD='your-strong-password'
docker compose up -d
# Kubernetes:在 deployment 的 env 中加入 ADMIN_INIT_PASSWORD
安全说明:
| 项 | 说明 |
|---|---|
| 密码文件权限 | 0600,属主 opsxloki |
| 自动清理 | admin 首次修改密码后,该文件自动删除(见 seed.CleanupInitPassword) |
| 日志内容 | 仅在首次启动打印,且容器日志由运行时轮转 |
| 忘了密码 | 数据卷里文件仍在;若文件已删,用 ADMIN_INIT_PASSWORD 重新初始化(需清空数据卷) |
| 环境变量优先 | 设置了 ADMIN_INIT_PASSWORD 时不生成随机密码、不写文件 |
生产部署
方式 A:带密钥文件的 Docker Compose(推荐)
# 创建密钥文件
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
# 初始化 swarm(如果尚未初始化)
docker swarm init
# 创建 secrets
echo "$(openssl rand -hex 32)" | docker secret create opsxloki_jwt -
echo "$(openssl rand -hex 32)" | docker secret create opsxloki_key -
# 部署
docker stack deploy -c docker-compose.prod.yml opsxloki
安全清单
JWT_SECRET至少 32 字符且为密码学随机ENCRYPTION_KEY至少 32 字节且为密码学随机- 密钥存放在 Docker Secrets 或外部保险柜中(而非 env 文件)
.env与secrets/已加入.gitignore- 启用了
no-new-privileges:true(docker-compose 默认启用) - kubeconfig 以只读方式挂载(
:ro) - SSH 密钥以只读方式挂载(
:ro) - 已合理配置资源限制
- 健康检查通过
- 已配置日志轮转
从源码构建
# 构建前端 + 后端
bash build.sh
# 用本地脚本启动(开发)
./run.sh
开发模式(热重载)
# 终端 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
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),由单一串行化的saveWorkergoroutine 执行。常规写入用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 (读写) |
| | (opsxloki | | +---------------------+
| | 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 |
是 | 统计信息 |
健康检查
容器对静态文件路径暴露健康检查:
# 容器级(内部)
wget --spider http://localhost:8080/index.html
# 宿主级
curl http://localhost:8080/index.html
# Docker inspect
docker inspect --format='{{.State.Health}}' opsxloki
健康检查参数:
- 间隔: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 的示例:
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 与内存受限
- 日志轮转:防止磁盘写满
密钥管理最佳实践
- 绝不 把密钥提交到版本控制
- 生产环境使用 Docker Secrets(Swarm/Kubernetes)
- 定期 轮换密钥:
openssl rand -hex 32 - 企业部署可使用密钥保险柜(HashiCorp Vault、AWS Secrets Manager 等)
- 设置文件权限:对密钥文件执行
chmod 600
资源配置
默认限制(docker-compose.yml)
| 资源 | 上限 | 预留 |
|---|---|---|
| CPU | 2.0 核 | 0.25 核 |
| 内存 | 512 MB | 64 MB |
| PIDs | 100 | - |
按规模调整
# 更大规模部署(10+ 集群)
deploy:
resources:
limits:
cpus: '4.0'
memory: 1G
reservations:
cpus: '1.0'
memory: 256M
故障排查
容器立即退出
# 检查密钥是否已设置
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
健康检查失败
# 查看容器状态
docker inspect --format='{{.State.Health}}' opsxloki
# 查看日志
docker compose logs --tail=50
kubeconfig 不可访问
# 确保宿主侧权限正确
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 文件