AWS

AWS ANGEL Dojo 2025で開発したサービスのアーキテクチャ詳細解説

numa

はじめに

こんにちは、ぬまです。
本稿では AWS ANGEL Dojoで開発したシステムの技術構成 に絞り、開発時の構成に沿ってアーキテクチャと技術選定の意図を整理します。(忘れないうちにまとめたいとは思っていたものの、ブログにするのが遅くなってしまいました・・・)

前提として Lambda や API Gateway の用語説明は最小限にし、技術選定理由を中心に書きます。

気になったセクションだけでも読んでいただけると幸いです。

ANGEL Dojo 2025 の概要や参加の動機、サービス「AI審査アシスタント mody」の全体像については、別記事 AWS ANGELDojo2025で最優秀技術賞を獲得しました! に投稿していますので、合わせてお読みいただけると嬉しいです。

あわせて読みたい
AWS ANGEL Dojo2025で最優秀技術賞を獲得しました!
AWS ANGEL Dojo2025で最優秀技術賞を獲得しました!

アーキテクチャ図

アーキテクチャ解説

図の各ブロックに対応する形で、利用者リクエストから審査ワークフローまでを簡単に解説します。

フロントエンド

React アプリは Terraform で用意した S3 と CloudFront に載せ、Cognito で認証します。本番のビルド時には API Gateway のベース URL(FastAPI 用と審査実行用)を環境変数として注入するため、インフラの出力とフロントのビルドが連鎖する前提になっています。

API と「審査実行」の分離

業務 API は 1 つの Lambda 上の FastAPI(後述の Lambdalith)に集約し、審査パイプラインの開始だけは別の API Gateway リソースから Step Functions の StartExecution に直接つないで非同期化しています。審査は長時間になりやすく処理時間が読めなかったので、HTTP の同期レスポンスとワークフローの寿命を切り離す意図です。

Step Functions とコンテナ Lambda

ステートマシンが、文書・画像レビュー用の コンテナ型 Lambda を並列実行します。開始時に DynamoDB の審査ステータスへ 進行中 などの状態を書き込み、各 Lambda 内で Bedrock を呼び出して結果を DynamoDB に蓄積する流れです。X-Ray トレースや Step Functions のログ設定も SAM 側で有効にしており、分散した処理の追跡を意識した構成にしています。

DynamoDB と RDS の役割分担

入稿データやルール・審査結果の多くは キー・バリュー中心でスループットを上げる用途として DynamoDB に構築しています。

一方、アプリのダッシュボードに表示する情報(一覧・絞り込み・担当者・チケットステータス・レビュー完了の有無など)は、想定される クエリパターンが多様になりやすいため、RDS(MySQL)側で管理しています。担当者アサインやレビュー完了フラグなどを SQL とインデックスで柔軟にクエリする前提で、ダッシュボード専用の行をリレーショナルに持ちます。「全部 NoSQL」でも実装は可能ですが、ダッシュボード要件の変化に GSI を増やし続けるより、チームが慣れた SQL と Alembic で進めやすいと判断しました。

技術選定

ここからは採用したコンポーネントごとに、選定理由を整理します。

DynamoDB と RDS

うまくいった点

  • 入稿が 1案件あたり1レコードに対応する形でまとまるため、DynamoDB のパーティション設計と一覧・更新の単位が揃えやすいです。
  • 審査結果やステータスを書き込む処理が Lambda から並列に走っても、スキーマ変更の少ないキー値ストア向きの負荷特性に寄せられます。
  • ダッシュボード向けのデータを RDS に寄せたことで、担当者・ステータス・期間など 条件の組み合わせが増えても SQL で表現しやすいです。画面要件が変わったときの逃げ道を広くしています。
  • RDS 側は ダッシュボード用のステータス行のように、行単位で更新頻度が高く、インデックスで一覧を引きたいデータに寄せられ、マイグレーションの責務(Alembic)も一箇所にまとまります。

負担やリスク(二系統の突き合わせ)

  • データが二系統に分かれるため、ダッシュボードでは RDS の行と DynamoDB の入稿・審査結果を API 内で突き合わせる必要があります。キャッシュや取得順の設計を誤ると N+1 やレイテンシのばらつきの原因になります。
  • DynamoDB のアクセスパターンを後から変えると GSI 追加やテーブル設計の見直しが伴いやすいです。

Lambdalith

REST エンドポイントを FastAPI アプリ=Lambda(コンテナイメージ) にまとめる Lambdalith 方式を採用しました。関数をエンドポイントごとに分割する方式と比べ、ルーティングとミドルウェアをアプリ内で完結させられます。

Lambdalith 方式のメリット

  • ルート追加が「Lambda 追加+API Gateway 設定追加」に比べ、同一責務内では開発速度とレビュー単位が扱いやすいです。
  • 後述の OpenAPI / Swagger により、プロセス境界ごとに仕様を共有しやすいです。

ただし Lambda を VPC 内に置くと設計が一気に複雑化する、という論点があり、ANGEL Dojo ではここはしっかりと議論しました。

VPC 内 Lambda が複雑になる理由

  • RDS など VPC 内リソースへ経路が必要がある処理だけが VPC 内に存在すればよいのですが、Lambdalith で 1 本の Lambda に全部載せると、VPC に入れる必要のない処理まで VPC 内から実行されることになります。
  • VPC 内の Lambda がパブリック API の AWS サービス(DynamoDB、S3、SSM など)にアクセスするには、VPC エンドポイントNAT ゲートウェイ経由のいずれかが必要になります。

今回の構成では Cognito を使ったアプリケーション側の処理も Lambda で実装していましたが、当時はCognito 用の VPC エンドポイントは存在しません(現在はCognito用のVPCエンドポイントは存在しています。https://aws.amazon.com/jp/about-aws/whats-new/2025/11/amazon-cognito-user-pools-private-connectivity-aws-privatelink/)。
そのため「VPC 内 Lambdalith 1 本だけ」にすると、Cognito など VPC 外でよいはずの処理のために NAT ゲートウェイを立てるかという議論に落ちます。コストの観点から、不要な NAT だけを増やしたくない、という判断になりました。

結果としての構成

  • VPC 内 Lambdalith(例: RDSなど、VPC 接続が必要な業務 API 一式)
  • VPC 外 Lambdalith(Cognito 連携や Slack 通知など、VPC に入れる必然性が薄いエンドポイント)

2 本に分けました。SAM では VPC 内用のメイン API 用 LambdaVPC 外用の補助 API 用 Lambdaとして、別リソースに定義しています。コードベースは FastAPI のままですが、デプロイ単位として FastAPI で実装された Lambda が二つあるイメージです。

2 本のLambdalith(VPC 内/外)に分けて得たこと

  • VPC 内に載せる Lambda の責務を絞れたことで、VPC エンドポイントを用意する範囲と本数を抑えられアーキテクチャをシンプルに保てました。
  • VPC エンドポイントにかかる時間課金(インターフェイス型エンドポイント)も抑えられました。

運用で増えること

  • コールドスタートやメモリ設定は「関数ごと」ではなく Lambdalith ごとに寄るため、重い処理と軽い処理の混在時はチューニングの妥協点が出ます
  • 2 本の API スタックになるため、デプロイ・監視・ロールの境界が増えます。エンドポイントをどちらに載せるかのルールをチームで共有する必要があります。

FastAPI × Mangum

採用効果

  • OpenAPI ドキュメント/docs など)をそのままフロントへの仕様のたたき台にできました。ANGEL Dojo期間でも「口頭のすれ違い」を減らす効果は大きかったです。
  • Pydantic でリクエスト/レスポンスを型付けし、Lambda 上でも同じコードパスをローカル(Uvicorn)で再現しやすいです。

DevContainer とコンテナ型 Lambda

バックエンド用 DevContainer は 開発用 Docker Compose と結合し、Terraform / AWS CLI / Session Manager など 本番に近いツールチェーンをコンテナに閉じ込めています。

ANGEL Dojo のチームは 経験年数や得意領域がそろわない編成であり、初学者の方にも参加いただく前提でした。各人の PC に Python/Node/Terraform のバージョンをそろえて入れる、という 個別の初期セットアップに時間を取られないことが重要だと考え、DevContainer を軸にしました。リポジトリを開き、コンテナを起動できれば 短時間で開発のスタートラインに立てられる状態を目指し、結果として 全員が環境構築ではなく本来の機能開発に集中できるようにしています。

チーム全体の開発速度

  • 初学者を含むメンバー全員が、初期セットアップを短く済ませて開発に入れます(「自分の環境だけ動かない」という状況を回避できます)。
  • コンテナ Lambda により、レビュー処理に必要なランタイム・ライブラリを 本番イメージに近い形で持てます。

CI/デプロイのオーバーヘッド

  • Lambda を Zip デプロイ(ソースや依存をまとめたアーカイブをアップロード)する方式に比べ、コンテナイメージではデプロイのたびに イメージのビルドと ECR へのプッシュを行うため、イメージが大きいほど デプロイ時間が伸びやすいボトルネックになり得ます。SAM で sam build --use-container のように ビルドをコンテナ内で行う場合も、Docker レイヤーやパッケージキャッシュの設計を誤ると毎回フルビルドに近くなり、同様に時間がかかりやすいです。
  • Lambdalith はアプリ一式を 1 イメージに載せるため、ビルド対象がまとまって大きくなりやすく、当初は「デプロイが重くなるのでは」と懸念しました。実際の開発では おおむね 5 分程度で収まり、そこまで深刻なボトルネックにはならなかったという体感でした。

ログフォーマットの統一に Powertools for AWS Lambda

API 層と審査ワークフローの両方で Powertools for AWS LambdaLogger を使い、JSON ログとサービス名などの共通属性を揃えています。SAM の全体設定でログ形式を JSON にそろえ、CloudWatch Logs Insights で横断しやすくする意図です。

揃えた効果

  • 構造化ログのキーが揃い、障害調査時にフィルタしやすいです。
  • FastAPI の例外ハンドラからも同じロガーに流せます。

運用ルール

  • ログ量とコストのバランス(本番で DEBUG を広げすぎないなど)は運用ルールの整備が別途必要だと感じました。
  • AWS Lambda Powertools 自体の欠点というより、チーム運用の話ですが、どのログを DEBUG にし、どれを INFO にするかを事前に軽くでも認識合わせしておけば、開発者ごとの出力の差や本番ログのノイズを減らせたかもしれません。フォーマットは揃っても、レベルの付け方は個人差が出やすいので、振り返りとして次に活かせるポイントでした。

IaC に Terraform と SAM

Terraform で VPC・RDS・DynamoDB・Cognito・S3・踏み台など ライフサイクルが長く、チームのインフラ定番になりやすいリソースを管理し、SAM で API Gateway・Lambda・Step Functions など アプリリリースの頻度が高いサーバレスを管理しています。

手元のタスク実行(例: SAM のビルド/デプロイ)や GitHub Actions のインフラ系ワークフローでは、Terraform の出力を SAM のパラメータに渡して スタック間の依存を明示的に解決しています。

ツール分担のメリット

  • ツールの得意領域に寄せられます。SAM の sam build とコンテナイメージの相性も活かしやすいです。
  • Terraform の state と SAM のスタックを分けられるため、権限や変更頻度の違いをチームで説明しやすいです。

2スタックの注意点

  • 二つのスタックをまたぐため、デプロイ順序と output の整合が壊れると即不整合になります。CI/CD パイプラインで terraform apply のあと sam deploy を固定順で流すことで緩和しています。

Bedrock 呼び出しに strands を使用

strands とは

Strands Agents SDK(PyPI では strands-agents など)は、Python で エージェント型の生成 AI アプリを組み立てるためのオープンソースのフレームワークです。Agent にモデルとシステムプロンプトを渡し、ユーザ入力相当の文字列を渡して実行する、というシンプルな入り口が用意されています。

モデル提供者として Amazon Bedrock を使う場合は strands.modelsBedrockModel を指定し、bedrock-runtime 経由で推論します。AWS の公式ドキュメントでは、Strands と Amazon Bedrock AgentCore(メモリやランタイムデプロイなど)を組み合わせる例も載っており(Strands Agents SDK と AgentCore)、エージェント実装とセットで語られることが多いです。公式サイトのドキュメントは Strands Agents から辿れます。

補助的に strands-agents-tools のようなツールパッケージもあり、画像読み取りなど必要に応じてエージェントに能力を足せます。mody では画像レビュー用 Lambda に strands-agentsstrands-agents-tools を入れて画像への審査を実現しています。

Bedrock API を直接呼ぶ場合との比較

ここでいう「直接」は、boto3bedrock-runtime クライアントに対し、converseinvoke_model などを 自前で組み立てるやり方のことです。

観点Bedrock API 直接Strands(Agent + BedrockModel
ボイラープレートメッセージ配列、システムプロンプト、推論パラメータを API 都度・モデル都度で組み立てる必要があるモデル ID・リージョン・温度・タイムアウトなどを BedrockModel に集約し、呼び出し側はプロンプト文字列に寄せられる
API の変化への追従Bedrock のリクエスト/レスポンス形式の差分をアプリコードがすべて吸収する提供者実装側(SDK)のアップデートに一部委ねられます。アプリはエージェントのインタフェースに留めやすい
エージェント機能の拡張ツール呼び出しやマルチターンを自前でループと状態管理として実装する必要があるAgent とツール定義の組み合わせで、後から画像ツールや追加ステップを足しやすい
依存と制御依存は boto3 のみに抑えやすく、細部まで制御したい場合向けフレームワーク分の依存と学習コストが増える。挙動のデバッグは SDK 内部も意識する必要がある

ANGEL Dojo での開発において、Strands が優れている点は次のような観点です。

  • 審査ロジックを「エージェント1呼び出し」として表現できます。直接 API でも実現は可能ですが、推論パラメータとプロンプト組み立てが関数全体に散らばりやすく、Strands では 境界が Agent に集約されます。
  • 将来の拡張のしやすさです。ルール追加やツール連携が進んだとき、converse の生 JSON をいじり続けるより、エージェント層に載せた方が変更点が追いやすい。
  • Bedrock 固有のオプション(ガードレールやキャッシュなど、SDK がサポートする範囲)を、自前で JSON を組まずに設定に寄せられる。

一方で、レイテンシの最小化やバイナリプロトコルの完全制御が最優先なら、直接 API の方がシンプルなこともあります。また Strands は抽象化の分、不具合やバージョンアップ時には SDK 側の挙動も確認対象になります。

ANGEL Dojo では、短期間でチームが同じパターンで Bedrock を触り、あとから審査要件が増えても エージェント周りに変更を閉じやすいことを優先し、Strands を選びました。

ANGEL Dojo での使い方

文書・画像の審査ロジックでは AgentBedrockModel を使い、プロンプト実行とモデル設定(リージョン、タイムアウト、温度など)を エージェント層に閉じる形にしています。レスポンスは JSON 抽出に失敗した場合のフォールバックもコード側で持ち、生成物のブレを運用で許容できるラインに収める工夫をしています。

実装で揃えられたこと

  • Bedrock の呼び出しパターンを Python 側で統一し、モデル ID やリージョンの切り替えを環境変数ベースにしやすい。
  • トークンやコストのメタ情報を レビュー実行単位で集計する処理で追い、後から分析しやすい。

生成まわりのメンテ

  • モデルやプロンプトの微差で出力形式が変わるため、パース処理とプロンプトの両方をセットでメンテする必要があります。

単体テストに Amazon Q Developer を使用(AI オンボーディング)

ここでいう AI オンボーディングは、「とりあえず Q にコードを書かせる」ことではなく、プロジェクト固有の前提をドキュメントとルールで先に渡し、Q の振る舞いを開発フローに乗せることを指します。一般的な言語知識やフレームワークの定石は LLM 側にありますが、リポジトリ構成・仕様・コーディング規約や開発ルールは学習データにないので、AmazonQ.mdREADME.md.amazonq/rules/*.md などで明示的に与える、という整理です。

「テスト仕様書(開発者)→ テストコード生成と整形・実行(Amazon Q)→ 結果のサマリ(Amazon Q)」 という一連の流れに沿った運用です。ざっくり次のとおりです。

  1. テスト仕様書の更新(開発者)
    docs/job_test/ 配下などに、ジョブ(または対象モジュール)ごとの Markdown で 単体テスト仕様を書きます。テスト ID・観点・入力データ・期待結果を表形式でそろえ、どの入力に対してどういう出力(または失敗)を期待するかが一目で追えるようにし、Amazon Q がそのままテストコードを作成できる粒度にします。
  2. テスト生成の依頼(Amazon Q)
    仕様と実装を踏まえ、Amazon Q にテストの追加・更新を依頼します。実装のブラックボックス化ではなく、仕様書に書いたケースをコードに落とすのが主目的です。
  3. フォーマットと単体テストの自動実行(Amazon Q)
    リポジトリに用意したテスト実行用のシェルスクリプトを例外なく通す前提で回します。
  4. コンテキストファイル(AmazonQ.md
    Amazon Q は 推測や一般論だけでのコード改変を禁止し、テスト結果や仕様書に基づいてだけ修正案を出す、コード(テスト含む)を変えたら必ずフォーマッタとテストを再実行する、変更内容は 仕様ドキュメントと突き合わせる、といった 開発全般のルールAmazonQ.md に書き、Amazon Q が自動で読み込む前提にしました。

CI でテストを必須ゲートにしていたわけではありませんが、開発者が期待値を仕様書に書き、Amazon Q はその記述と Pytest の結果だけを手がかりにするという分担にすることで、単体テスト追加のスピードと、チーム内での説明可能性の両方を実現しています。期待値の最終判断は常に開発者側(仕様書の記述)に置く、という前提は変わりません。

負荷試験に Distributed Load Testing on AWS

本番に近い構成でスループットとエラー率を見るため、Distributed Load Testing on AWS を用いて API とワークフロー周りの余裕を確認しました(閾値や数値は環境ごとに異なるため、ここでは割愛)。サーバレスはスケールしやすい一方、同時実行数・レート制限・下流の Bedrock がボトルネックになりうるため、そこを意識したシナリオ設計が重要でした。

おわりに

以上が mody のアーキテクチャと主要な技術選定の整理です。ANGEL Dojo の短期間でも「自分たちが説明できる構成」に寄せることを意識し、チーム全員で。

さまざまな角度から技術的な説明をしました。読み手によって関心が分かれると思いますが、どれか一つのセクションでも有意義な情報になりますと幸いです。

AUTHOR
numa
numa
記事URLをコピーしました