精読「AWSで実現するモダンアプリケーション入門」(2)
AWSで実現するモダンアプリケーション入門
AWSを活用したモダンアプリケーション開発に興味がある方にとって非常に有益な一冊です。初心者から中級者向けにバランスよく解説されており、実践的な知識を得ることができる点が評価できます。
関連記事
モダンアプリケーションパターンの適用によるアーキテクチャの最適化
パターンとは
「特定の状況下で課題を解決するときに直面する目的や制約に対する解決策」
今回の方針
-
非同期的な通信、イベント駆動で処理するサービスにはLambdaを採用する
- Lambdaを利用することで、SQS/SNS/EventBridgeといったイベント発行元から後続サービスにイベントを伝搬するためのAWSサービスとの統合がしやすい
- Lambdaは抽象度が高いため、よりアプリケーション開発に集中できる
-
同期的な通信、APIを提供するサービスにはECSを採用する
- マイクロサービスにおいて重要な通信制御の運用負荷を軽減するためにApp Meshを使う
- 執筆時点ではLambdaにおけるApp Meshの利用がサポートされていないため、APIのバックエンドをECSにした(そうでなければ、API GatewayとLambdaの組み合わせも候補に上がる)
シングルページアプリケーション(SPA)
シングルページアプリケーションとは、静的コンテンツを取り扱うフロントエンドと、動的コンテンツを取り扱うバックエンドを分割し、APIのレスポンスを使って画面全体を再描画せず必要な箇所のみを更新するアーキテクチャ。
SPA(シングルページアプリ)より
今回採用したAmplify Hostingは、フロントエンドアプリケーションをビルドし、デプロイし、配信できるフルスタックなサービス
- サービス自体にCI/CDパイプラインも組み込まれている
- 静的コンテンツをドラッグ&ドロップでデプロイしたりリッチなUIを提供
- 内部にCloudFrontが組み噛まれている
API Gateway:API呼び出しの複雑性を集約する
-
API Gateway:クライアントに対する単一のエンドポイント
API Gatewayはエンドポイントとして単一だが、クライアントの種類を判別することで、それぞれの画面描画に必要な情報を取得できる(バックエンドのECSではそれぞれに必要な情報を集約して返している)。APIは、他のサービスへのリクエストをプロキシしたり、複数のサービスのAPIを呼び出して結果を集約する処理をしている。これにより、クライアントは1つのAPIを呼び出せばよく、複数サービスのAPIを呼び出す処理の実装が不要になる。
-
BFF(Backends for Frontends):クライアントごとに異なるエンドポイント
API Gatewayパターンではそれぞれのクライアントに対する単一のエンドポイントとしてAPIを公開していたが、BFFではそれぞれのクライアントごとに異なるエンドポイントを公開する。これにより、API Gatewayパターンでクライアントの種類が増えるごとにAPIが複雑になりモノリシックな構成になることを回避できる。
メッセージング:サービス間の非同期コラボレーションの促進
-
キューモデル
代表的なAWSサービスはSQS。大きなメリットはバッファリングで、リクエストのスパイクが発生してもキューで受け止めておける。
考慮点としては、メッセージの順序性。SQSの標準キューは無制限のAPI呼び出しをサポートするが取り出すメッセージの順序保証はしない。業務要件によって順序保証が必要な場合は、FIFOキュー(最大300回API呼び出し/秒をサポート)を採用するなど考慮する必要がある。
-
パブサブモデル
キューモデルが1対1のメッセージを送るのに対して、パブサブは1対多。パブリッシャーがバス(トピック)にメッセージとしてイベントを送信し、そのトピックを登録しているサブスクライバーがメッセージを受け取る。EventBridgeやSQSを使ってバス(トピック)を実現でき、Lambda関数やSQSキュー、その他のAPIエンドポイントにメッセージを一斉送信できる。
-
キューモデルとパブサブモデルを組み合わせる
同じ購入確定イベントを領収書サービスと通知サービスに送りたい、且つ領収書サービスと通知サービスはビジネス要件として即時である必要はないため、EventBridgeからそれぞれのSQSキューにメッセージを保存する
Saga:サービスにまたがったデータ整合性の維持
-
コレオグラフィ
コレオグラフィを直訳すると、「ダンスなどにおける振り付け」という意味。EventBridgeなどを使って実現できる。
-
オーケストレーション
Step Functionsを使ってオーケストレーションを実現できる。
CQRS:データの登録と参照の分離
購入履歴データをデータ登録する処理(コマンド)と参照する処理(クエリ)で分離する
-
登録サービス(コマンド)
- 購入履歴データを順次登録していくだけのアクセスパターン
- 特定の購入履歴データに対してレビューが投稿されたり、領収書が発行されたりするのも、イベントとして新たなデータを登録できる(購入履歴データを直接更新するのではなく)
- このように、大量のデータを登録し、更新せず時系列に積み上げていくアクセスパターンではキーバリューが適している
-
参照サービス(クエリ)
- 多くのユースケースがある(購入履歴データの一覧を参照したり、レビュー済の購入履歴や返本をした購入履歴を検索するなど)
- このようなアクセスパターンを考慮して、リレーショナルデータベースが適している
- また、DynamoDBにデータ変更があればキャプチャ情報[1]をストリーミングできるDynamoDB Streamsを使ってRDSに同期できる
イベントソーシング:イベントの永続化
ビジネスに重要なイベントをログとして、耐久性のあるストレージに保存し、他のサービスに伝搬するパターン。
-
DynamoDBを使った実装例
DynamoDBは大量のデータを保存でき、キーバリューであることから決まったスキーマを管理することなく複数のイベントを保存できる。
購入ID(パーティションキー) | 登録日(ソートキー) | ユーザーID | イベントタイプ | 詳細 |
---|---|---|---|---|
purchase001 | 2025-01-01 | user001 | 商品購入 | 商品Aを購入 |
purchase001 | 2025-01-02 | user001 | キャンセル | 商品Aの購入をキャンセル |
purchase002 | 2025-01-03 | user002 | 商品購入 | 商品Bを購入 |
purchase003 | 2025-01-04 | user003 | 商品購入 | 商品Cを購入 |
purchase003 | 2025-01-05 | user003 | レビュー投稿 | 商品Cのレビュー投稿 |
purchase004 | 2025-01-06 | user004 | 商品購入 | 商品Dを購入 |
-
EventBridgeを使った実装例
EventBridgeはイベントバスとして利用でき、幅広くメッセージを後続に伝搬できる。また、「アーカイブ機能」があり、保持期間を指定してイベントを蓄積できる。イベントソーシングとして使う場合、保持期間は無制限にするのが良い。さらに「リプレイ機能」もあり、アーカイブの範囲を開始時刻と終了時刻で指定してリプレイができるので、特定の期間に対して過去を遡って再処理をすることもできる。
サーキットブレーカー:障害発生時のサービスの安全な切り離し
同期的な通信の課題である1つのサービスの障害が全体に影響することを回避する方法として、サーキットブレーカーがある。
「クローズ」「ハーフオープン」「オープン」の状態により障害が発生したサービスを遮断し、サービス全体への影響を防ぐ。
サービスディスカバリ:サービスを見つける
疎結合にしたマイクロサービス間で他のサービスを探す時、サービスディスカバリとしてCloud Mapが利用できる。AWS CLIやAWS SDKなどAPIベースで接続先を問い合わせることや、DNSベースで接続先を問い合わせることができる
ECSはCloud Mapと連携でき、サービスインスタンスの登録と解除が自動で行うことができる。
ちなみに、Cloud Mapを利用する場合は負荷分散の仕組みをクライアント側に持たせる必要がある。(キャッシュしたインスタンス情報をラウンドロビンで順番に利用するなど)
サービスメッシュ:大規模サービス間通信の管理
-
ネットワークは信頼できない
一般的に「ネットワークは信頼できない」と考えられているため、以下のようなトラフィック制御や可視化の機能をAPIを呼び出すクライアント側に持たせる必要がある。
●リトライ/タイムアウト
●ヘルスチェック
●サーキットブレーカー
●サービスディスカバリ
●ログ/メトリクス/トレーシング
-
サービスメッシュ
一貫性のトラフィック制御や可視化を実現する方法として登場したのがサービスメッシュ。IstioやApp Meshは、プロキシ[2]を一元管理するためのコントロールプレーンを提供し、通信制御の設定はプロキシに対して動的に反映されるため、設定変更によるプロキシの再デプロイが不要になる。
-
App Mesh
App MeshはEnvoyを利用し、Envoyを管理するフルマネージドなコントロールプレーンを提供する。
ネットワークモデルとして、以下の要素を持っている。
メッシュ:サービスメッシュの論理的な境界。仮想ノードなどのリソースは、メッシュの中に存在する
仮想ノード:アプリケーションへの論理的なポインタ。アプリケーション間の通信を中継するEnvoyが仮想ノードにマッピングされる
仮想サービス:実際のサービスを抽象化したもの。サービス間の通信を行う場合、仮想ノードは仮想サービスに対してリクエストを送信する
仮想ルーター:リクエストのルーティングを管理するロードバランサ。仮想サービスに送信されたリクエストを仮想ノードに振り分ける
ECSタスクは、アプリケーションコンテナのサイドカーとしてEnvoyコンテナを実行しているので、App Meshのサーキットブレーカーやサービスディスカバリといった設定がEnvoyに反映され、それぞれのサービス間の通信制御を一元管理できる。
-
App Meshのサーキットブレーカー
App Meshはサーキットブレーカー機能として「外れ値の検出」を提供しており、検知されたら接続先サービスの候補から取り除く
フィーチャーフラグ:新機能の積極的なローンチ
-
フィーチャーブランチ
フィーチャーブランチを使えば並行開発を行えるが、長期化することによりコンフリクトの発生やコード修正量も増えるという課題がある。
-
フィーチャーフラグ
「リリース時にコードを書き換えることなく、動的にアプリケーションの振る舞いを切り替える仕組み」のこと。これにより、コンフリクトの発生やコード修正量の増加を回避できる。 -
フィーチャーフラグと設定の外部化
Systems Manager Parameter Storeにフラグ情報を外部化できる
import boto3
from botocore.exceptions import ClientError
def get_feature_flag(parameter_name):
# boto3クライアントの作成
ssm_client = boto3.client('ssm')
try:
# パラメータを取得
response = ssm_client.get_parameter(Name=parameter_name, WithDecryption=False)
return response['Parameter']['Value']
except ClientError as e:
print(f"Error getting parameter: {e}")
return None
def main():
# フィーチャーフラグのパラメータ名
feature_flag_param = '/myapp/new_feature_enabled'
# フィーチャーフラグを取得
feature_flag = get_feature_flag(feature_flag_param)
# フィーチャーフラグが有効かどうかを判定
if feature_flag == "true":
print("New feature is enabled!")
# 新しい機能を有効化する処理を追加
else:
print("New feature is disabled.")
# 新しい機能を無効化する処理を追加
if __name__ == "__main__":
main()
-
フィーチャーフラグの管理をサービスに任せる
CloudWatch EvidentlyはCloudWatchの機能で、フィーチャーフラグの管理やモニタリングを実現する -
フィーチャーフラグのタイプ
リリーストグル:新機能を事前にリリースしておく
実験トグル:機能を改善するときに効果検証をする=ABテスト
運用トグル:機能の様子がおかしいときに無効化する=ブレーカー
許可トグル:特別なユーザーにのみリリースする
分散トレーシング:サービスを横断するリクエストの追跡
-
トレースデータとX-Ray
X-Rayでは、サービスとして実行されているアプリケーションがユーザーからのリクエストを受け取ると、アプリケーションの処理した作業の詳細をセグメントというデータとして送信される。
トレースについて理解するより
Discussion