Files
junhong_cmp_fiber/openspec/changes/archive/2026-01-26-deployment-self-init/design.md
huang 45aa7deb87
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 5m33s
feat: 添加环境变量管理工具和部署配置改版
主要改动:
- 新增交互式环境配置脚本 (scripts/setup-env.sh)
- 新增本地启动快捷脚本 (scripts/run-local.sh)
- 新增环境变量模板文件 (.env.example)
- 部署模式改版:使用嵌入式配置 + 环境变量覆盖
- 添加对象存储功能支持
- 改进 IoT 卡片导入任务
- 优化 OpenAPI 文档生成
- 删除旧的配置文件,改用嵌入式默认配置
2026-01-26 10:28:29 +08:00

282 lines
7.3 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.
# 技术设计:部署自初始化
## Context
### 当前状态
1. **目录创建分散**
- `pkg/storage/s3.go` 在初始化时创建临时目录
- 日志目录依赖 Dockerfile 预创建
- 没有统一的初始化入口
2. **配置管理复杂**
- 4 个外部配置文件:`config.yaml``config.dev.yaml``config.staging.yaml``config.prod.yaml`
- `pkg/config/watcher.go` 实现热重载(开发阶段不需要)
- 必须手动拷贝配置文件才能启动
3. **部署流程繁琐**
- 需要手动创建目录结构
- 需要手动拷贝配置文件
- Docker Compose 挂载 configs 目录
### 约束
- 使用 Go 1.16+ 的 `go:embed` 特性
- 保持 Viper 作为配置解析库
- 容器内以非 root 用户 (appuser:1000) 运行
## Goals / Non-Goals
**Goals:**
- 应用启动时自动创建所有必需目录
- 配置嵌入二进制,无需外部配置文件
- 环境变量作为配置覆盖机制
- 部署流程简化到 1 步
**Non-Goals:**
- 配置热重载(开发阶段移除)
- 多配置文件支持(统一用嵌入默认值 + 环境变量)
- 向后兼容旧的配置方式
## Decisions
### Decision 1: 目录初始化放在应用层
**选择**: 在 `main.go` 启动时调用集中化的目录初始化函数
**备选方案**:
| 方案 | 优点 | 缺点 |
|------|------|------|
| Dockerfile RUN mkdir | 镜像固定 | 不支持运行时配置路径 |
| Entrypoint 脚本 | 运行时动态 | Shell 脚本维护成本高 |
| **应用代码初始化** | 跨环境通用、错误处理清晰 | 无 |
**理由**: 应用代码可以读取配置中的路径,提供 Go 级别的错误处理并在所有部署环境Docker、K8s、裸机通用。
### Decision 2: 配置嵌入 + 环境变量覆盖
**选择**: 使用 `go:embed` 嵌入默认配置,环境变量覆盖
**配置优先级**:
```
环境变量 (JUNHONG_*) > 嵌入默认值
```
**备选方案**:
| 方案 | 优点 | 缺点 |
|------|------|------|
| 纯外部文件 | 运行时可改 | 部署复杂 |
| 纯环境变量 | 12-Factor | 复杂配置难表达 |
| **嵌入 + 环境变量** | 开箱即用 + 灵活覆盖 | 改默认值需重编译 |
**理由**: 嵌入配置保证"开箱即用",环境变量覆盖满足生产环境定制需求。开发阶段不需要频繁改默认值。
### Decision 3: 环境变量命名规范
**选择**: `JUNHONG_` 前缀 + 下划线分隔
**格式**: `JUNHONG_{SECTION}_{KEY}`
**示例**:
```bash
JUNHONG_DATABASE_HOST=localhost
JUNHONG_DATABASE_PORT=5432
JUNHONG_REDIS_ADDRESS=localhost:6379
JUNHONG_SERVER_ADDRESS=:3000
```
**理由**:
- 前缀避免与系统环境变量冲突
- 下划线分隔便于 Viper 的 `SetEnvKeyReplacer` 处理
- 符合业界惯例(类似 `POSTGRES_``REDIS_` 等)
### Decision 4: 删除配置热重载
**选择**: 删除 `pkg/config/watcher.go` 及相关逻辑
**理由**:
- 开发阶段,配置变更频率低
- 重启容器即可应用新配置
- 减少代码复杂度
### Decision 5: 目录降级策略
**选择**: 权限不足时使用系统临时目录作为 fallback
```go
if err := os.MkdirAll(dir, 0755); err != nil {
if os.IsPermission(err) {
fallback := filepath.Join(os.TempDir(), "junhong", filepath.Base(dir))
os.MkdirAll(fallback, 0755)
return fallback, nil
}
return "", err
}
```
**理由**: 提高容错性,即使权限配置不当也能启动(降级运行)。
## Architecture
### 启动流程
```
main.go
├── 1. config.Load() // 加载嵌入配置 + 环境变量覆盖
│ ├── 读取 go:embed 的 defaults/config.yaml
│ ├── 应用 JUNHONG_* 环境变量覆盖
│ └── 验证必填配置
├── 2. bootstrap.EnsureDirectories(cfg) // 创建所有必需目录
│ ├── 临时文件目录
│ ├── 日志目录
│ └── 其他运行时目录
├── 3. logger.Init(cfg) // 初始化日志
├── 4. database.Init(cfg) // 初始化数据库
└── 5. ... 其他组件初始化
```
### 目录结构
```
pkg/
├── bootstrap/
│ └── directories.go # 新增:目录初始化
├── config/
│ ├── config.go # 保留:配置结构定义
│ ├── loader.go # 重写:嵌入配置加载
│ ├── embedded.go # 新增go:embed 逻辑
│ ├── defaults/
│ │ └── config.yaml # 新增:嵌入的默认配置
│ └── watcher.go # 删除
```
### 嵌入配置内容
```yaml
# pkg/config/defaults/config.yaml
server:
address: ":3000"
read_timeout: 30s
write_timeout: 30s
shutdown_timeout: 30s
prefork: false
database:
host: "" # 必须通过 JUNHONG_DATABASE_HOST 设置
port: 5432
user: "" # 必须通过 JUNHONG_DATABASE_USER 设置
password: "" # 必须通过 JUNHONG_DATABASE_PASSWORD 设置
dbname: "" # 必须通过 JUNHONG_DATABASE_DBNAME 设置
sslmode: "disable"
max_open_conns: 25
max_idle_conns: 10
conn_max_lifetime: 5m
redis:
address: "" # 必须通过 JUNHONG_REDIS_ADDRESS 设置
port: 6379
password: ""
db: 0
pool_size: 10
min_idle_conns: 5
dial_timeout: 5s
read_timeout: 3s
write_timeout: 3s
storage:
provider: "s3"
temp_dir: "/tmp/junhong-storage"
s3:
endpoint: ""
region: ""
bucket: ""
access_key_id: ""
secret_access_key: ""
use_ssl: false
path_style: true
presign:
upload_expires: 15m
download_expires: 24h
logging:
level: "info"
development: false
app_log:
filename: "/app/logs/app.log"
max_size: 100
max_backups: 3
max_age: 7
compress: true
access_log:
filename: "/app/logs/access.log"
max_size: 100
max_backups: 3
max_age: 7
compress: true
queue:
concurrency: 10
retry_max: 5
timeout: 10m
jwt:
secret_key: "" # 必须通过 JUNHONG_JWT_SECRET_KEY 设置
token_duration: 24h
access_token_ttl: 24h
refresh_token_ttl: 168h
middleware:
enable_rate_limiter: false
rate_limiter:
max: 100
expiration: 1m
storage: "memory"
```
## Risks / Trade-offs
| 风险 | 影响 | 缓解措施 |
|------|------|----------|
| 嵌入配置修改需重新编译 | 低(开发阶段) | 敏感配置通过环境变量,嵌入的只是默认值 |
| 环境变量泄露 | 中 | 使用 K8s Secrets 或 Docker Secrets 管理 |
| 目录降级后行为不一致 | 低 | 降级时打印 WARN 日志,明确告知 |
| 删除热重载后调试不便 | 低 | 开发时直接重启进程,生产用 rolling update |
## Migration Plan
### 实施步骤
1. **新增目录初始化模块**
- 创建 `pkg/bootstrap/directories.go`
- 在 main.go 中调用
2. **新增配置嵌入模块**
- 创建 `pkg/config/defaults/config.yaml`
- 创建 `pkg/config/embedded.go`
- 重写 `pkg/config/loader.go`
3. **清理旧配置逻辑**
- 删除 `pkg/config/watcher.go`
- 删除 `configs/*.yaml`
4. **更新 Docker 配置**
- 更新 `Dockerfile.api``Dockerfile.worker`
- 重写 `docker-compose.prod.yml`
5. **更新文档**
- 更新 README.md 部署说明
- 更新环境变量文档
### 回滚策略
如需回滚,恢复 git 提交即可(开发阶段无生产数据风险)。
## Open Questions
无。设计已明确,可直接实施。