モダンなWebアプリのあるべき姿 The Twelve-Factor App (AWSやIaCであるTerraformと絡めた話)
概要
- 先日、弊社の情報システム部門で開催されている勉強会にお呼ばれいたしまして、「モダンなWebアプリのあるべき姿 The Twelve-Factor Appとは?」という内容でお話しさせていただきましたので、その内容についてブログとして記載していきたいと思います。
- 内容なのですが、The Twelve-Factor AppのそれぞれのベストプラクティスとAWSを使った場合の適合方法、それぞれについての理解とモダンなwebアプリ開発など絡めたものになっております。
Twelve-factor Appって??
- モダンなWebアプリケーションのあるべき姿として、12のベストプラクティスにまとめた方法論
- Herokuプラットフォーム上で開発・運用・スケールした何百何千ものアプリケーションから得られた知見が基になっている
- 2012年に提唱。少々古い一面もあるが、現在でも示唆に富む数々のプラクティスが得らる
対象 : サービスとして動くアプリを開発しているすべての開発者
何が嬉しい?
- プロジェクトに新しく加わった開発者が要する時間を最小化できる
- 実行環境間での移植性を最大化できる
- モダンなクラウドプラットフォーム上へのデプロイに適しており、サーバー管理やシステム管理を不要にできる
- 開発環境と本番環境の差異を最小限にし、アジリティを最大化する継続的デプロイを可能にできる
- ツール、アーキテクチャ、開発プラクティスを大幅に変更することなくスケールアップできる
The twelve-factor App
- 今回対象とするのは、12個あるベストプラクティスの内5つのみとします
Ⅰ. コードベース
バージョン管理されている1つのコードベースと複数デプロイ
- バージョン管理、コード管理 : GitHub, GitLab, CodeCommit
- ここでのコードベースという言葉の意味が若干曖昧
- リポジトリを指す場合
- ブランチを指す場合
1つのリポジトリで管理するアプリが同じ
- ここでは、1つのリポジトリで管理するアプリが同じ場合を例に挙げています。
- この場合、Twelve-factorに適合しているパターンというのは、単一のブランチで複数の環境に対しデプロイすることが良いといっています。
- 逆に複数のブランチ、例えばリリースブランチで分け、それぞれのリリースブランチ毎に異なる環境にデプロイするといった場合は、Twelve-factorに適合していないパターンといえます。
- なぜか、それは環境毎にリリースブランチを分ける場合、本当にそれぞれのリリースブランチ毎に差分がないのかということです。
- 例えば、Hotfixが生まれ変な逆mergeが起きた場合、それによってリリースブランチに差分が生まれてしまうためです。
1つのリポジトリで複数のアプリを管理する =>マイクロサービスとか
- 今度は、1つのリポジトリで複数のアプリを管理するような場合、例えばマイクロサービスとかがそれに該当します
- 1つのリポジトリ内でディレクトリでそれぞれのアプリを分け管理する場合、Twelve-factorに適合していないパターンというのは、先程と逆になります。1つのリポジトリで同一環境にデプロイするような場合ですね。
- これ何が問題かというと、マイクロサービスは組織論とよく言いますが、アプリを管理するチームが同一のリポジトリに変更を加えまくったら、コードの品質の担保ができなくなったりコンフリクトも発生しまくりますよね。
- これを無くすために、ここでいっているのはアプリ毎にリポジトリは切り分けるべきで、それぞれ毎を同一環境にデプロイするようにすべきだといっています。
ブランチ戦略
- 開発する際に一人でゴリゴリやるというのであれば、ブランチ戦略もクソもない
- アプリケーション開発をやるとチームで開発
- 複数の開発者がPushしていくとメインブランチには複数の変更が常に入り乱れた状態となる
=> コード品質の担保や、コードの全体像の把握が難しくなる
一本のブランチで開発を進める代わりに、個々が行う開発はメインブランチから分岐したブランチ(featureブランチなど)で行い、開発が完了したらメインブランチへのマージリクエストをする
さまざまなブランチ戦略
- 下記のようにブランチ戦略といってもさまざまです。チームの思想に依存しかなり分かれるのではないでしょうか。
- ただ、ここで言いたいのは繰り返しになりますがブランチ戦略においては、環境毎にリリースブランチを分けるようなブランチ戦略の場合、本当にそれぞれのリリースブランチ毎に差分がないのか。Hotfixが生まれ、変な逆mergeが起きていないか。それによってリリースブランチに差分が生まれていないのかということです。
- もし、変な逆mergeが起き環境差分がでてしまうのであれば、単一ブランチで運用するのがいいというのが、1つ目のベストプラクティスになります。
Ⅱ. 依存関係
依存関係を明示的に宣言し分離する
よくある依存関係
- システム全体にインストールされるパッケージが暗黙的に存在してはいけない
- 実行時にはパッケージ管理ツールを使って、システムから暗黙の依存関係が抜けないことを保証
- パッケージ管理ツール使えば、環境毎に依存するパッケージのversionも異らない(ように設定もできる)
IaC (例えばterraformの場合)
Terraformでもこのプラクティスは使えそう
- State fileの管理
- 環境毎のModule管理
ちょっと待ってTerraformって何?
-
インフラを安全かつ効率的に管理する為のツール
-
terraformでは、terraformのsyntaxで記述されたファイルを利用しawsなどに資源を作成しにいきます。(左側の図がtsファイルのサンプル)
-
tfファイルが作成できたら、terraformのcliを利用し、図だとaws環境に対して資源作成を行なっています。その時に自動で生成されるのが、tfstateファイルというものです。
tfstate file
- tfstate fileは、Terraformによって構築したリソースを記録するためのファイル
- このstate fileの管理方法については、Terraformで資源を当てると自動で作成されるので、それを開発者間で依存しないようRemoteで管理するのが良いとされます
- Terraform管理していない手動作成のS3バケットに作成が良く、かつ環境別tfstateを保持することで環境毎に依存関係を明示的にすることができます。
各種資源の書き方と管理方法
- 各AWSのサービス毎にファイルを作成
- 環境毎に作成した資源を呼び出すようにディレクトリ毎変えると良い
=> 何が嬉しいか
それは、環境毎に依存するmoduleを分離できるというメリットがあります。
Ⅲ. 設定
設定を環境変数に格納する
-
Twelve-Factorでは、設定をコードから厳密に分離することを要求します
そのため、環境変数で設定する必要があるということです -
なぜなら、コードを変更することなくデプロイごとに簡単に設定を変更できるうえ、誤ってリポジトリに混入する可能性もほとんどなくなるからですね
まあ、一発でこの項目についてtwelve-factorに適合しているか、そうではないかを見極める時には以下の質問を投げかけてみてください。
認証情報を漏洩させることなく、コードベースを今すぐにでもオープンソースにすることができるか?
AWSを使うと
- AWS Systems Manager - Parameter Store
これは、設定データ管理と機密管理のための安全な階層型ストレージです。
- パスワード、データベース文字列などのデータをパラメータ値として保存可能で、プレーンテキストまたは暗号化されたデータとして保存可能となっています。
よくあるパターン
- 例えば、ECSで実行するコンテナがあった時に、コンテナ内で実行するアプリで使用するデータベースのパスワードをParameter Storeを使用し、KMSで暗号化して、ECSのタスク定義で読み込ませるとかですね
Ⅳ. バックエンドサービス
バックエンドサービスをアタッチされたリソースとして扱う
- バックエンドサービスというのは : アプリケーションがネットワーク越しに利用するすべてのサービス
- 例えば・・DB, キューイングシステム, キャッシュ, 監視サービス, 外部APIなどです。
Twelve-Factor におけるバックエンドサービス
- リソースは自由にアタッチ/デタッチできるようにする
- DBに問題が起きた場合、既存のDBをデタッチし、バックアップから復元したDBをアタッチできるようにする
- ローカルサービスとサードパーティサービスを区別しない
- すべてのバックエンドサービスを設定に格納されたURLでアクセス可能にする
- ローカルのMySQLをサードパーティサービス(RDSなど)に切り替えることができる
なぜ?? => コード修正が必要になると環境ごとにビルドが必要となってしまうからです
AWSにおける具体的なバックエンドサービス例
バックエンドサービスを扱う上で重要なのは可用性
-
AWSでも/サードパーティのサービスでも遅延や、停止することを考慮しないといけない
-
例えば、スロットリングするAPIを使用している場合、どうしますか?
- 単純にRate Limitを確認して、必要に応じて上限緩和申請?
- 上限緩和ができない場合は??=> キャッシング、キューイング、シャーディングなどのデザインパターンを導入しバックエンドの負荷を緩和する必要がある
Exponential Backoff / Jitter
- クライアントアプリからAWS SDKを使用して、例えばDynamoDBをデータストアに処理を実施したとする
- アプリを使用するユーザが急激に増えた時、処理しきれなくなり待ち行列が発生
- 結果、スロットリングが起こった全ユーザが同時にリトライすると、リソースを急激に食い潰す
=> そこで、Exponential Backoff / Jitter
Exponential Backoff
- 直訳すると「指数関数的後退」指数関数的に処理のリトライ間隔を後退させるアルゴリズム
Jitter
- Random関数を用いてリクエストするタイミングをずらす
- バックオフでも解決しない場合、オプションで使うイメージ
- 失敗したすべてのコールが同じ時間にバックオフされた場合、再試行時に再び競合または過負荷が発生
通常状態を図で表すと・・
引用: https://aws.typepad.com/sajp/2015/03/backoff.html
Exponential Backoffを図で表すと・・
引用: https://aws.typepad.com/sajp/2015/03/backoff.html
Jitterを図で表すと・・
引用: https://aws.typepad.com/sajp/2015/03/backoff.html
AWSにおけるExponential Backoff / Jitter
-
部分的で一時的な障害のためにサービスを止めたりしていると可用性に影響が出ます
-
各AWS SDKには自動リトライ処理が実装されているので、そのクラスを使おう
-
もしAWS SDKを使用していない場合は、サーバーエラー(5xx)やスロットリングエラーを受け取ったら同一のリクエストを再送するよう実装する
ただし、この辺りの実装すると変なバグを生むので、なるべくならAWS SDKに機能を使う
V. ビルド、リリース、実行
ビルド、リリース、実行の3つのステージを厳密に分離する
Twelve-Factorな、3つのステージ
- ビルドステージ : コードリポジトリを実行可能なアーティファクトへ変換
- リリースステージ : ビルドステージで生成されたアーティファクトを受け取り、それをデプロイの現在の設定と結合=> アーティファクトと設定の両方が含まれ、実行環境の中ですぐにでも実行できるよう準備が整う
- 実行ステージ(ランタイム) : 選択されたリリース資源に対して、アプリケーションを起動することで、実行環境の中で実行
モダン開発手法でのCI/CDとは
- 継続的インテグレーション - Continuous Integration (CI)
- 継続的デリバリー/デプロイ - Continuous Delivery/Deployment (CD)
そもそもCI/CDの目的は?
- ソフトウェアのビルド/デプロイ/テスト/リリースというプロセスのあらゆる部分が関係者全員から見える化
- フィードバックを改善し、プロセスにおいてできる限り早い時期に問題が特定されて解決されるようにすること
- あるソフトウェアの任意のバージョンを任意の環境に対して、完全に自動化されたプロセスを通じて好きなようにデプロイできるようにすること
いや分からん!!
簡単にまとめると
-
継続的インテグレーション (CI)
- 継続的にコードをチェックインして開発者の最新のコードがマージされた状態にしておくこと
- 安全にマージを行うために自動ビルドや自動テストを構築すること
-
継続的デリバリー/デプロイ(CD)
- デプロイパイプラインを構築すること
- パイプラインの可視化をすること
CI/CDにおいて重要なことを問う
-
継続的インテグレーション (CI)
- 何を継続的にインテグレーションしたいのか => コード変更を統合したい
-
継続的デリバリー/デプロイ(CD)
- なんで継続的にデリバリーしたいのか
- => 例: 変化する顧客の要求への対応力を高めたい
リリースプロセス例
- 下記にリリースプロセスの例を記載しました。
- ここで注意としては、CDについてです。継続的なデリバリーと言った場合に、例えばチームにQAの方がいてテストをやってくれるとします。その場合は、キェパシティステージにbuildした資源をデプロイし、その後の承認を経てProduction環境にデプロイするといった感じになるでしょう。
- ただ、そのQAのテストも全て自動化し承認のフローも含め自動化した場合は、一連の流れをパイプラインとして落とせばいいと思います。
継続的デリバリー/デプロイ
実例
- 私が所属しているチームでは、artifactの作成にはCodeBuildを使用しています
- CodeBuildを使用すると、GitHubからHookをかけ、MainブランチにMergeされたタイミングでCIすることができます
- CDについても実はCodeBuildを使用しているのですが、一連の資源をデプロイするためのCodeBuildの実行をStepFunctionsを使用し逐次実行する形にしています
- StepFunctionsを使用すると逐次実行しつつ、特定のCodeBuildに関しては並列で実行させたり、その後の処理に承認が欲しい場合は承認されるまで待つといった実装をすることもできます
Discussion