✋

The Twelve-Factor App:クラウドネイティブな設計の基礎知識

に公開

以下の書籍を読んでいると、Twelve-Factor Appが出てきたので、
知識の整理がてらまとめてみました。
AWSコンテナ設計・構築[本格]入門 増補改訂版

The Twelve-Factor Appとは?

The Twelve-Factor AppはSaaS(Software as a Service)を作るための開発方法論になります。
特に以下のようなSaaSを作ることを目的としています。

  • セットアップ自動化のために宣言的なフォーマットを使い、プロジェクトに新しく加わった開発者が要する時間とコストを最小化する。
  • 下層のOSへの依存関係を明確化し、実行環境間での移植性を最大化する。
  • モダンなクラウドプラットフォーム上へのデプロイに適しており、サーバー管理やシステム管理を不要なものにする。
  • 開発環境と本番環境の差異を最小限にし、アジリティを最大化する継続的デプロイ を可能にする。
  • ツール、アーキテクチャ、開発プラクティスを大幅に変更することなくスケールアップできる。

すなわちクラウドでのアプリケーション開発のための方法論です。

The Twelve-Factor Appの12項目

  1. コードベース
  2. 依存関係
  3. 設定
  4. バックエンドサービス
  5. ビルド・リリース・実行
  6. プロセス
  7. ポートバインディング
  8. 並行性
  9. 廃棄容易性
  10. 開発・本番一致
  11. ログ
  12. 管理プロセス

次からそれぞれを深掘りしてみましょう。

1.コードベース

~バージョン管理されている1つのコードベースとデプロイ~
コードベースとSubversionやGitのような構成管理システムで管理されているリポジトリのことを指します。
The Twelve-Factor App曰く、

コードベースとアプリケーションには常に1v1の関係がある

ということです。
OK例
以下のような例はアプリケーションではなく、分散システムとなります。
分散システムの場合は個別にTwelve-Factorに適合することが可能です。
コードベース2つで1つのデプロイ
図がわかりにくく申し訳ないのですが、以下のような例はコードベースを共有している例です。これはTwelve-Factorに違反している状態です。
共通のコードをライブラリに分解して、Mavenなどの依存関係管理ツールで組み込む必要があります
NG例

2.依存関係

~依存関係を明示的に宣言し分離する~
Twelve-Factor Appでは

システム全体にインストールされるパッケージが暗黙的に存在することに決して依存しない

としています。
つまり、暗黙的に依存しているようなパッケージが存在してはならないということです。
Mavenであればpom.xml、RubyであればBundlerに全ての依存関係を完全かつ厳密に宣言する必要があります。
メリットとしては、ランタイムとMavenなどの依存関係管理ツールさえあれば、新しく開発者が加わった際に環境構築を単純化することができる点です。

3.設定

~設定を環境変数に格納する~
これは開発の常識かもしれません。
DBや外部サービスへの認証情報などは環境変数に格納し、

設定をコードから厳密に分離すること

が必要です。
このあたり、常識レベルの話かと思われるので、詳細は割愛します。
注意してほしいのはプロパティファイルなどの設定ファイルはコードの一部です。
Twelve-Factor Appでは環境変数まで分離することを要求しています。

4.バックエンドサービス

~バックエンドサービスをアタッチされたリソースとして扱う~
これはアプリケーションがネットワークを経由して利用するサービス(バックエンドサービス ex)MQやMemcached、DBなど)は

ローカルサービスとサードパーティサービスを区別しない

アプリケーションにとってはどちらもアタッチされたリソースで設定としてもつURLや認証情報などでアクセスできるように、疎結合化されているべきであるということを示しています。
例えば、バックエンドの内部処理が変わったからそれを使用するアプリケーションも変更しなければならないという状態ではダメだということです。

5.ビルド、リリース、実行

~ビルド、リリース、実行の3つのステージを厳密に分離する~
Twelve-Factor Appではビルド、リリース、実行のステージをそれぞれ以下のように定義しています。

  • ビルドステージ
    コードリポジトリを実行可能な塊に変換するステージ。
    コミットされた指定のバージョンを使って、依存関係を取得してローカル環境に配置し、バイナリやアセットファイルをコンパイルする。
  • リリースステージ
     ビルドを受け取り、デプロイの設定と結合する。
     出来上がるリリースにはビルドと設定の両方が含まれ、実行環境の中ですぐに実行できる準備が整っている。
  • 実行ステージ(ランタイム)
     選択されたリリースに対して、アプリケーションのいくつかのプロセスを起動することで、アプリケーションを実行する。

この3つのステージの厳密な分離は現代の開発手法としては、当たり前のように行われていることだと思います。
Mavenを使った、AWSのECSで稼働させるプロジェクトであれば、

  • Mavenを使ってビルド(ビルドステージ)
  • Dockerイメージ化し、ECRへプッシュ(リリースステージ)
  • EventBridgeなどをトリガーに起動(実行ステージ)
    当たり前のように行われていることですが、このステージの分離は
  • コードが壊れているときにビルドステージでエラーになる(障害を未然に防げる)
  • リリースの状態をリリースIDやバージョンなどで管理できる
    といったメリットがあると私は考えています。

6.プロセス

~アプリケーションを1つもしくは複数のステートレスなプロセスとして実行する~

プロセスはステートレスかつシェアードナッシングである

これは永続化する必要のあるデータは、ステートフルなバックエンドサービス(簡単に言えばデータベース)に格納しなければならないということを示しています。
例えば、オンメモリであったりファイルシステムであったりを、短い単一のトランザクションとしてのキャッシュとしてではなく、永続的なデータとしたり、別プロセスとの共有財産としたりするのは違反行為です。

  • 瞬断やデプロイ、設定変更で消えてしまうことがある
  • 再起動時の状態が正しいことは担保できない(ファイルが更新途中の場合がある等)

また、Twelve-Factor Appではスティッキーセッションも違反行為としています。
スティッキーセッションはセッションデータをメモリにキャッシュしておき、同じクライアントからのリクエストを受け取る際にそのセッション情報を利用するというものです。
Twelve-FactorではMemcachedやRedisなどの有効期限をもつデータストアに格納すべきとしています。

7.ポートバインディング

~ポートバインディングを通してサービスを公開する~
簡単に言えば、指定したポート(HTTPであれば80、HTTPSであれば443など)にアクセスすれば、アプリケーションの実行環境(Tomcatなのか、ApacheHttpdなのかなど)を意識せずにサービスにアクセスできるということです。
こちらも当たり前のように見えますが、ポートバインディングによって他のアプリケーションに対するバックエンドサービスにもなれます。
つまり、アプリケーション間の疎結合化に寄与することができます。

8.並行性

~プロセスモデルによってスケールアウトする~
個々のワークロードの種類をプロセスタイプに割り当てましょうというものです。
ワークロードを個々のプロセスとしておくことによって、プロセスを増やすだけでスケールアウトが可能になります。
これは「6.プロセス」で説明した、シェアードナッシングでステートレスなプロセスが実現されていることが前提となります。

9.廃棄容易性

~高速な起動とグレースフルシャットダウンで堅牢性を最大化する~

  • 起動時間を最小化する
    • 起動から数秒でリクエストやジョブを受け取れる状態になるべき。
    • Javaであれば、GraalVMやAppCDSの利用が起動の高速化に有効な手段です。
  • SIGTERMシグナルを受け取ったときに、グレースフルにシャットダウンする
    • 新たなリクエストの受け取りは拒否するものの、処理中のリクエストは完了してからシャットダウンする
    • 暗黙的にHTTPリクエストが短いことが前提となっている。
    • JavaのSpringBootではプロパティでGraceful Shutdownを設定することができる。(その仕組みについては、今度まとめたい)

10.開発/本番一致

~開発、ステージング、本番環境をできるだけ一致させた状態を保つ~
大規模なウォーターフォールによる機能追加などで開発環境と本番環境の間に大きな差異がある状態は、多くのエンジニアが経験していることだと思います。
この差異というのは単純にコードの差異だけではなく、

これらのギャップを小さく保とうというのが開発/本番一致です。

これを実現する試みとして、CI/CD(継続的インテグレーション/継続的デリバリー)という手法があるように思える。

  • CI(継続的インテグレーション)
    • ビルドやテストを自動化し、リポジトリへの統合を頻繁に行う
  • CD(継続的デリバリー/デプロイメント)
    • CIのプロセスを引き継ぎ、本番環境へいつでもデプロイ可能な状態を維持(デリバリー)or自動的に本番環境へ適用する(デプロイメント)
      ただウォーターフォール開発だと規模にもよるが、CIはできても、CDが難しい点があると感じています。この手法はアジャイル向きな手法と考えています。

11.ログ

~ログをイベントストリームとして扱う~
Twelve-Factor Appでは

アプリケーションの出力ストリームの送り先やストレージについて一切関知しない。

としています。これは、アプリケーションがログファイルへの書き込みや管理を行うべきではないという思想です。
代わりにプロセスはイベントストリームを標準出力にバッファリングせずに書き出すことになります。
開発中は標準出力をターミナル等から確認すればよいですし、ステージング/本番環境では、アプリケーションからのストリームが指定した目的地(例えばAWSのCloudWatch)へと送られるようになります。
これによりtailなどでリアルタイムに監視することが可能となり、

  • 特定のイベントの検知
  • スケール傾向のグラフ化(リクエスト数/分など)
  • アラートを出すためのメトリクス定義
    が可能となります。

ただこれは組み込みアプリケーションのような画面やクラウド環境下での実行環境を持たないようなアプリケーションには適用できませんので注意が必要です。

12.管理プロセス

~管理タスクを1回限りのプロセスとして実行する~
管理タスクとは

  • DBのマイグレーション
  • 任意のコードの実行やDBに対してアプリケーションのモデルを調査するためのコンソール実行
  • リポジトリ上のスクリプト実行
    などが挙げられます。
    これらの管理プロセスは、リリースに対して実行され、そのプロセスと同じコードベースと設定を使わなければなりません。
    すなわちアプリケーションコードと管理用コードは一緒にデプロイされるべきというものです。

Discussion