📦

Cloud Nativeアプリケーション設計ガイドThe Twelve-Factor App

2022/07/31に公開

概要

Cloud Nativeなアプリケーション設計ガイドとして、Heroku社のエンジニアが書いたTwelve Factor Appというドキュメントがあります。10年を経てクラウドが発達しこともあり、現在ではPivotalのアーキテクトを務めるKevin HoffmanさんがアップデートしましたBeyond the 12 Factor Appというドキュメントが配布されています。
読んだ上で私自身もこれらの設計において非常に重要な指摘だと感じ、また思う部分もあったので投稿します。

Cloud Nativeな設計にはどのようなメリットがあるのか

端的に言えば、任意のクラウドインフラでHerokuのような、超高効率な開発・運用をクラウドベースのアプリ開発で行う上で必須となります。このガイドの対象はアプリケーションのみなのでインフラ側は別途注意して設計が必要ですが、両方合わせることで下記が可能になります。

  • CDによるブランチに同期した自動デプロイ
    • Pull Requestごとの検証用のような使い捨て環境の自動的な構築/破棄
    • Staging環境の自動デプロイ・そのコンテナの本番環境への昇格
  • 性能負荷に対するオンデマンドでのサーバーインスタンス増加による水平スケール
  • デプロイの容易なロールバック

高い品質を維持したまま継続的に要求の再確認と開発を続けるアジャイル方式とも親和性が高いです。

内容

Beyond Twelve-Factor Appでは項目は15個に増えましたが、想定する設計順序(下記)に合わせて進めます。

  • コード設計
  • コード・インフラ境界の設計
  • 開発・運用プロセス

コード設計

One Codebase, One Application

いわゆるモノリポ。アプリに対し1つのリポジトリでコードベースを管理する。マイクロサービスで構成する場合も、同一アプリであれば共通リポジトリとする。
この原則では、デプロイされた内容は全て1コミットに残り同じ内容のデプロイを複数の環境に行える。これにより、フロントエンド・バックエンドなどの依存関係を保証した状態を作れるので、CDが可能になる。
違った見方をすれば、これはコンウェイの法則(ソフトウェアと組織の構成が相似する)の反した構成をとったために、コミュニケーションや作業が増えるとみなせる。1アプリを複数チームでの開発も同様の理由で避けるべき。

逆コンウェイ等と言われるようなフィーチャーチームを構成することで、運用がシンプルになるとう好例のように思われる。また、CIもまとめてできるのでフロント/バックの結合チェックが可能になり、コードジェレータを使った場合に生成コードのバージョン管理が不要になる(CIで都度生成できるため)。

Dependency Management

暗黙の依存物(ライブラリなど)が前提になる構成は避ける。Ruby Gemやのような明示的に必要なライブラリを宣言できる機能を使って、どの環境でも最初からインストールできるようにする。
CIでゼロからインストールしてビルド直すことで、暗黙の依存物がないことを担保できる。

現在ではGoのworkspace modeなど、モノリポで共通の依存構成を利用する仕組みがある。また、これを使えない場合も可能なら共通の宣言ファイルを使った方がいい。エントリーポイントが増えた場合に、共通のライブラリであってもそれぞれを更新しなければならなくなるなど保守コストが増す。

Authentication and Authorization

セキュリティは必須条件であり、この機能を後から追加してはならない。

理想的には全てのエンドポイントにRBAC(ロールベースアクセス制御)により、誰からのアクセスで、そのユーザーがどのロールかを判定、その操作が実行可能かを判定できる。

この部分が揺らぐとコードも混乱したものになる印象。認可による絞り込みが前提にあることで、ユーザー起点でSqlを書くなどコードが早期にパターン化される。

ここで判定するのは操作が行えるかであり、単純な機能のように定義すると破綻しやすい。リソース×操作になるのでエンドポイントごとに定義してもいいかもしれない。

また、認可処理は下記三種類が考えられるためそれぞれ考慮が必要。これも事前に方針を立てないとコード的に破綻しやすい。

  • 単純な操作の可否
  • データ取得範囲の制限(企業内のデータのうち、自身が所属する組織分の閲覧できる、など)
  • データフィールドの制限(個人情報にあたる部分は本人しか見えない、など)

コード・インフラ境界の設計

Configuration, Credentials, and Code

DB接続情報、外部APIのURL、あるいはクレデンシャルなどの環境ごとの情報は環境変数に格納する。
yamlなどの設定ファイルは避けるべきである。個人✖️ステージングなどで組み合わせ爆発が容易に起こる。ただし、開発者はローカルで開発するので、そのやりやすさは

実際、クラウド環境は本番、ステージングだけでなく、開発での性能検証・E2E、営業のデモ用など使い捨て的なものも含めて発生する認識。そのようなものを、抜け漏れなく、ゴミが残らないように管理するのは難しい。
現実的にはローカル、テスト、クラウド環境の3種類になると思われる(クラウド環境でこの原則を使う)
Goであればviperのようなライブラリを使うことで、設定ファイルがあればそれを参照、なければ環境変数を使う制御が行え、各環境に対応できる。
Feature Toggleなどの用途でフロントエンドでもこの考えは必要になる。Firebase Remote Configなどのツールが使える。

Port Binding

アプリAPIは外部のwebサーバーを経由せず、任意のポートで公開し、クラウドプロバイダーがそのポートと実際に公開させるポートをつなぐ。これによりマイクロサービス構成で、アプリ間での呼び出しが可能になる。

Concurrency

プロセスごとにスケールアウトするように設計する。

何らかのコンテナ環境の場合はプロセス=コンテナの単位となるし、Lambdaなども選択肢になり得る。

Disposability

インスタンスのデプロイとその破棄に長時間かけると、その間に障害を誤検出したり、リリース作業そのものを高負荷にし、フィーチャーチーム構成が困難になる。
ここにロジックが必要になって時間が増加しないよう、キャッシュなどは後述のバッキングサービスに外部化する。

Backing Services

DB接続や外部APIは、先述の通り環境変数で接続先の情報を管理し、コードと独立してバッキングサービスとして管理する。
アプリコードを環境に依存した部分を減らすことで簡素にできるし、この結合を疎にすることで障害があるDBインスタンスの交換などが迅速に行え、可用性を高められる。
また、サイドカーなどを用いてhead-of-line問題に対応できる。head-of-line問題は、障害で無応答になった外部APIを使うリクエストをアプリが同時に複数受けた時、リソースを過大に消費してしまう現象。外部APIを待つだけの処理で数秒リソース使ってしまうのことが原因なので、間にサイドカーが入り、その場合にリクエストを即時に代理エラー応答することで待ち時間をなくす。

Stateless Processes

永続的に保存する情報は全てBacking Serviceに残し、アプリ側には一切持たない。これにより水平スケールが可能になる。

HTTPの基本の考え方だが、バッチ処理や管理プロセスも同じように捉えられる。その場合はよく言われるように処理が冪等になることを考慮する。それをしないとリトライができなくなるため。

Administorative Processes

管理プロセスは下記が該当する。それぞれは一回限りのプロセス(いわゆるバッチジョブ)として設計する。アプリケーションの一部なので、モノリポで管理して、API等と依存関係のギャップが起こらないようにまとめてデプロイする。

  • データベースマイグレーション
  • 調査用シェル
  • 夜間バッチなどの定期的な処理

AWS Lambdaが最適解となることが多い。

Log

ログはストリームとして扱うことで、アプリ側からは一方的に出力するのみにする。その発行先や、保存などは全てクラウド側で行う。少なくともコードが簡素になるし、インスタンスはいつでも破棄されるのでクラウド側に残さなければ対応できない。

開発・運用プロセス

API First

アプリ全体として、依存関係の中心にいるのはAPIであるため、そこから設計することで手戻りを避けられる。

要求仕様検討時でもデータから考えることが重要。最近ではGrapqhQLの一部のようにスキーマファーストの考え方もある。

Design, Build, Release, Run

下記のフェーズを明確に分ける。

  1. 開発: 依存物を、どのようにバンドルするかを検討
  2. ビルド: CIでバイナリを生成する。このバイナリはどの環境でも動くようにする。バイナリに対する自動テストもここで行う。
  3. リリース: リリースごとにバージョンタグを付与し管理する。これによりいつでもロールバック可能にする。
  4. ラン: 本番などはクラウド環境で、開発時にはローカルで動く必要がある。

Environment Parity

開発/本番環境の差異を縮小させる。開発環境で動くが本番で動かない、ような事態は環境差異が大きいほどおこる。

これは下記3種の軸で考えられる。

時間

開発からリリースまでの時間が空くと、リリース作業の抜け漏れがおこる。これのチェックは実際にデプロイしないと確認できないため、期間を縮めないと防げない。一ヶ月では長く、理想は数時間。

開発者とデプロイ実施者は共通であるべき。デプロイ内容を決定しているのは開発者であり、伝言ゲームを挟むと抜け漏れが発生する。CD化することで開発者のデプロイは行いやすくなる。

リソース

SQLiteなどで代用しても本番と差異は必ず発生しエラーとなるので、デプロイが信用できなくなる。
これの問題が起こった時、本番環境を使って調査しないといけないため、コスト・リスクが高くなる。
Dockerを使えばほとんど対応できる。

全てのコミットで本番デプロイ可能にすることを推奨。

Telemetry

監視・計測は大きく下記のカテゴリがあり、区別して実施する。

  • 画面表示時間などの性能
  • ビジネス上の意味のある情報
    • 多くの場合、ログストリームとしてデータウェアハウスに保存
  • ヘルスログ・システムログ
    • サーバーサイドで発生したエラーログや、自動含む構成変更時のログなど

水平スケールを前提に考えると、ログ量は爆増することが考えれる。初期設計時に忘れられやすいが、この部分はクラウドベースでのアプリ開発では成否に大きく影響する。

性能面ではRUM、エラーチェックではSentryのようなエラー収集ツールを使っても良い。

Discussion