关于 twelve-factor
为 SaaS 和其他进程型应用提供 Twelve-Factor 实践指导,涵盖全部 12 项因素,并在按需资源中提供 Node/TypeScript 实现示例。
主要功能
- 01覆盖全部 12 项因素
- 02配置与依赖管理实践
- 03后端服务与无状态进程模式
- 04并发与优雅关闭指导
- 05结构化进程流日志
- 06resources/ 中的 Node/TypeScript 示例
适用场景
- 01评估新的 SaaS 应用
- 02改进旧服务的配置管理
- 03拆分 Web 与 Worker 进程
- 04添加优雅关闭和健康检查
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ citypaul/ .dotfiles --skill twelve-factor安装后在 Claude Code 中输入/twelve-factor确认已加载。
附带文件resources/node-patterns.md
详细介绍
十二要素应用模式
原始方法论面向软件即服务应用。它的原则也可为其他可部署程序提供参考,但端口绑定、并发性、进程可处置性、运行时配置和制品一致性并不能直接套用于所有前端、无服务器函数或 CLI。
内容基于 12factor.net。下文涵盖全部 12 个要素:规则、反模式和代码层面的影响都在这里;Node/TypeScript 实现示例位于 resources/。
请参阅 typescript-strict 技能,了解信任边界处以架构为先的模式。请参阅 testing 技能,了解如何通过 TDD 实践这些模式——配置验证、关闭行为和后端服务集成都可以通过行为驱动测试进行测试。
深入学习资源位于 resources/ 目录中。按需加载:
| 资源 | 加载时机…… |
|---|---|
node-patterns.md | 在 Node/TypeScript 中实现任何要素时——配置架构验证、选项对象注入、.env.example、系统依赖检查、后端服务工厂、Redis 会话存储、Web/worker 入口点、健康检查、优雅关闭、结构化日志记录器、管理脚本 |
适用时机
- 绿地 SaaS/进程应用:评估全部十二个要素,并采用适合运行时和平台的要素。
- 棕地应用:根据观察到的风险逐步改进。常见的起始顺序如下:
- 配置(要素 III)——添加环境变量验证,不必重构
- 日志(要素 XI)——切换为结构化的进程流日志记录
- 可处置性(要素 IX)——添加优雅关闭处理程序
- 后端服务(要素 IV)——注入适合平台的资源绑定
- 无状态进程(要素 VI)——将内存状态迁移到后端服务
代码库(要素 I)
一个由版本控制追踪的代码库,多次部署。代码库可以是一个仓库,也可以是 monorepo 中明确归属的部分;不要仅仅为了符合这个标签而拆分仓库。通过明确归属的包或模块共享代码,而不是复制粘贴。
在 monorepo 中,每项服务都应有自己的入口点、部署流水线和后端服务连接集合。使用一个仓库没有问题,只要每项服务都能独立部署。
配置(Factor III)
将随部署变化的配置存储在构建产物之外。环境变量是十二要素应用的默认方式;如果运行时有要求,则使用等效的平台原生密钥或配置注入机制。切勿将凭据或特定环境的端点硬编码。
规则:
- 启动时使用 schema 验证配置——如果配置无效,应快速失败(以非零状态退出,并提供清晰的错误信息)
- 通过选项对象注入配置——切勿在调用链深处导入
process.env - 引入或更改配置模块时,应在同一次更改中将应用中所有
process.env读取都移入该模块,包括报告的问题从未提及的设置,例如日志级别、输出路径或功能开关。如果配置模块仍将其他环境变量读取分散在请求处理程序、领域模块和启动代码中,那只是转移了配置漂移,并未消除它。完成后,应在源代码中搜索process.env,确认仅配置模块和调用它的进程入口点包含匹配项。 - 为运维人员记录所需配置,而不只是写在代码中。如果环境变量是配置接口,应在同一次 schema 更改中编写或更新
.env.example:schema 要求的每项设置各占一行,仅使用占位值,绝不使用真实凭据,也绝不提交.env。验证 schema 会在启动时告知进程哪里出错,却不会告诉用户该向容器传入什么——两者都要提供。如果平台自己的清单或密钥声明格式是配置接口,则按照相同规则更新该文件。
有关 Zod 配置 schema、选项对象注入和 .env.example 示例,请参阅 resources/node-patterns.md。
配置反模式
const DB_HOST = 'prod-db.internal.example.com';
if (process.env.NODE_ENV === 'production') {
connectTo('prod-db');
} else {
connectTo('localhost');
}
const config = require(`./config.${process.env.NODE_ENV}.json`);
这些做法为什么不对: 随部署变化的配置应置于源代码和构建产物之外,通过运行时支持的注入机制提供。按环境名称进行分支会导致组合爆炸,并破坏开发环境与生产环境的一致性。
修复方法: 明确分支实际判定的事项,并将其作为独立的、经过验证的设置,例如 SECURE_COOKIES=true、DATABASE_URL=...,然后删除该分支。检查你修改的文件,删除其中所有环境名称比较,而不只是针对所要求设置的比较:两个部署只要收到相同的设置,就必须表现一致;只要还残留一个 NODE_ENV 比较,就会破坏这一点。在总结中指出此类分支,却不删除它,只会让配置漂移继续存在——应先修复,再说明已完成。
依赖项(Factor II)
显式声明所有依赖项。切勿依赖隐式的系统级软件包。
规则:
- 在
package.json(或等效清单)中声明每项依赖 - 将锁文件(
package-lock.json、pnpm-lock.yaml)提交到代码库 - 隔离依赖项——应用不得从系统环境泄漏,也不得依赖系统环境(使用
node_modules,而非全局安装) - 通过子进程调用的系统工具属于明确的运行时依赖:记录这些工具,并在实际可行时注明版本;在启动或构建时检查它们
后端服务(Factor IV)
将每个后端服务(数据库、缓存、队列、电子邮件、存储)视为附加资源,在组合时通过注入的配置提供。URL 很常见,但平台绑定、套接字路径、资源名称、句柄和结构化凭据,只要符合平台契约,也都是有效形式。
领域代码不区分本地服务和第三方服务。 替换为等效的服务提供方时,只需更改注入的资源配置,不应影响领域行为;若服务提供方使用不同协议,也可能需要不同的适配器。有关通过一个常见示例工厂接入基于 URL 的后端服务,请参阅 resources/node-patterns.md。
对于采用六边形架构的项目,后端服务可以自然地映射到端口(接口)和适配器(实现)。请参阅 hexagonal-architecture 技能。
无状态进程(Factor VI)
以无状态、互不共享的进程运行应用。任何必须持久化的数据都应存储在后端服务中。有关由 Redis 支持的会话存储,请参阅 resources/node-patterns.md。
无状态反模式
const sessions = new Map<string, UserSession>();
app.post('/upload', (req, res) => {
fs.writeFileSync(`/tmp/uploads/${req.file.name}`, req.file.data);
});
let requestCount = 0;
app.use(() => { requestCount++; });
setInterval(() => sendReport(), 60_000);
**这些做法为何错误:**内存状态会在重启时丢失,其他进程实例也无法看到。无法在进程之间共享本地文件系统状态。进程内调度器会在每个副本中各运行一次,导致任务重复执行,除非另有独立的协调机制介入。应使用后端服务(Redis、S3、数据库),以及外部调度器或明确的分布式协调机制。
有关自然支持无状态性的不可变数据模式,请参阅 functional 技能。
并发(Factor VIII)
通过进程模型横向扩展。设计应用时,应确保工作可在不同进程类型之间拆分。
规则:
- 为每种进程类型(web、worker、scheduler)设置独立的入口点——参阅
resources/node-patterns.md - 将持久化或长时间运行的后台工作移至合适的 worker/队列;如果更简单且能满足延迟和重试要求,则将少量由请求发起的工作内联处理
- 每种进程类型都可独立扩展
- 在平台现有的配置中声明进程类型(
Procfile是一种示例)
web: node dist/web.js
worker: node dist/worker.js
可弃置性(Factor IX)
通过快速启动和优雅关闭,最大限度提高稳健性。有关健康检查路由和完整的优雅关闭实现,请参阅 resources/node-patterns.md。
规则:
- 处理 SIGTERM 和 SIGINT,以实现优雅关闭
- 设置排空超时时间——如果关闭过程卡住,则强制退出
- 等待
server.close()完成,以排空正在处理的连接 - 关闭数据库连接池、Redis 连接和队列消费者
- 关闭失败时以非零代码退出
- 保持快速启动,但不要因此将不安全的初始化失败推迟到首个线上请求中
- 将后台任务设计为可重入/幂等,以便在工作中断后安全重试
- 如果部署平台使用探测机制,则提供其所需的健康检查/就绪状态机制
日志(Factor XI)
将日志视为事件流。将结构化记录写入部署平台捕获的进程流;stdout 是常规应用流,如果日志收集器同时捕获两者,stderr 可用于错误记录或诊断信息。切勿在应用内部将日志路由到文件或存储在文件中。
**无论请求具体要求什么,只有满足以下三点,日志变更才算完成:**记录是结构化的,记录只写入进程流,且处理请求时写入的每条记录都带有该请求的标识符。第三点最常被忽略——无法按单个请求筛选的结构化 stdout 日志,在事故期间仍然派不上用场——因此,在迁移各处调用之前,应在每个处理程序入口处绑定按请求创建的子日志记录器,并在宣告工作完成前检查处理程序是否带有该标识符。
此要点规定日志的传输方式和格式。有关写入流的内容——宽事件 / 规范日志行、追踪、SLO、告警——请参阅 observability 技能。
对于面向互联网的服务器,RFC 6302(BCP 162)规定了最低日志要求:源地址和目标地址及端口、时间戳(最好使用 UTC)以及传输协议。除应用层结构化日志外,还应在服务器/框架层捕获这些信息。
语义要求
对于长时间运行的服务,先遵循以下属性;具体字段则以仓库的可观测性和平台契约为准:
- 结构化输出——日志可由机器解析(优先使用 JSON),而不是自由格式的字符串
- 仅使用进程流——遵循平台规定的 stdout/stderr 契约;应用不会写入日志文件,也不会配置文件传输器
- 适当的严重级别——使用平台识别的级别并对其排序,在发出每条记录前都与阈值进行比较。阈值应来自日志记录器之外:作为工厂的一个选项并提供合理的默认值(以便现有的无参数调用仍能正常工作),或者在构建配置时读取一个部署时设置并注入。无参数工厂会将阈值硬编码,因此无法在不发布代码的情况下,为某次部署启用 debug。
- 上下文数据——日志接受结构化元数据(键值对),而不仅仅是消息字符串
- 包含时间戳——每条日志都包含 ISO 8601 时间戳
- 请求关联——处理请求期间发出的每条记录都带有请求或跟踪标识符。如果收到传入的跟踪标头或请求 ID 标头,就使用其中的标识符;否则为每个请求生成一个标识符。然后将其传递给该请求的每次日志调用,通常是在处理程序入口处绑定一个每请求子日志记录器,并通过它记录日志。“没有收到 ID”不是省略该字段的理由:如果运维人员无法从一小时的日志中筛选出单个请求的记录,就根本无法使用这些日志。平台支持 W3C 跟踪上下文集成时,优先使用该集成。
只要满足这些语义,项目可以使用任何日志记录库(pino、搭配 console transport 的 winston、OpenTelemetry、自定义实现)。如果现有日志记录器缺少级别或结构化数据支持,应对其进行适配以满足这些要求。示例日志记录器实现请参见 resources/node-patterns.md。
日志反模式
import fs from 'fs';
fs.appendFileSync('/var/log/app.log', message);
import winston from 'winston';
const logger = winston.createLogger({
transports: [new winston.transports.File({ filename: 'error.log' })],
});
console.log(`User ${userId} logged in`);
**这些做法为何不正确:**文件传输器意味着应用在路由自己的日志。非结构化的字符串插值会生成无法解析或查询的日志。执行环境(容器编排器、PaaS)会捕获平台规定的进程流,并将其路由到适当的目标。
构建、发布、运行(Factor V)
严格分离构建和运行阶段。配置在发布或运行时注入,绝不嵌入构建中。
对运行时配置型进程应用的代码层面要求:
- 尽量让各环境使用相同的构建产物
- 在发布或运行时注入随部署变化的配置。编译时配置属于不同的契约,对于静态前端可能是必需的
- 发布版本不可变——代码更改需要重新构建,而不是在运行时打补丁
端口绑定(Factor VII)
应用是自包含的,并通过绑定端口来提供服务。
const server = app.listen(config.PORT, () => {
logger.info('Server started', { port: config.PORT });
});
此原则适用于长时间运行的 Web 服务。静态站点、无服务器处理程序,以及有意由平台管理服务器生命周期的服务,应遵循各自的平台契约。
开发/生产环境一致性(Factor X)
在影响行为的特性上,保持开发环境与生产环境相似。如果经过审慎选择并通过契约测试的替代产品能够兼容,具体产品可以有所不同。
规则:
- 对于语义差异会产生影响的集成测试和一致性测试,优先使用与生产环境等效的后端服务
- 使用仓库现有的本地服务机制;容器只是一个选项,并非必需
- 配置架构验证(Factor III)可在启动时捕获不匹配项
管理进程(Factor XII)
使用一次性进程运行管理任务(迁移、数据修复、控制台会话),并使用相同的代码库和配置。迁移脚本示例请参阅 resources/node-patterns.md。
管理脚本与应用代码一起存放在代码库中(例如 scripts/migrate.ts)。它们不是独立工具或临时 Shell 命令。管理进程运行于与应用完全相同的环境中——使用相同的版本、配置和依赖项。
测试 12-Factor 模式
可以通过行为驱动测试验证 12-Factor 模式:
- 配置:测试
createConfig在缺少必需变量时会抛出错误,并返回正确的默认值 - 可处置性:测试关闭进程时会关闭所有连接(为数据库/缓存注入测试替身)
- 后端服务:通过注入配置,测试受支持的资源绑定
- 无状态性:测试请求处理程序不会依赖之前请求的状态
通过选项对象注入配置,可以自然地测试所有这些模式,无需模拟 process.env 或全局状态。工厂模式和行为驱动测试示例请参阅 testing 技能。
检查清单
- 一个由团队负责的代码库支持多个部署;共享代码有明确的负责人
- 运行时配置的进程应用优先在各环境中使用相同的构建产物
- 随部署变化的配置在构建之外注入,并在启动时通过架构进行验证
- 如果配置无效,启动会快速失败,并显示清晰的错误消息
- 面向运维人员的示例会与架构一同记录每项必需设置——环境变量作为接口时使用
.env.example,否则使用平台的清单(仅包含占位符,不含真实凭据,不提交.env) - 清单中明确声明所有依赖项,并提交锁定文件
- 后端服务以符合平台要求的资源定位符、句柄或凭据形式注入;可在组合时替换为等效提供方
- 不保存内存中的会话状态,也不在请求之间保留本地文件系统状态
- 对于扩缩或生命周期需求不同的工作,设有单独的进程类型
- 配置带排空超时的 SIGTERM/SIGINT 处理程序,以实现优雅关闭
- 关闭时关闭数据库连接池和连接
- 实现平台要求的健康检查/就绪探针
- 日志以结构化形式输出到平台捕获的进程流中,不使用文件传输器
- 每条请求范围内的日志记录都携带请求 ID 或跟踪 ID;该 ID 取自入站标头,或在请求未携带 ID 时为其生成
- 长期运行的 Web 服务符合平台的端口绑定约定
- 在差异会产生影响的情况下,对开发和生产环境的后端服务进行一致性测试
- 管理脚本存放在代码库中,并使用相同的配置和依赖项