2011年のThe Twelve-Factor App
(12factor.net)- The Twelve-Factor Appは、WebアプリやSaaSを長期にわたって運用・拡張するための方法論で、設定の自動化、移植性、クラウドへのデプロイ、継続的デプロイをあわせて扱う
- 特定のプログラミング言語や、データベース・キュー・メモリキャッシュといったバックキングサービスの組み合わせに縛られず、さまざまなサービス型アプリケーションに適用できる
- Herokuプラットフォームで数百個のアプリの開発・デプロイに直接関わり、数十万個のアプリの開発・運用・拡張を間接的に観察した経験が基盤になっている
- 中心にある問題意識は、アプリが有機的に成長する際に生じる協業コストとソフトウェアの浸食を減らすための共通語彙を提供することにある
- サービスアプリケーションを作る開発者だけでなく、それをデプロイ・管理する運用エンジニアにとっても実務基準として活用できる
SaaSアプリのための12の運用原則
- 現代のソフトウェアは一般にWebアプリまたはSaaSの形で提供されており、Twelve-Factor Appはこうしたアプリケーションを作るための方法論である
- 目的は、アプリの開発・デプロイ・運用のプロセスをより予測可能にすることにある
- 新しい開発者がプロジェクトに参加する際にかかる時間とコストを減らすため、宣言的な設定自動化を使う
- 基盤となるオペレーティングシステムとの明確な契約を設け、実行環境間の移植性を高める
- サーバーとシステム管理の負担を減らし、現代的なクラウドプラットフォームへのデプロイに合わせて設計する
- 開発環境と本番環境の差を縮め、継続的デプロイを可能にする
- ツール、アーキテクチャ、開発慣行を大きく変えずに拡張できるようにする
- 適用範囲は特定の技術スタックに限定されない
- どのプログラミング言語で書かれたアプリにも適用できる
- バックキングサービスには、データベース、キュー、メモリキャッシュなどが含まれる
Herokuでの経験から整理された背景
- 貢献者たちはHerokuプラットフォームで数百個のアプリの開発・デプロイに直接関わり、数十万個のアプリの開発・運用・拡張を間接的に観察した
- 実際のSaaSアプリから得た経験と観察をもとに、アプリ開発の理想的なプラクティスを整理している
- 時間とともにアプリが有機的に成長していく流れに着目する
- 同じコードベースで複数の開発者が協業する方法を扱う
- ソフトウェア浸食コストの回避を重要な目標と見なす
- 形式はMartin Fowlerの*Patterns of Enterprise Application Architecture* と Refactoring から着想を得ている
12の要素
- I. Codebase: バージョン管理された1つのコードベースと複数のデプロイを持つ
- II. Dependencies: 依存関係を明示的に宣言し、分離する
- III. Config: 設定を環境に保存する
- IV. Backing services: バックキングサービスを接続されたリソースとして扱う
- V. Build, release, run: ビルド段階と実行段階を厳密に分離する
- VI. Processes: アプリを1つ以上のステートレスなプロセスとして実行する
- VII. Port binding: ポートバインディングでサービスを外部に提供する
- VIII. Concurrency: プロセスモデルでスケールアウトする
- IX. Disposability: 高速な起動と優雅な終了で堅牢性を高める
- X. Dev/prod parity: 開発、ステージング、本番をできるだけ近い状態に保つ
- XI. Logs: ログをイベントストリームとして扱う
- XII. Admin processes: 管理タスクを一回限りのプロセスとして実行する
1件のコメント
Hacker News のコメント
12-Factor App は、2011年の Heroku と当時のコンテナ型インフラの制約にかなり寄りかかって作られた推奨事項であって、工学原則に深く根ざした文書には見えない
たとえば設定を環境変数に入れろという主張は、著者たちが Heroku で働いていて、Heroku が Web アプリの入力フィールドとして環境変数を埋める方式だったからだ
設定履歴をバージョン管理で追跡したい、GitOps を使いたい、k8s ConfigMap を使いたい、マウントされたボリュームに設定ファイルを置きたい、というなら、どれもおおむね問題ない選択だ。設定の状態をアプリのデプロイ状態から分離してくれるからだ
この文書は森と木を混同しており、実際の工学原則よりも、それを書いた会社の製品機能に合わせた推奨をしているので、有害なガイドラインだと思う
Kubernetes で安全な設定保存を求めるなら、結局 Secrets を使うことになり、これは環境変数と同じようにキー・バリュー形式になる。Git で同等のセキュリティを得るには暗号化レイヤーが必要で、そうすると差分比較が壊れ、追加のツールが必要になる
結局、Heroku のような高水準のデプロイツールがなぜ作られたのか、という話に戻る
ログはストリームとして扱うという原則は今でも正しい。ファイルではなく STDOUT にログを書き、オーケストレーターに読ませて保存させればいい
設定は環境から取得する。アプリはデプロイ方法に応じて、ローカルでは .env、本番ではシークレットストアのように、別のソースから設定を読む傾向がある
ポートバインディングも同様で、アプリがポートを開き、前段に nginx のようなものを置いてリバースプロキシを構成する。K8S の Service と Ingress がその役割を担う
最近 12-Factor に対してできる最大の批判は、文書が実際にはうまく書かれておらず、読者が何を言っているのかをすでに正確に理解している前提に立っていることだ
ただし Heroku がこの概念を広く知らしめ、利用を広げた功績は認める
アプリを起動する前にマシンへ settings.json を追加する方式ではなく、同じソースが Azure EU north の AKS クラスターにデプロイされればそのクラスターに設定された値を使い、Ikea の額縁の中の RPi Zero Docker Swarm クラスターにデプロイされればそのクラスターの設定を使う、ということだ
そのクラスター名は Gibson と呼ばれている
これらの項目は、それぞれかなり妥当に反論できると感じる
第一に、アプリ1つにつきリポジトリ1つという原則は、根本的に間違っているわけではない。機能的に強く結合していてリリース周期を共有するが、別プロセスと独立したスケーリングの利点を得るために別々にデプロイすべき複数のアプリを、1つのリポジトリで開発しても問題はない。公開 API とワーカープロセスの分離、たとえば Ruby の Sidekiq、Python の Celery、一般的な Kafka コンシューマーが思い浮かぶ
第二に、「12-Factor アプリはシステム全体のパッケージが暗黙に存在することに依存しない」というのは、Nix のようなものを使わない限り、実際に達成するのは非常に難しい。カーネルのシステムコール API への依存も漏れ出すし、ほとんどの Rust アプリは musl を除けば glibc に暗黙的に依存する。Alpine のような slim ディストリビューションを除けば、主要な Linux ディストリビューションには存在する。Docker という支えも、この問題を緩和するために必要なのだと思う
第三に、設定を 環境変数に保存するのは、より脆弱に見える。シークレット値を環境に入れることを強制してセキュリティを下げる可能性があり、型安全性、IDE の補完、自動パースを享受できる構造化されたファイル設定を捨てることになる。環境変数では、文字列ではない複雑な設定値のパーサーを自分で実装しなければならない。実際には .env ファイルの形でリポジトリにコミットされることも多いので、コミット安全性という論点も無意味になる
glibc を含むオペレーティングシステムに依存するなという意味ではなく、特定の言語パッケージマネージャーのパッケージがそのマシンにインストールされていなければならない状態にするな、という意味だ
残りの指摘には同意する。特に環境変数の話は、実際に根拠のある助言というより、当時の Ruby 開発で一般的だったやり方を著者たちが最善だと仮定して書いたものに近い
だからそうした状況に備える必要があり、異なるバージョンの組み合わせを簡単にテストするには、別リポジトリのほうがよいと思う
高水準言語の大半は特定の glibc に依存しない。その言語ランタイムが正しく動けば、アプリもその上で動く。もちろん場合によっては Docker のようなものを使うことになる。難しいからといって価値がないわけではない
動的言語やマネージド言語では、システムパッケージ関連の面倒の大半を無視できることが多い
全体的には気に入っているが、非技術者や半分だけ技術者のような人が「12 factor」を万能のイエローカードのように持ち出してリリースを遅らせたことがあまりに多く、ほぼ完全に無視するようになった
実際「agile」も似たようなものだった。こうしたガイドラインの意図は理解できるが、実際の価値は象牙の塔的な技術リーダーシップしか提供できない人たちにとってのほうが、はるかに有用に見える
やる気が先走ったジュニアエンジニアやアーキテクト志望者が、12-Factor の記事をあらゆるリリースの必須要件のように使うケースを経験したことがある
こうしたものは目指すべき良い目標ではあるが、現実にはリリースのために妥協が必要で、どの部分を遅らせるか、あるいは先送りするかを選ばなければならない、と断固として一貫して説明する必要がある
些細な逸脱を理由にリリースを止めることはないだろうが、全体として合っていないなら少なくとも技術的負債として扱うべきだ。あるリリースが一項目で大きく後退するなら、止めるか、少なくともそのトレードオフに価値があると判断した理由について、より詳しいレビューを強制するのは公平だ
組織が 12FA をベストプラクティスとして採用したなら満たすべきだが、デプロイを妨げてはならない
12FA はチェックすべき単一の箱ではない。プロダクトが成熟するにつれて各項目を一つずつ、必要ならさらに分割してプロダクトに追加する形で実装できるし、多くの場合そうすべきだ
初期に良いエンジニアリングをしておいたなら、つまり適切な抽象化とインターフェースを用意し、すべてをハードコードしていないなら、問題にはならないはずだ
YAGNI も 12FA と同じくらい乱用される。12FA に最も大きく欠けているのは、ジュニアエンジニアが参照できる具体的な例だ
こういう問題は善意に受け取ることを学んだ。なぜ問題を提起しているのか、既存プロセスが不明確なのか、欠陥があるのか、信頼性に欠けるのかを見る必要がある
合理的な懸念が見つかったなら、プロセスを変えてドキュメントも更新すればよい
Twelve-Factor App は設定に環境を使うように言い、Docker は設定に環境を使うなと言う。安全ではないからだ
12-Factor の多くのパターンは気に入って使っているが、一部は VPS の文脈で使われていたものだ。当時は環境が安定していて安全で、より固定的だったが、コンテナでは環境がどこかのレイヤーに載ってしまう可能性がある
コンテナ時代には、この特定の項目はかなり悩ましい点だった。Docker secrets が常にうまく合うわけでもなく、動かすにはいろいろな曲芸が必要になる
シークレットを環境変数としてアプリに注入するからといって、その外部での処理まで安全でないという意味ではない。たとえば AWS ECS コンテナには、起動時に Secret Manager からシークレットを取得して環境変数として渡す組み込みサポートがある: https://docs.aws.amazon.com/AmazonECS/latest/developerguide/...
コンテナ起動時点でシークレットを取得し、実行中アプリケーションの IAM 認証情報で Secret Manager から取得する。したがって、そのシークレットに対する権限が必要になる
環境変数を使う利点は、結局それがどのように設定されるかに完全に依存しているように見える。こうした仕組みなら、大きな欠点はあまり見当たらない
見える主な欠点は、環境変数をダンプしようとする一般的なマルウェアが値を取得できる点だが、シークレットをメモリに永続保存しないのでなければ、その脅威を防ぐのは概して難読化に近い
秘密鍵がそのように露出するのは本当に避けるべきだ
k8s でファイルシステムにシークレットをマウントする方式には特に問題はない。もちろん、これらはすべてデプロイ環境に依存する
ときには環境変数のほうがまだましな選択肢になる。シークレットはそもそも常に難しいものだからだ
シングルテナントだからという理由で入力を盲信すると、後で任意の事業方針転換によって条件が変わったとき、追跡しにくいバグと長い夜に直面することになる
ここ数年、12-Factor App について多く議論してきたし、混乱もたくさん見てきた。12-Factor のサイトは優れているが、主に各項目がなぜ重要なのかをすでに理解している人向けにはよいものだ。
ルールの背後にある理由を知らない人には、より深い説明が必要だった。そこで “What are 12 Factor Apps and Why Should You Care?”[1] という動画を作ったところ、いくつかの会社がエンジニア/DevOps の新入社員教育に使っており、とても役に立ったと聞いている。
どこで学ぶにせよ、12-Factor App について 1〜2時間かけて学ぶ価値はある。ほとんどの「ルール」は認識しておくべきもので、自分で失敗して痛い目を見るまでは、すぐには明らかにならない。
[1] https://youtu.be/REbM4BDeua0
「単体テスト」という概念が90年代後半頃に traction を得始め、権威ある誰かが支持してくれたことに安堵した。
Kent Beck が JUnit を発表するまで自分でやらなかった理由は、扱っていたコードが他のコードから制御されるのに向いた構造ではなかったからだ。グローバル変数、広く散らばった状態、外部システムや特定のファイルシステムレイアウトへの依存、モジュール性の無視のせいで、設計された文脈の外では何も実行できなかった。
これらはすべて「悪い設計」だったが、締め切りには間に合ったので、皆がそうしていた。単体テストが traction を得れば、プログラマーたちが一枚岩の設計から離れてくれることを期待していた。
25年間、getter/setter 用の単体テストや、アプリ内のどの関数も実行するだけでライブデータベースが必要になるためインメモリデータベースを作る巨大な単体テストを見てきて、結局それらが失敗してコメントアウトされるのを見た後では、単体テストが意味のないチェックボックス以上のものになるという信念を失った。皆が「ベストプラクティス」だからとチェックを入れるが、なぜやるのか立ち止まって考えない。
設定に関する助言には、いつも最も同意できなかった。設定は複数の主体が定義することになるし、開発者が定義する場合も多いので、アプリケーションに合理的なデフォルト値を同梱し、環境別ファイルや環境変数で上書きするのが最善であることが多い。
ほとんどのサーバーサイドアプリケーションでは、この方式が最も柔軟だ。どのような構成にすべきか既に分かっていることが多く、それはソース管理される方がよいが、シークレット値は実行時に注入されるべきだ。
開発、テスト、本番で膨大な設定時間を費やさないためには、設定には階層的な上書きが必要だ。
開発用だと言うなら、結局本番を壊すだろうし、本番用のデフォルトは開発では何の意味もないかもしれない。
設定セクションを考える一つの方法は、「この設定戦略はコンテナとうまく合うか?」というものだ。イメージをビルドすると、新しいイメージを作らない限り変更が持続しない静的なディスク状態ができる。
設定がファイルベースだけなら、テストと本番の挙動を切り替えるために、完全に新しいイメージをビルドしなければならない。
設定を基盤となるディスクと無関係に変更できるようにすると、変更の切り分けに役立つ。アプリが壊れた理由がデプロイ、つまりイメージ生成が壊れたからなのか、設定が間違っていたからなのかを区別する必要がある。
イメージ生成と設定変更を分離すれば、その問い自体が消える。
たとえば QA から本番の作業投入キューへ、認証拒否のリトライを10万回投げるような、十分に予測可能な人為ミスを許してはならない。
ただし多くの設定はインフラに関するものではなく、たとえば各環境でどの ResolverStrategy Bean を接続するか、といった問題であることが多い。
設定はバージョン管理されるべきだが、ソースコードとは別に管理されるべきだ。設定はデプロイに使われるイメージではなく、デプロイそのものを記述するものだからだ。
確かに影響力のあるエンジニアリング規範だった。Render や Vercel のようなホスティングの簡単な抽象化は今ではたくさんあるが、この文書が2012年に書かれ、当時の Web アプリは受け入れられた共通慣行という面ではずっと西部開拓時代のようだったことを考えると、少し不思議な感じもする。
この文書で大きく欠けているのは、ルールに対する正当化だ。ほとんどすべてが単なるルールである。
そのルールが良いものか判断しにくく、この文書はそれを見極める助けにならない。
タイトルだけを見て、二要素認証についての論評かと思った。一度ログインするために、パスポート写真、顔スキャン、運転免許証、SMS、Google Authenticator、メールリンク、パスワード、指紋をすべて要求するアプリのようなものだ。
Docker 初期に、WordPress を Twelve-Factor App のように動作させるためにかなり作業した。
従来、WordPress はそのようには動作しなかったし、ある程度は当然だ。WordPress は、書き込み可能で永続的なローカルディスクを持つ長寿命サーバーが一般的だった世界で育ったからだ。
その後、状況は大きく変わっているはずだ。2016年頃の話だが、本当に面白い挑戦だった。
物事がどれほど単純になるかに驚いたし、セッション固定性を気にせず複数ノードに負荷分散できるようになった。
もちろん、セッション用の別 DB が必要になるなど、別の面ではより難しくなった。