Twelve-Factor App から学んだこと
本記事の目的
Webアプリケーションをサービスとして提供する開発者・インフラエンジニアが知っておくべき原則であるThe Twelve-Factor Appについて学んだので、その要点をまとめます。
学んだこと
Webアプリケーションがサービスと呼ばれるからには常に正しい動作で提供する必要があります。負荷が掛かって落ちたりバグが残ったまま公開されてはいけません。そのためにアプリのバージョン管理をしっかり行ってロールバックしたり、デプロイ後のスケールアウトや廃棄が迅速に行える必要があります。そのために重要なことは、アプリの関わるプロセスが自己完結していることです。そのアプリに必要なライブラリは明示的に宣言されて自動処理でも同じ環境を再現でき、このアプリがいつ落ちても大丈夫なようにステートに関わる部分はデータベースに追いやります。このように自己完結したアプリであればスケールアウトで並行性を保つことができます。
Twelve-Factor Appとは
The Twelve-Factor Appは以下の特性を満たすWebアプリケーションの事です。
- 宣言的なフォーマットを用いて、セットアップが自動化されている
- 実行環境での移植性が最大化されている
- モダンなクラウドプラットフォーム上へのデプロイに適している
- 開発環境と本番環境の差異を最小限にし、継続的デプロイが行える
- スケールアウトに大きな変更を要しない
1. コードベース
**Twelve-Factor Appは常にバージョン管理システムで変更を追跡される必要があります。**コードベースとはバージョン管理システムにおけるリポジトリのことです。コードベースとアプリは1対1の関係があり、コードベース単位でTwelve-Factorは適用されます。そして、一つのアプリに対してコードベースは一つですが、一つのコードベースに対して複数のデプロイが存在します。デプロイされたアプリには、本番環境で動いているものやステージング環境で動いているものが存在しますが、それらは一つのコードベースから生成されており、それぞれがどのバージョンであるかはバージョン管理して特定できる必要があります。
2. 依存関係
Twelve-Factor Appはその中で使われる全てのライブラリの依存関係が明示的に宣言され、その中で使われるライブラリの依存関係はシステム全体から分離されていなくてはいけません。 多くの言語ではサードパーティライブラリを管理するためのパッケージ管理ツールと、これらのライブラリを記録するマニフェストファイルを提供しています。Twelve-Factor Appでは、この二つを用いて依存関係を明示的に宣言し分離されなくてはいけません。その理由は、このアプリを動かすためのランタイムが準備されていればパッケージ管理ツールを用いて簡単にかつ、再現性が約束されたセットアップが可能になるからです。
3. 設定
Twelve-Factor Appはデプロイ毎に異なる設定は環境変数に格納しなくてはいけません。 デプロイ毎に異なる設定の例は以下の通りです。
- データベースなどの外部リソースへのハンドル
- 外部サービスの認証情報
- デプロイされたホストの名前など、デプロイごとの値
環境変数で指定する理由は二つあります。一つはコードに直に設定を書き込んでしまうとデプロイの際に、そのデプロイに必要な設定のために毎回コードを変更する必要が出てくるからです。二つめは設定のためにコード変更が必要な場合、スケールアウトを自動で行えないからです。
4. バックエンドサービス
バックエンドサービスとはネットワーク越しに利用する全てのサービスのことです。Twelve-Factor Appは、バックエンドサービスがローカルサービスであるかサードパーティサービスであるかによらず環境変数で指定するだけで自由にアタッチ、デタッチできなくてはいけません。 そのためには、コーディングの段階で外部サービスとのインターフェースは抽象化しておき、バックエンドサービスとは疎結合にしておく必要があります。
5. ビルド リリース 実行
Twelve-Factor App では、ビルド、リリース、実行の三つのステージは厳密に分離されてなくてはいけません。 どこかのステージの変更が他のステージに影響を及ぼしてはいけません。
- ビルドステージ:コードベース内のリリースしたいバージョンを選択して、そのバージョンで明示的に宣言された依存関係を使って実行可能なファイルを作成するステージ
- リリースステージ:ビルドステージで作られた実行可能ファイルと環境変数で指定した設定を結合させて、いつでも実行可能なアプリを作成するステージ
- 実行ステージ:リリースステージで作られたアプリを実行環境で実行するステージ
ビルドステージはデベロッパーが行い、ビルド段階でのエラーはデベロッパーが解決できるのでここで可能な限りCI/CDに必要な事項を網羅しておくのが良いです。例えば、アプリで使われる全てのプログラムのコンパイル、テストなどです。
一方、実行ステージはサーバの再起動時や、アプリのクラッシュ時にプロセスマネージャが自動的に再起動させようとするステージなので、ここでの複雑な処理でエラーが発生しても自動で解決することは難しいので避けるべきです。この段階では、アプリを起動することだけに集中するべきです。
また、リリースステージではロールバック機能を用いて、何か問題が生じた場合でも以前のリリースに直ちに切り替える準備をしておくべきです。
6. プロセス
Twelve-Factor Appはステートレスかつシェアードナッシングなプロセスで実行されなくてはいけません。絶対にこの実行ステージのプロセスが常に動作し続けているという仮定をしてはいけません。 この規則を破ったアプリは以下に述べる問題点を持っています。これを解決するために、ステートフルであるデータベースなどのバックエンドサービスを使う必要があります。
ステートレスではないアプリは負荷分散のためにスケールアウトした場合に、トランザクションの一貫性を保てない可能性が発生します。それは、トランザクションが複数回このアプリにアクセスする必要があるときに、どのアプリインスタンスにアクセスするかは決まっておらず前回のステートを覚えているプロセスにアクセスすることは約束されないからです。
また、シェアードナッシングではないアプリはそのアプリの全てのプロセスが同じコンピュータリソースを共有しています。これはデータの更新を行う際に同時に更新してしまう恐れがあり、トランザクションの一貫性が保たれなくなってしまいます。
7. ポートバインディング
Twelve-Factor AppはポードバインディングによってHTTPサービスとして公開されなくてはいけません。 Twelve-Factor Appのアプリはどのポートを外部に公開していても構いません。このアプリが本番環境で実行されるときにはルーティング層がHTTPポートでリクエストを受け付け、対応するポートにルーティングすれば良いのです。この方法を用いることで、動的に変わりうるポートを意識せずにあるアプリが他のアプリのバックエンドサービスになる事ができます。それぞれのアプリは設定として環境変数にバックエンドサービスのURLを指定すればそこにアクセスする事ができます。
8. 並行性
Twelve-Factor Appは負荷分散の際にプロセスモデルでスケールアウトします。 本番環境ではアプリへの負荷が大きい部分と小さい部分が存在し、負荷の大きい部分ではスケールアウトしていく必要があります。あらかじめ、負荷の大きくなる事が予想される部分と小さくなる事が予想される部分をアプリ、もしくは実行環境で分けた設計をする事でプロセス単位でスケールアウトする事ができます。そして、この並行性を高めるためにプロセスで述べたようなステートレスでシェアードナッシングなアプリを作る事が大事になります。
9. 廃棄容易性
Twelve-Factor Appは即座に起動・終了できなくてはいけません。 このことによるメリットは二つあります。まず、スケールアウトされてできたアプリが即座に起動して即座に負荷分散できます。そして、コードベースや設定への変更を即座に反映したアプリを起動できます。ビルド リリース 実行での実行ステージで複雑な処理をしないことはここでも効いてきます。
また、プロセスを廃棄する際にトランザクションの廃棄が発生しないように、グレースフルシャットダウンを採用するべきです。
10. 開発/本番一致
Twelve-Factor Appでは、開発環境と本番環境のギャップを小さく保って継続的デプロイを行う必要があります。 ギャップを生まないために重要なことは、開発環境でもバックエンドサービスは本番環境と同じものを用いることです。結合テストの際により軽いデータベースを用いて行うなどで、本番環境に対する完全なテストができずに問題が生じるような事があってはいけません。
11. ログ
Twelve-Factor Appはログをイベントストリームとして標準出力に出力します。 すべての実行中のプロセスとバックエンドサービスから受け取ったログが集約された時間順に並んだストリームです。ローカルでの開発中はこの標準出力を見てアプリの動きを確認します。本番環境ではオープンソースのログルーターを用いる事で、ログの保存を行います。 しかし、アプリ自身は標準出力しか担っておらず保存することまでは考えていません。
12. 管理プロセス
Twelve-Factor Appは管理用タスクを1つのプロセスとして実行できるように、リリースステージに用意されていなければいけません。 ターミナルで操作をするのはこの用意されたプロセスを実行する時だけです。そして、このプロセスもコードベースでアプリと一緒に管理する必要があります。プロセスとして用意する理由は、アプリのバージョン毎に特有の設定があるはずで、その特有の設定を指定し間違えて不正なプロセスを実行してしまうことを避けるためです。
Discussion