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

7.3 KiB
Raw Blame History

技术设计:部署自初始化

Context

当前状态

  1. 目录创建分散

    • pkg/storage/s3.go 在初始化时创建临时目录
    • 日志目录依赖 Dockerfile 预创建
    • 没有统一的初始化入口
  2. 配置管理复杂

    • 4 个外部配置文件:config.yamlconfig.dev.yamlconfig.staging.yamlconfig.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}

示例:

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

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          # 删除

嵌入配置内容

# 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.apiDockerfile.worker
    • 重写 docker-compose.prod.yml
  5. 更新文档

    • 更新 README.md 部署说明
    • 更新环境变量文档

回滚策略

如需回滚,恢复 git 提交即可(开发阶段无生产数据风险)。

Open Questions

无。设计已明确,可直接实施。