mcpField
T
twelve-factor

twelve-factor

github.com/citypaul云与 DevOps760212 次安装4 周前更新
代码仓库

关于 twelve-factor

为 SaaS 和其他进程型应用提供 Twelve-Factor 实践指导,涵盖全部 12 项因素,并在按需资源中提供 Node/TypeScript 实现示例。

主要功能

  1. 01覆盖全部 12 项因素
  2. 02配置与依赖管理实践
  3. 03后端服务与无状态进程模式
  4. 04并发与优雅关闭指导
  5. 05结构化进程流日志
  6. 06resources/ 中的 Node/TypeScript 示例

适用场景

  1. 01评估新的 SaaS 应用
  2. 02改进旧服务的配置管理
  3. 03拆分 Web 与 Worker 进程
  4. 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/进程应用:评估全部十二个要素,并采用适合运行时和平台的要素。
  • 棕地应用:根据观察到的风险逐步改进。常见的起始顺序如下:
    1. 配置(要素 III)——添加环境变量验证,不必重构
    2. 日志(要素 XI)——切换为结构化的进程流日志记录
    3. 可处置性(要素 IX)——添加优雅关闭处理程序
    4. 后端服务(要素 IV)——注入适合平台的资源绑定
    5. 无状态进程(要素 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 服务符合平台的端口绑定约定
  • 在差异会产生影响的情况下,对开发和生产环境的后端服务进行一致性测试
  • 管理脚本存放在代码库中,并使用相同的配置和依赖项

评论

支持 Markdown