はじめに
前回の記事では、AWS CDKを使ったインフラ構成の実装について解説しました。今回は最終回として、実際のデプロイ手順と動作確認、そして全体の振り返りを行います。
デプロイ手順
前提条件
事前に必要な設定やインストールを行います。AWS CLIの設定と、CDKおよびPython依存関係のインストールを済ませておいてください。
# AWS CLIの設定 aws configure # CDKのインストール npm install -g aws-cdk # Python依存関係のインストール pip install -r requirements.txt
Lambda Layerの準備
Lambda関数で使用する外部ライブラリをLayerとしてパッケージングします。PyPDF2とrequestsの2つのLayerを作成します。
# {プロジェクトTOP}/lambda に移動
cd lambda
# PyPDF2レイヤー
mkdir -p python
pip install PyPDF2 -t python/
zip -r pypdf2-layer.zip python/
rm -rf python/
# requestsレイヤー
mkdir -p python
pip install requests requests-aws4auth -t python/
zip -r requests-layer.zip python/
rm -rf python/
# プロジェクトルートに戻る
cd ..CDKデプロイ
準備が整ったら、CDKを使ってAWSリソースをデプロイします。初回実行時はブートストラップが必要です。
# CDKブートストラップ(初回のみ) cdk bootstrap # スタックのデプロイ cdk deploy
デプロイには15〜30分程度かかります(特にOpenSearchドメインの作成に時間がかかります)。
デプロイが完了すると、以下のような出力が表示されます。
✅ RagStack ✨ Deployment time: xxxx.xxs Outputs: RagStack.RagApiEndpoint12345678 = https://xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/prod/ Stack ARN: arn:aws:cloudformation:ap-northeast-1:123456789012:stack/RagStack/...
DynamoDBにサンプルデータ投入
AWSコンソールからDynamoDBにユーザー情報を登録します。
- AWSコンソールでDynamoDBを開く
- 「テーブル」から「rag-context-table」を選択
- 「項目を探索」タブを選択
- 「項目を作成」ボタンをクリック
- 以下のJSON形式でデータを入力
{ "user_id": "user_001", "age": 35, "family": "妻・子供2人", "occupation": "会社員", "income": "600万円", "current_insurance": "なし", "concerns": "家族の将来が心配" } - 「項目を作成」ボタンをクリックして保存
同様の手順で、必要に応じて他のユーザーデータ(user_002、user_003など)も登録できます。
S3にPDFファイルをアップロード
S3バケットにPDFファイルをアップロードします。ここでは、生命保険プランなどのサンプルPDFを用意する必要があります。
PDFファイルの準備
今回のRAGシステムでは、以下のような内容のPDFファイルを任意で作成してください。
- 生命保険のプラン情報(プラン名、保障内容、月額保険料など)
- 医療保険のプラン情報
- その他、検索対象としたいドキュメント
例: plan_001.pdfの内容イメージ
【プランA】 死亡保障: 3000万円 月額保険料: 8000円 対象: 家族向け 特徴: お子様の教育費と生活費をカバーできる標準的なプラン
WordやGoogleドキュメントなどで上記のような内容を記載し、PDF形式でエクスポートしてください。複数のプラン情報を含むPDFを3〜5件程度用意することをお勧めします。
AWSコンソールからアップロード
- AWSコンソールでS3を開く
- 「rag-datalake-bucket」バケットを選択
- 「アップロード」ボタンをクリック
- 「ファイルを追加」ボタンをクリック
- 作成したPDFファイルを選択(複数選択可)
- 「アップロード」ボタンをクリック
- アップロード完了を確認
インデックス処理の実行
AWSコンソールから、indexing-handler Lambda関数をテスト実行します。
Lambda関数のページで「テスト」タブを選択し、任意のテストイベント(空のJSONでも可)で実行します。
ログで確認
Indexed plan_001.pdf successfully Indexed plan_002.pdf successfully Indexed plan_003.pdf successfully Indexed plan_004.pdf successfully Indexed plan_005.pdf successfully
動作確認
RAG処理の実行
AWSコンソールから、rag-handler Lambda関数をテスト実行します。
テストイベント(sample.json)
{
"user_id": "user_001",
"query": "私に最適な保険のプランはどれですか?"
}期待される出力
{
"statusCode": 200,
"body": "{\"prompt\": \"...\", \"answer\": \"35歳で会社員、妻とお子様2人をお持ちの方には...\"}"
}API Gateway経由でのテスト
curl -X POST https://[API_ID].execute-api.ap-northeast-1.amazonaws.com/prod/ \
-H "Content-Type: application/json" \
-d '{
"user_id": "user_001",
"query": "私に最適な保険のプランはどれですか?"
}'API GatewayのエンドポイントURLは、CDKデプロイ時の出力に表示されます。
コスト試算
今回の構成での月額コスト概算(東京リージョン、軽い負荷を想定)
| サービス | 構成 | 月額コスト |
|---|---|---|
| OpenSearch | t3.small.search × 1(検証環境向け) | 約 $30 |
| Lambda | RAG処理 1000回/月 | 約 $1 |
| Bedrock | Titan + Claude 1000回/月 | 約 $10 |
| DynamoDB | オンデマンド、低負荷 | 約 $1 |
| S3 | 数GB | 約 $1 |
| API Gateway | 1000リクエスト/月 | 約 $1 |
| 合計 | 約 $44/月 |
本番環境へのスケールアップ
今回は検証環境向けの低コスト構成(t3.small.search × 1ノード)を採用していますが、本番環境で高可用性が必要な場合は、以下のような構成に変更できます。
本番環境構成例
- データノード: r5.large.search × 3(マルチAZ配置)
- 専用マスターノード: m5.large.search × 3
- 月額コスト: 約$600(OpenSearchのみ)
- 合計コスト: 約$614/月
構成変更のポイント
第五弾の記事で解説したCDKコードのcluster_config部分を変更することで、簡単に本番環境向けの高可用性構成に切り替えられます。
- 3ノード構成で高可用性を確保
- マルチAZ(3つのアベイラビリティゾーン)に分散配置
- 専用マスターノードでクラスター管理の安定性向上
- メモリ最適化インスタンスでベクトル検索のパフォーマンス向上
コスト最適化のポイント
- 検証・開発環境では今回の構成(約$44/月)で十分
- トラフィックが少ない本番環境では、2ノード構成も検討可能
- Lambda予約同時実行数を制限してコストを抑制
- DynamoDBのアクセスパターンが予測可能な場合、プロビジョニング済みキャパシティモードに変更
ハマったポイントと対処法
OpenSearchへのインデックス作成が失敗する
問題: indexing-handler実行時に403 Forbiddenエラー
原因: Lambda関数のIAMロールにOpenSearchへのアクセス権限が不足
対処: 第五弾で設定したIAM権限が正しく適用されているか確認。特にaccess_policyとlambda_role.add_to_policyの設定を見直す
DynamoDBからデータが取得できない
問題: rag-handler実行時にユーザー情報が取得できない
原因: DynamoDBにデータが投入されていない、またはuser_idが一致していない
対処: DynamoDBコンソールでテーブルの内容を確認。リクエストのuser_idとテーブルのuser_idが完全一致しているか確認
ベクトル検索結果が0件
問題: OpenSearch検索は成功するが、結果が0件
原因: インデックス処理が正常に完了していない、またはインデックス名が一致していない
対処:
- indexing-handlerのCloudWatch Logsを確認
- 「Indexed xxx.pdf successfully」のログが出ているか確認
- 環境変数OPENSEARCH_INDEXの値が両Lambda関数で一致しているか確認
全体の振り返り
実装した機能
全6回の連載を通して、以下の機能を実装しました。
- AWS Summit Japan 2025での学び
- 生成AIとRAGの重要性
- プロジェクトの狙いと目標
- RAGシステムのアーキテクチャ設計
- 各AWSサービスの選定理由
- データフローの全体像
- OpenSearchインデックスの初期化(create_index)
- S3からPDFファイルの取得
- PyPDF2によるテキスト抽出
- テキストのチャンク分割(オーバーラップ付き)
- Bedrockを使った埋め込みベクトル生成
- OpenSearchへのドキュメント登録
- Lambda Layerの準備と設定
- ライブラリのインポートとクライアント初期化
- ユーザー情報の取得(DynamoDB)
- 質問のベクトル化(Titan Embeddings)
- 類似ドキュメントの検索(OpenSearch kNN)
- プロンプトの構築(RAGの肝)
- LLMでの回答生成(Claude 3.5 Sonnet v2)
- IAMロール・ポリシーの設定
- S3、DynamoDB、OpenSearchの作成
- Lambda関数とLayerのデプロイ
- API Gatewayの作成
- Custom Resourceによる自動初期化
記事⑥ デプロイと動作確認と振り返り(今回)
- 実際のデプロイ手順
- サンプルデータの投入
- 動作確認とテスト
- コスト試算
- トラブルシューティング
学びと気づき
RAGの効果
- ハルシネーションの大幅な削減 – 事実に基づいた回答のみを生成
- 情報の鮮度を保てる – LLM再学習不要で最新情報を反映
- パーソナライズされた回答 – ユーザー情報に基づいた提案
AWSサービスの組み合わせ
- サーバーレスで運用負荷を削減
- Bedrockで最新のLLMを簡単に利用
- OpenSearchのベクトル検索が強力
- マネージドサービスによる高い可用性
CDKの利便性
- インフラをコードで管理
- 再現可能な環境構築
- Custom Resourceで柔軟な初期化
- TypeScriptやPythonで記述可能
今後の改善案
精度向上
- Hybrid Search(ベクトル検索 + キーワード検索)の導入
- リランキングモデルの追加
- チャンクサイズの最適化
パフォーマンス向上
- Lambda ProvisionedConcurrencyの活用(コールドスタート削減)
- OpenSearchのインデックス最適化
- ベクトルのキャッシング
- 非同期処理の導入
ユーザー体験
- ストリーミング応答(Claude Streamingを使用)
- 会話履歴の管理(複数ターンの対話)
- フィードバック機能(回答の品質向上)
- フロントエンドの実装(WebアプリやSlack連携)
セキュリティ対策
- API GatewayにCognito認証を追加
- VPC内にOpenSearchを配置
- KMSでの暗号化強化
- IAMポリシーのさらなる最小化
スケーラビリティ
- S3イベントトリガーによるインデックス自動更新
- 複数の言語モデルへの対応
- 他のファイル形式のサポート(Word、Excel、テキストなど)
- マルチテナント対応
まとめ
全6回にわたって、AWSでRAGチャットボットを構築してきました。
- 記事① – RAGとは?AWS Summit参加の学びと狙い
- 記事② – 構成設計と各AWSサービスの役割
- 記事③ – インデックス作成とPDF埋め込み処理の実装
- 記事④ – RAG検索処理とLLM応答生成の実装
- 記事⑤ – CDKによるインフラ定義
- 記事⑥ – デプロイと動作確認と振り返り(今回)
RAGは、LLMの限界を補完する非常に強力な手法です。特に、企業の社内文書検索や専門知識が必要な業務において、大きな価値を発揮します。
今回の実装では、生命保険プランの提案というユースケースを通して、RAGシステムの実践的な構築方法を学びました。この仕組みは、他の多くの用途にも応用可能です。
- カスタマーサポートのFAQシステム
- 社内ナレッジベースの検索
- 法律・医療などの専門分野の情報提供
- 製品マニュアルの問い合わせ対応
今回の実装を参考に、ぜひ皆さんの業務でもRAGシステムを活用してみてください!
最後まで読んでいただき、ありがとうございました!