twelve-factor について
SaaSやその他のプロセス型アプリケーションにTwelve-Factorの原則を適用するためのスキルです。全12要素を扱い、Node/TypeScriptの実装例は必要に応じて参照できます。
主な機能
- 01全12要素のガイダンス
- 02設定と依存関係の管理方法
- 03バックエンドサービスとステートレスプロセスのパターン
- 04並行処理とグレースフルシャットダウンの指針
- 05構造化されたプロセスストリームログ
- 06resources/内のNode/TypeScript例
活用シーン
- 01新しいSaaSアプリケーションの評価
- 02既存サービスの設定管理の改善
- 03WebとWorkerプロセスの分離
- 04グレースフルシャットダウンとヘルスチェックの追加
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ citypaul/ .dotfiles --skill twelve-factorインストール後、Claude Code で/twelve-factorを入力して読み込まれたか確認します。
同梱ファイルresources/node-patterns.md
詳細
Twelve-Factor App パターン
元の方法論は、Software as a Service アプリケーションを対象としています。その原則はほかのデプロイ可能なアプリケーションにも役立ちますが、ポートバインディング、並行性、プロセスの破棄可能性、実行時設定、成果物の同一性は、すべてのフロントエンド、サーバーレス関数、CLI にそのまま当てはまるわけではありません。
12factor.net に基づいています。以下では 12 の要素すべてを取り上げます。ルール、アンチパターン、コードレベルでの影響はここに、Node/TypeScript の実装例は resources/ に記載しています。
信頼境界でのスキーマファーストパターンについては、typescript-strict スキルを参照してください。これらのパターンを TDD する方法については、testing スキルを参照してください。設定の検証、シャットダウン時の動作、バックエンドサービスとの連携は、いずれも振る舞い駆動テストでテストできます。
詳細リソースは resources/ ディレクトリにあります。必要に応じて読み込んでください。
| リソース | 読み込むタイミング |
|---|---|
node-patterns.md | Node/TypeScript でいずれかの要素を実装するとき — 設定スキーマの検証、オプションオブジェクトによる依存性の注入、.env.example、システム依存関係のチェック、バックエンドサービスのファクトリー、Redis セッションストア、Web/ワーカーのエントリーポイント、ヘルスチェック、グレースフルシャットダウン、構造化ロガー、管理者用スクリプト |
適用するタイミング
- グリーンフィールドの SaaS/プロセスアプリケーション:12 の要素すべてを評価し、実行環境とプラットフォームに適したものを採用します。
- ブラウンフィールドアプリケーション:確認されたリスクに応じて、段階的に改善を取り入れます。一般的な導入順序は次のとおりです。
- 設定(要素 III)— 構成を再構築せずに、環境変数の検証を追加する
- ログ(要素 XI)— 構造化されたプロセスストリームのロギングに切り替える
- 破棄可能性(要素 IX)— グレースフルシャットダウンのハンドラーを追加する
- バックエンドサービス(要素 IV)— プラットフォームに適したリソースバインディングを注入する
- ステートレスプロセス(要素 VI)— メモリ内の状態をバックエンドサービスに移行する
コードベース(要素 I)
バージョン管理で追跡する 1 つのコードベースから、複数のデプロイを行います。コードベースは、リポジトリでも、モノレポ内で明確に管理されている部分でも構いません。この名称に合わせるためだけに、リポジトリを分割してはいけません。コードの共有には、コピー&ペーストではなく、明示的に管理されたパッケージまたはモジュールを使用します。
モノレポでは、各サービスに専用のエントリーポイント、デプロイパイプライン、バックエンドサービス接続を用意します。各サービスを独立してデプロイできるなら、単一のリポジトリで問題ありません。
設定(ファクター III)
デプロイごとに異なる設定は、ビルドの外部に保存します。Twelve-Factor では環境変数を標準とします。ランタイムで必要な場合は、同等のプラットフォームネイティブなシークレットまたは設定の注入機構を使用してください。認証情報や環境固有のエンドポイントをハードコードしないでください。
ルール:
- 起動時にスキーマで設定を検証します。設定が無効な場合は、速やかに終了し(ゼロ以外の終了コード)、明確なエラーを表示します。
- オプションオブジェクトを介して設定を注入します。呼び出しツリーの深い場所で
process.envをインポートしないでください。 - 設定モジュールを導入または変更するときは、報告された問題で言及されていない設定(ログレベル、出力パス、機能トグルなど)も含め、アプリケーション内のすべての
process.envの読み取りを同じ変更でそのモジュールに移します。リクエストハンドラー、ドメインモジュール、起動コードにほかの環境変数の読み取りを散在させたままの設定モジュールは、設定のばらつきをなくしたのではなく、移動させただけです。最後に、ソース内でprocess.envを grep し、該当箇所が設定モジュールと、それを呼び出すプロセスのエントリーポイントだけであることを確認します。 - コード内だけでなく、運用担当者向けにも必要な設定を記載します。環境変数が設定インターフェースの場合は、スキーマと同じ変更で
.env.exampleを作成または更新します。スキーマで必須となる設定ごとに1行記載し、値はプレースホルダーのみとします。実際の認証情報を記載したり、.envをコミットしたりしないでください。検証スキーマは起動時にプロセスの問題を判定しますが、コンテナーに何を設定すればよいかを人に伝えるものではありません。両方を用意してください。プラットフォーム独自のマニフェストやシークレット宣言形式が設定インターフェースの場合は、同じルールに従ってそちらを更新します。
Zod 設定スキーマ、オプションオブジェクトによる注入、.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=... のように指定し、分岐を削除します。変更するファイル内の環境名による比較は、依頼された設定に関するものだけでなく、すべて削除してください。同じ設定を与えられた2つのデプロイは同じ動作をする必要があり、残った NODE_ENV の比較がまさにそれを妨げます。そのような分岐を削除せず、概要で指摘するだけでは、設定のばらつきが残ります。修正してから、修正したことを伝えてください。
依存関係(ファクター II)
すべての依存関係を明示的に宣言します。暗黙のシステム全体のパッケージに依存しないでください。
ルール:
- すべての依存関係を
package.json(または同等のマニフェスト)に記載します。 - ロックファイル(
package-lock.json、pnpm-lock.yaml)をリポジトリにコミットします。 - 依存関係を分離します。アプリがシステム環境から漏れ出したり、システム環境に依存したりしないようにします(グローバルインストールではなく
node_modulesを使用します)。 - サブプロセスを通じて呼び出すシステムツールは、明示的なランタイム依存関係です。文書化し、可能な場合はバージョンも記載して、起動時またはビルド時に確認します。
バッキングサービス(ファクター 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を参照してください。 - 永続性が必要なバックグラウンド処理や長時間実行される処理は、適切なワーカー/キューに移します。レイテンシや再試行の要件を満たし、より簡単である場合は、リクエストに紐づく小規模な処理をインラインで実行します。
- 各プロセスタイプを個別にスケールします。
- プロセスタイプは、プラットフォームで既に使用されている設定(
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 にはエラーレコードや診断情報を出力できます。アプリ内からログをファイルに振り分けたり、ファイルに保存したりしてはいけません。
依頼で指定された内容にかかわらず、次の3つが満たされるまで、ロギングの変更は完了ではありません。 レコードが構造化されていること、プロセスストリームにのみ出力されること、そしてリクエストの処理中に書き込まれるすべてのレコードに、そのリクエストの識別子が含まれることです。最も見落とされやすいのは3つ目です。構造化されて stdout に出力されていても、1つのリクエストに絞り込めないログは、インシデント発生時に活用できません。そのため、呼び出し箇所を移行する前に各ハンドラーの入口でリクエストごとの子ロガーをバインドし、作業完了を報告する前に、各ハンドラーで識別子が出力されていることを確認してください。
この要素が扱うのは、ログの転送方法と形式です。ストリームに何を出力するか(ワイドイベント/正規ログ行、トレース、SLO、アラート)については、observability スキルを参照してください。
インターネットに公開するサーバーについては、RFC 6302(BCP 162)で、ログに記録すべき最低限の要件が定められています。送信元と宛先のアドレスおよびポート、タイムスタンプ(UTC が望ましい)、トランスポートプロトコルです。これらは、アプリケーションレベルの構造化ロギングに加えて、サーバー/フレームワークのレベルで記録してください。
意味論上の要件
長時間実行されるサービスでは、まず以下の特性を満たし、正確なフィールドはリポジトリのオブザーバビリティとプラットフォームの契約に委ねます。
- 構造化出力 — ログは自由形式の文字列ではなく、機械で解析できる形式にします(JSON を推奨)。
- プロセスストリームのみ — プラットフォームで文書化されている stdout/stderr の契約を使用します。アプリはログファイルに書き込んだり、ファイルトランスポートを設定したりしません。
- 適切な重大度 — プラットフォームが認識するレベルを使用し、順位付けしたうえで、出力する前に各レコードをしきい値と比較します。しきい値はロガーの外部から指定します。既存の引数なし呼び出し箇所との互換性を保てる適切なデフォルト値を持つファクトリーのオプションにするか、設定の構築時に読み込んで注入するデプロイ時の設定を 1 つ使用します。引数を取らないファクトリーではしきい値がハードコードされるため、コードをリリースせずに特定のデプロイで debug を有効にすることはできません。
- コンテキストデータ — ログではメッセージ文字列だけでなく、構造化メタデータ(キーと値のペア)を受け付けます。
- タイムスタンプの付与 — すべてのログエントリに ISO 8601 タイムスタンプを含めます。
- リクエストの相関付け — リクエストの処理中に出力されるすべてのレコードに、リクエスト ID またはトレース ID を付けます。受信したトレースヘッダーまたはリクエスト ID ヘッダーから ID を取得し、どちらもない場合はリクエストごとに生成します。その後、そのリクエストの各ログ呼び出しに ID を渡します。通常はハンドラーの入口でリクエストごとの子ロガーをバインドし、それを通じてログを記録します。「ID が届かなかった」ことは、フィールドを省略する理由にはなりません。大量のログの中から特定のリクエストの行を抽出できなければ、運用担当者はログをまったく活用できません。利用可能な場合は、プラットフォームの W3C トレースコンテキスト統合を優先します。
これらの意味論を満たしていれば、プロジェクトでは任意のロギングライブラリ(pino、console トランスポートを使った 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 });
});
この要素は長時間実行されるウェブサービスに適用されます。静的サイト、サーバーレスハンドラー、サーバーのライフサイクルを意図的に管理するプラットフォームでは、代わりにそれぞれのプラットフォームの契約に従います。
開発環境と本番環境の同等性(Factor X)
動作に影響する特性について、開発環境と本番環境の類似性を保ちます。互換性のある代替サービスを意図的に選び、契約テストを行っている場合は、実際の製品が異なってもかまいません。
ルール:
- 意味上の違いが重要な場合、統合テストと同等性テストには本番環境と同等のバックエンドサービスを使用することを優先します。
- リポジトリに既存のローカルサービスの仕組みがあれば、それを使用します。コンテナーは選択肢の 1 つですが、必須ではありません。
- 設定スキーマの検証(Factor III)により、起動時に不一致を検出できます。
管理プロセス(Factor XII)
マイグレーション、データ修正、コンソールセッションなどの管理タスクは、同じコードベースと設定を使用する単発プロセスとして実行します。マイグレーションスクリプトの例については、resources/node-patterns.mdを参照してください。
管理スクリプトは、アプリケーションコードと並んでリポジトリ内に置きます(例:scripts/migrate.ts)。これらは独立したツールや、その場限りのシェルコマンドではありません。管理プロセスはアプリと同一の環境で実行されます。同じリリース、同じ設定、同じ依存関係を使用します。
12-Factor パターンのテスト
12-factor パターンは、振る舞い駆動テストで検証できます。
- 設定:
createConfigが必須変数の欠落時に例外をスローし、正しいデフォルト値を返すことをテストします - 破棄可能性:シャットダウン時にすべての接続が閉じられることをテストします(db/cacheにはテスト用の代替オブジェクトを注入します)
- バックエンドサービス:注入された設定を通じて、サポートされているリソースバインディングをテストします
- ステートレス性:リクエストハンドラーが以前のリクエストの状態に依存しないことをテストします
オプションオブジェクトによる設定の注入により、process.envやグローバル状態をモックせずに、これらすべてのパターンを自然にテストできます。ファクトリーパターンと振る舞い駆動テストの例については、testingスキルを参照してください。
チェックリスト
- 1つの管理対象コードベースで複数のデプロイを行い、共有コードには明確な所有者がいる
- 実行時に設定するプロセス型アプリでは、環境間で同じビルド成果物を使用することを優先する
- デプロイごとに異なる設定はビルドの外部から注入し、スキーマを使って起動時に検証する
- 設定が無効な場合、起動時に明確なエラーメッセージを表示して速やかに失敗する
- オペレーター向けの例で、スキーマと併せてすべての必須設定を説明する。環境変数がインターフェースの場合は
.env.exampleを、そうでない場合はプラットフォームのマニフェストを使用する(プレースホルダーのみとし、実際の認証情報は使用せず、.envはコミットしない) - すべての依存関係をマニフェストで明示し、ロックファイルをコミットする
- バックエンドサービスは、プラットフォームに適したリソースロケーター、ハンドル、または認証情報として注入し、同等のプロバイダーは構成時に切り替え可能にする
- メモリ内のセッション状態がなく、リクエスト間でローカルファイルシステムの状態に依存しない
- 作業ごとに異なるスケーリングやライフサイクルの要件がある場合は、プロセス型を分ける
- グレースフルシャットダウンのために、ドレインタイムアウト付きのSIGTERM/SIGINTハンドラーを設ける
- シャットダウン時にデータベースのプールと接続を閉じる
- プラットフォームで必須のヘルス/レディネスプローブを実装する
- ログはプラットフォームが取得するプロセスストリームに構造化して出力し、ファイルへの出力は使用しない
- リクエストスコープの各ログレコードにリクエストIDまたはトレースIDを含める。受信ヘッダーから取得し、存在しない場合はリクエスト用に生成する
- 長時間稼働するウェブサービスは、プラットフォームのポートバインディング契約を満たす
- 差異が重要な場合は、開発環境と本番環境のバックエンドサービスでパリティテストを行う
- 管理スクリプトはリポジトリ内に置き、同じ設定と依存関係を使用する