はじめに
いかにもAIが考えたであろうタイトルはさておき、
既存のReactシステムに「業務メモ的なチャット機能」を埋め込みたいという要望がありました。
すでに稼働しているシステムへの追加実装であるため、以下の要件が求められました。
-
既存のAWSリソースには変更を加えず、別アカウントで構築する
-
既存のバックエンド・フロントエンドへの影響を最小限にする(フロントはコピペレベルで完結させたい)
-
汎用性の高い構成にして、今後別のシステムでも使えるようにする
てなわけで、完全に0からのAWS設計が必要となりました。
私にとってAWSの設計は初めての経験でしたが、今回はサーバーレス構成を採用したことで、AWS初心者として非常に多くの学びを得ることができました。(AWS設計入門の題材として最適すぎた)
本記事では、その設計過程と学んだことをまとめます。
最終的に作成した画面(component)
チャットはlambda経由でdynamodbに保存されます。
最初に検討した構成
最初はこんな構成を考えていました。
特徴
- AppSync + GraphQL でリアルタイム通信(Subscription)
- Apollo Client をフロントエンドで使用
- WebSocket によるリアルタイム更新
なぜこの構成を検討したか
- リアルタイムでメッセージが届く体験を提供したい(チャットなので)
- AppSync の Subscription は WebSocket 接続を自動管理してくれる
構成を変更した理由
要件を改めて整理したところ、この構成はオーバースペックと判断しました。
要件の再整理
| 要件 | 優先度 | 備考 |
|---|---|---|
| メッセージの送受信 | 必須 | 基本機能 |
| リアルタイム通知 | 低 | 手動更新で十分(業務メモレベルなので) |
| 導入の容易さ | 高 | コピペで導入できたらいいな |
| 外部依存の最小化 | 高 | 使うシステムの package.json を汚したくない |
AppSync 構成の課題
-
Apollo Client への依存
- ホストシステムに Apollo Client を追加する必要がある
- 「コピペで導入」はできなそう
- 導入を予定しているシステムではremixを使っていたので相性が悪そう
-
WebSocket 接続の管理
- 常時接続のコスト
最終的な判断
シンプルな REST API + 手動更新 で十分だと判断しました。
AppSync + GraphQL + Subscription
↓
API Gateway + Lambda + REST API
採用した構成
最終的に採用した構成がこちらです。
変更点
- AppSync → API Gateway (REST)
- Apollo Client → ネイティブ fetch
- Subscription → 手動更新ボタン
採用した技術スタック
| レイヤー | 技術 | 選定理由 |
|---|---|---|
| API | API Gateway (REST) | WAF対応、スロットリング内蔵、シンプル |
| 認証 | Lambda Authorizer | JWT検証を分離できる |
| Compute | Lambda | サーバーレスで運用負荷ゼロ |
| Database | DynamoDB (On-Demand) | スケーラビリティとコスト効率 |
| Security | AWS WAF | アプリ層でのDDoS対策等 |
| IaC | AWS CDK (TypeScript) | 使いたかった |
設計思想① Lambda Authorizer による認証分離
最初は、一つのLambdaで認証とAPIロジックを完結させようとしていました。
が、同期にアドバイスをもらい、認証とAPIロジックを分離するために Lambda Authorizer を採用しました。(APIGatewayに統合できるの知らなかった)
アーキテクチャ
メリット
- 認証で弾くべきリクエストがAPIロジックLambdaに到達しない
- 関心の分離(認証ロジック vs APIロジック)
- 認証結果をキャッシュできる(最大1時間?、同一トークンの再検証を省略)
設計思想② DynamoDB の Single Table Design
DynamoDB では Single Table Design を採用しました。
設計パターン
AdminChatTable(1つのテーブルに全エンティティ)
├── PK: MSG#{prefix}#{roomId}
└── SK: TS#{timestamp}#{uuid}
この設計のポイント
| 観点 | 設計意図 |
|---|---|
| パーティションキー | データを論理的に分離。アクセスパターンに基づいて設計 |
| ソートキー | タイムスタンプ順でソート可能に。末尾のUUIDで衝突を防止 |
| TTL | 古いデータを自動削除してコスト削減 |
なぜ Single Table Design か
- 1回のクエリで必要なデータを取得できる
- アクセスパターンが明確になる(どのKeyでどんなデータを取るか)
- 運用がシンプル(テーブル1つだけ管理すればいい)
GSIも今回の要件的に必要なかったのでめちゃくちゃシンプルになっています。
ソートキーの衝突回避
TS#{timestamp}#{uuid} とすることで、ミリ秒単位の同時書き込みによるデータ消失(上書き)を防ぐ設計。
設計思想③ 環境分離(dev / prod)
必須ではありませんでしたが、本番運用を見据えて最初から環境分離を設計に組み込みました。
| 項目 | dev | prod |
|---|---|---|
| WAF | なし | あり |
| CloudWatch Alarms | 最小限 | フル設定(Slack通知) |
| DynamoDB TTL | 30日 | 2年 |
| Point-in-Time Recovery | なし | あり |
| ログ保持期間 | 7日 | 1年 |
CDKではstageパラメータで分岐
// prod環境のみ監視設定を追加
if (stage === 'prod') {
new ChatMonitoring(this, 'Monitoring', {
...
});
}
- 開発中は余計なコストがかからないので🙆
- 環境ごとの設定差異がコードで明示されるので🙆
設計思想④ WAF による保護
本番環境では AWS WAF を導入しました。
設定したルール(一部抜粋)
| ルール | 設定値 | 目的 |
|---|---|---|
| Rate Limit | 2,000 req / 5分 / IP | DoS攻撃の緩和 |
| Body Size | < 64KB | 異常な巨大リクエストのブロック |
| AWS Managed Rules | Common Rule Set | 一般的な攻撃パターンのブロック |
学び WAF高い。(関係ない)
設計思想⑤ CloudWatch による監視
本番環境では障害を早期発見するために、CloudWatch Alarms を設定しました。
// Lambda エラーアラーム
new Alarm(this, 'LambdaErrorAlarm', {
metric: handler.metricErrors({
period: Duration.minutes(5),
statistic: 'Sum',
}),
threshold: 5,
evaluationPeriods: 1,
comparisonOperator: ComparisonOperator.GREATER_THAN_THRESHOLD,
});
Amazon Q Developer in chat applications (旧称: AWS Chatbot) と連携して、Slackに通知が飛ぶようにしました。
コスト試算
100ユーザー規模での月額費用をGeminiに試算してもらいました。
| サービス | 月額費用 (USD) |
|---|---|
| API Gateway | $0.42 |
| Lambda | $0.02 |
| DynamoDB | $0.38 |
| WAF | $5.07 |
| CloudWatch | $0.05 |
| 合計 | 約 $6 / 月 |
WAFがほとんどを占めている。。他のサービスに関しては、ほんとか?ってぐらい安い。Geminiが嘘をついていたら私は誰も信用できなくなる。
苦労したこと
1. AppSync から REST API への方針転換
最初は「チャット出しリアルタイムがいいよな」と思って AppSync + Subscription で設計を進めていましたが、要件を整理し直したときに「別にリアルタイムじゃなくていいな」となりました。
結果的にはシンプルで導入しやすい構成になりました。
2. Lambda のコールドスタート
Lambda2台体制(認証、API)なので、初回リクエストの動作がどうしても遅い。。
対策
- メモリを1024MBに設定(メモリ↑ = CPU↑)
メモリを増やすと単価は上がって高くなるのでは?と最初は疑問に思いましたが、処理時間が短縮されるため、結果的に総コストが安くなるケースもあるらしい(AWS Lambda Power Tuningの考え方)。今回は処理速度を優先して上げました。
まとめ
設計で意識したこと
- オーバースペックを避ける - 要件に対して適切な技術選定
- 関心の分離 - Lambda Authorizer で認証とAPIロジックを分離
- シンプルなデータ設計 - Single Table Design でアクセスパターンを明確に
- 環境分離 - dev/prod を最初から設計に組み込む
- 監視体制 - WAF/CloudWatch で本番運用を保護
振り返り
最初に検討した AppSync 構成も悪くはなかったですが、「本当にリアルタイム通信が必要か?」を改めて考えたことで、よりシンプルな構成にたどり着けました。
「技術的に高性能でイケてる」より「要件に対して適切か」を意識することの大切さを学びました。

