Skip to content

环境与配置管理 ​

“在我电脑上是好的”是开发者最常说的一句话,它背后几乎都是环境和配置的问题:

  • 新服务器上少配了一个环境变量,程序照样启动了,直到半夜才因为连错了数据库出事;
  • 测试环境误用了生产的短信配置,真实用户收到了一堆测试短信;
  • 预发环境测试全部通过,上线后却出了问题,因为两边的依赖版本不同;
  • 某台服务器上有人手动改过一个参数,重新部署后行为变了,没人知道为什么。

环境与配置管理的目标是:同一份代码在每个环境里的行为都可预期、可复现,而且密钥不会泄露。

读完这篇你会

  • 做到“一份构建产物,多个环境只换配置”
  • 在启动时校验配置,缺失或错误时立即失败
  • 管理好密钥、隔离好测试环境,用功能开关降低发布风险

1. 原理:同一部手机,换不同的 SIM 卡 ​

同一款手机,插上不同运营商的 SIM 卡,就能连接不同的网络,手机本身完全不用改。

部署也应该这样:构建产物(代码、镜像)是手机,配置是 SIM 卡。开发、测试、预发、生产使用同一份构建产物,只是注入不同的配置。

text
          同一份构建产物(镜像 / dist 包)
         /            |              \
   开发环境配置    预发环境配置     生产环境配置

如果每个环境都单独构建、或者配置写死在代码里,那么“预发环境验证通过”就不能说明生产环境没问题,因为它们根本不是同一个东西。

环境用途数据稳定性要求
开发本地开发调试模拟数据或脱敏样本低
测试 / 预发上线前的完整验证脱敏的类生产数据尽量接近生产
生产服务真实用户真实数据最高,所有变更受控

2. 配置和代码分离 ​

数据库地址、第三方服务的 key、功能开关、各种阈值,都不应该写死在代码里,而是通过环境变量或配置中心注入:

js
// ❌ 写死在代码里:换环境要改代码,密钥还进了仓库
const db = connect('mysql://app:secret@db.example.internal/orders')

// ✅ 从环境变量读取
const db = connect(config.databaseUrl)
配置放在哪里
业务参数、阈值、功能开关环境变量或配置中心
数据库、缓存、消息队列的地址环境变量
密码、API 密钥、证书密钥管理服务,运行时注入
和环境无关的常量代码里

3. 坑一:缺了配置,程序照样跑起来了 ​

现象:新部署的服务器上忘了配置 PAYMENT_NOTIFY_URL,程序正常启动,直到用户支付后回调失败才被发现;或者某个超时配置拼错了名字,程序用了 undefined,变成了永不超时。

原因:代码读取 process.env.XXX 时,变量不存在只会得到 undefined,不会报错。问题被推迟到真正用到它的那一刻才暴露,而那往往是在生产环境、在关键流程里。

修复:启动时集中校验所有配置,缺失或格式错误就立即退出,让问题在部署时就暴露:

js
// config.js:所有配置只在这里读取和校验
import { z } from 'zod'

const schema = z.object({
  NODE_ENV: z.enum(['development', 'test', 'production']),
  DATABASE_URL: z.string().url(),
  REDIS_URL: z.string().url(),
  PAYMENT_NOTIFY_URL: z.string().url(),
  REQUEST_TIMEOUT_MS: z.coerce.number().int().positive().default(10_000)
})

const parsed = schema.safeParse(process.env)
if (!parsed.success) {
  console.error('配置错误:', parsed.error.flatten().fieldErrors)
  process.exit(1) // 启动失败,部署流程会立即发现
}

export const config = parsed.data

怎么避免再犯:业务代码里不直接读 process.env,统一从 config 模块获取;仓库里维护一份 .env.example,列出所有需要的配置项(只写占位值)。

4. 坑二:测试环境给真实用户发了短信 ​

现象:测试同学在测试环境跑了一遍批量通知功能,结果几百个真实客户收到了内容为“测试测试”的短信。

原因:测试环境的数据库里有一份从生产导出、没有脱敏的客户手机号,同时短信服务配置的是生产账号。

修复:测试环境必须和真实世界隔离:

  • 第三方服务使用沙箱账号(支付、短信、邮件服务商一般都提供测试环境);
  • 没有沙箱的服务,在测试环境配置白名单,只允许向内部测试号码、测试邮箱发送;
  • 从生产导出到测试环境的数据必须脱敏(手机号、姓名、地址、证件号替换为假数据);
  • 生产环境的密钥永远不出现在测试环境。

5. 坑三:密钥进了代码仓库 ​

现象:代码扫描发现,仓库的历史提交里有数据库密码和支付密钥。

修复:

  1. 立即更换泄露的密钥,这比清理历史更重要(删除文件后,历史提交里依然能看到它,详见 Git 协作与提交边界);
  2. 密钥放进密钥管理服务(云厂商的 Secret Manager、Vault 等)或 CI/CD 平台的加密变量中,运行时注入;
  3. .env 文件加入 .gitignore,提交前和 CI 中运行密钥扫描工具;
  4. 定期轮换密钥,并确保泄露时能快速吊销。

还要特别注意:前端项目的“环境变量”会在构建时被直接写进 JS 文件,任何人都能看到,前端永远不能放密钥,见 构建工具选型。

6. 坑四:预发环境通过了,生产还是出问题 ​

现象:预发环境测试全部通过,上线后立即报错。排查发现,预发环境的 Node.js 版本、某个中间件的版本、甚至一个 Nginx 配置,都和生产不一样。

修复:让预发环境尽可能像生产:

  • 同一份构建产物:预发验证通过的那个镜像或包,原样部署到生产,不要重新构建;
  • 同样的运行环境:用容器镜像固定操作系统、运行时和依赖版本;
  • 同样的配置结构:配置项完全相同,只是值不同。

预发和生产差异越大,预发验证的价值就越低。

7. 坑五:配置漂移,服务器变成了“黑箱” ​

现象:有一台服务器“谁也不敢动”,因为几年前有人手动调过一些参数,没人记得改了什么。需要扩容新机器时,新机器的行为总是和老机器不一样。

原因:手动登录服务器修改配置,这些修改没有被记录在任何地方。时间一长,每台服务器都变得独一无二,这叫配置漂移。

修复:所有环境配置都写成代码(基础设施即代码),提交到仓库,经过评审后由工具自动应用。Nginx 配置、防火墙规则、环境变量、定时任务,都应该能从一份声明文件加一条命令完整重建。禁止手动登录服务器修改配置,紧急修改后也要立即补回代码里。

8. 功能开关:把“部署”和“发布”分开 ​

传统的做法是“代码一上线,功能就生效”,风险集中在部署那一刻。功能开关(Feature Flag)把两件事拆开:

text
代码合并并部署到生产(功能默认关闭)
  → 先对内部员工打开,验证
  → 对 5% 的用户打开,观察错误率和指标
  → 逐步扩大到 100%
  → 出现问题时,关闭开关即可,不需要回滚代码

功能开关的坑在于只加不删:每一个开关都意味着代码里多了一个分支。功能全量上线、稳定一段时间后,要及时删除开关和旧的代码分支,否则代码里会堆满没人敢动的“死分支”。可以在创建开关时就登记负责人和计划删除的时间。

排查思路 ​

  1. 行为和预期不一致:在启动日志里打印实际生效的配置(密钥类字段要脱敏,比如只显示前几位),确认程序拿到的值是什么。
  2. 只在某个环境出问题:对比两个环境的配置项、运行时版本和依赖版本,差异往往就是原因。
  3. 怀疑配置漂移:把服务器上的实际配置和仓库里的声明文件做对比。
  4. 测试环境误触达真实用户:检查第三方服务的配置是否为沙箱账号,测试数据是否脱敏。

检查清单 ​

  • [ ] 所有环境使用同一份构建产物,只有配置不同
  • [ ] 配置在启动时集中校验,缺失或错误时启动失败
  • [ ] 仓库里有 .env.example,没有任何真实密钥
  • [ ] 密钥存放在密钥管理服务中,定期轮换
  • [ ] 测试环境使用沙箱账号和脱敏数据,外发通知有白名单
  • [ ] 环境配置写成代码,禁止手动修改服务器
  • [ ] 功能开关有负责人和计划删除时间

延伸阅读 ​

为复用而记录,为理解而整理。