最終更新:2025年6月 / 対象:AWS Lambda / Python 3.12 / OpenAI API or Amazon Bedrock
1. 結論:最適構成はこれだ
LLMチャットボットをAWSで低コスト・低運用負荷で動かすなら、API Gateway + AWS Lambda + DynamoDB(会話履歴)+ Bedrock or OpenAI APIの構成が現時点での最適解です。
- サーバー管理不要(EC2・ECSのインスタンス管理ゼロ)
- リクエストベース課金でアイドル時コストゼロ
- IAMロールでAPIキーをコードに書かずセキュアに管理
- DynamoDBにセッションIDで会話履歴を保持し、文脈付き応答を実現
「とりあえずEC2で動かした」→月数千円の無駄コスト発生。Lambdaなら月100万リクエストまで無料枠。まず Lambda から始めるのが鉄則。
2. そもそも何か?用語と全体像(初心者向け)
2-1. LLMチャットボットとは
LLM(Large Language Model)とはGPT-4やClaude、Amazon Titanなどの大規模言語モデルです。これをバックエンドに持つチャットボットは、ルールベースのbotと違い、自然な日本語で質疑応答・文章生成・要約が可能です。
2-2. AWS Lambdaとは
コードをアップロードするだけで動くAWSのサーバーレス実行環境です。リクエストが来た時だけ起動し、終わったら停止します。常時稼働のサーバー(EC2)と違い、使った分だけ課金される点が最大の特徴です。
2-3. 今回の構成で登場するAWSサービス一覧
| サービス | 役割 | 無料枠 |
|---|---|---|
| API Gateway | HTTPリクエストの受け口(エンドポイント) | 100万回/月 |
| AWS Lambda | チャットボットのロジック実行 | 100万回/月 |
| DynamoDB | 会話履歴の保存(セッション管理) | 25GB/月 |
| Amazon Bedrock | LLMのAPI呼び出し(Claude / Titan) | なし(従量課金) |
| IAM | Lambda→Bedrock/DynamoDBへの権限管理 | 無料 |
| CloudWatch Logs | Lambdaのログ収集・監視 | 5GB/月 |
| Secrets Manager | OpenAI APIキーの安全な保管 | 30日間無料 |
3. アーキテクチャ解説と構成図
3-1. リクエストの流れ
- ユーザーがフロントエンド(React等)からPOSTリクエスト送信
- API GatewayがリクエストをLambdaへプロキシ転送
- LambdaがDynamoDBから過去の会話履歴を取得
- 履歴+今回のメッセージをBedrockまたはOpenAI APIへ送信
- LLMの応答をDynamoDBに保存し、ユーザーへ返却
3-2. システム構成図(PlantUML)
3-3. DynamoDBのデータ設計
| 属性名 | 型 | 役割 |
|---|---|---|
| session_id(PK) | String | 会話セッションの識別子 |
| timestamp(SK) | Number | メッセージの時刻(エポック秒) |
| role | String | “user” または “assistant” |
| content | String | メッセージ本文 |
| ttl | Number | 自動削除タイムスタンプ(24時間後) |
4. 実装手順(ステップ形式)
4-1. ディレクトリ構成
llm-chatbot-lambda/
├── lambda/
│ ├── handler.py # Lambdaエントリーポイント
│ ├── llm_client.py # Bedrock/OpenAI呼び出しロジック
│ ├── dynamodb_client.py # 会話履歴CRUD
│ └── requirements.txt # 依存ライブラリ
├── terraform/ # (オプション)IaC
│ └── main.tf
└── README.md
4-2. Step 1:DynamoDBテーブルを作成する
AWSコンソール → DynamoDB → テーブルの作成
- テーブル名:
chat_history - パーティションキー:
session_id(文字列) - ソートキー:
timestamp(数値) - TTL属性を有効化:
ttl - キャパシティモード:オンデマンド(本番低負荷ならコスト最適)
4-3. Step 2:IAMロールを作成する
Lambda実行ロールに以下のポリシーをアタッチします。
// ファイル名:lambda_policy.json
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"dynamodb:GetItem",
"dynamodb:PutItem",
"dynamodb:Query"
],
"Resource": "arn:aws:dynamodb:ap-northeast-1:YOUR_ACCOUNT_ID:table/chat_history"
},
{
"Effect": "Allow",
"Action": [
"bedrock:InvokeModel"
],
"Resource": "arn:aws:bedrock:us-east-1::foundation-model/anthropic.claude-3-sonnet-20240229-v1:0"
},
{
"Effect": "Allow",
"Action": [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents"
],
"Resource": "arn:aws:logs:*:*:*"
}
]
}
⚠️ 現場注意:
"Resource": "*"は絶対に使わないこと。最小権限の原則(Principle of Least Privilege)を徹底し、テーブルARNとモデルARNを明示します。
4-4. Step 3:Lambdaコードを実装する
ファイル:lambda/dynamodb_client.py
import boto3
import time
from boto3.dynamodb.conditions import Key
dynamodb = boto3.resource("dynamodb", region_name="ap-northeast-1")
table = dynamodb.Table("chat_history")
def get_history(session_id: str, limit: int = 10) -> list[dict]:
"""直近limit件の会話履歴を取得する"""
response = table.query(
KeyConditionExpression=Key("session_id").eq(session_id),
ScanIndexForward=False, # 降順取得
Limit=limit
)
items = sorted(response["Items"], key=lambda x: x["timestamp"])
return [{"role": item["role"], "content": item["content"]} for item in items]
def save_message(session_id: str, role: str, content: str) -> None:
"""メッセージをDynamoDBに保存する(TTL:24時間)"""
table.put_item(Item={
"session_id": session_id,
"timestamp": int(time.time() * 1000), # ミリ秒エポック
"role": role,
"content": content,
"ttl": int(time.time()) + 86400 # 24時間後に自動削除
})
ファイル:lambda/llm_client.py
import boto3
import json
import os
bedrock = boto3.client("bedrock-runtime", region_name="us-east-1")
SYSTEM_PROMPT = """あなたは親切なAIアシスタントです。
日本語で丁寧に回答してください。"""
def call_bedrock(messages: list[dict]) -> str:
"""Amazon Bedrock (Claude 3 Sonnet) を呼び出す"""
body = {
"anthropic_version": "bedrock-2023-05-31",
"max_tokens": 1024,
"system": SYSTEM_PROMPT,
"messages": messages
}
response = bedrock.invoke_model(
modelId="anthropic.claude-3-sonnet-20240229-v1:0",
body=json.dumps(body),
contentType="application/json",
accept="application/json"
)
result = json.loads(response["body"].read())
return result["content"][0]["text"]
ファイル:lambda/handler.py
import json
import uuid
from dynamodb_client import get_history, save_message
from llm_client import call_bedrock
def lambda_handler(event, context):
"""API Gatewayからのリクエストを処理するエントリーポイント"""
try:
body = json.loads(event.get("body", "{}"))
user_message = body.get("message", "").strip()
session_id = body.get("session_id") or str(uuid.uuid4())
if not user_message:
return _response(400, {"error": "message is required"})
# 1. 会話履歴の取得
history = get_history(session_id)
# 2. 今回のメッセージを追加
history.append({"role": "user", "content": user_message})
# 3. LLM呼び出し
assistant_message = call_bedrock(history)
# 4. 履歴保存
save_message(session_id, "user", user_message)
save_message(session_id, "assistant", assistant_message)
return _response(200, {
"session_id": session_id,
"assistant_message": assistant_message
})
except Exception as e:
print(f"ERROR: {str(e)}") # CloudWatch Logsへ出力
return _response(500, {"error": "Internal server error"})
def _response(status_code: int, body: dict) -> dict:
return {
"statusCode": status_code,
"headers": {
"Content-Type": "application/json",
"Access-Control-Allow-Origin": "*" # 本番はドメイン指定に変更
},
"body": json.dumps(body, ensure_ascii=False)
}
ファイル:lambda/requirements.txt
boto3>=1.34.0
※ boto3はLambdaランタイムに同梱されていますが、バージョン固定のためrequirements.txtに明記することを推奨します。
4-5. Step 4:Lambdaにデプロイする
# 依存ライブラリをパッケージに同梱
cd lambda/
pip install -r requirements.txt -t ./package/
cp *.py ./package/
# ZIPファイルを作成
cd package/
zip -r ../function.zip .
cd ..
# Lambdaへデプロイ(CLIで実行)
aws lambda create-function \
--function-name llm-chatbot \
--runtime python3.12 \
--handler handler.lambda_handler \
--role arn:aws:iam::YOUR_ACCOUNT_ID:role/llm-chatbot-role \
--zip-file fileb://function.zip \
--timeout 30 \
--memory-size 256 \
--region ap-northeast-1
⚠️ タイムアウト設定:LLMの応答に最大10〜15秒かかるため、デフォルト3秒では必ず失敗します。最低30秒を設定してください。
4-6. Step 5:API Gatewayを設定する
- AWSコンソール → API Gateway → REST APIを作成
- リソース
/chatを作成 → メソッドPOSTを追加 - 統合タイプ:Lambda関数プロキシ統合を選択
- Lambda関数:
llm-chatbotを指定 - ステージ名
prodでデプロイ
エンドポイントURL例:https://xxxxxxx.execute-api.ap-northeast-1.amazonaws.com/prod/chat
4-7. Step 6:動作確認
curl -X POST https://xxxxxxx.execute-api.ap-northeast-1.amazonaws.com/prod/chat \
-H "Content-Type: application/json" \
-d '{"message": "AWSのLambdaとは何ですか?", "session_id": "test-session-001"}'
# 期待するレスポンス
{
"session_id": "test-session-001",
"assistant_message": "AWS Lambdaは、サーバーを管理せずにコードを実行できるサーバーレスコンピューティングサービスです。..."
}
5. 実務ユースケース
5-1. 社内FAQボット(最頻出ユースケース)
Confluenceや社内ドキュメントのテキストをS3に保存し、Lambda内でRAG(Retrieval-Augmented Generation)を組み合わせることで、「会社固有の知識に答えるボット」を実現できます。
- 追加コンポーネント:S3(ドキュメント格納)+ Amazon OpenSearch Serverless(ベクター検索)
- Lambda関数をRAG処理用とチャット用に分割し、Step Functionsでオーケストレーション
5-2. カスタマーサポートボット(SLA対応)
API Gatewayの使用量プラン・APIキー認証を設定し、顧客ごとのリクエスト制限を実装します。LLM応答に3秒以上かかる場合はSQSキュー経由の非同期処理に切り替え、WebSocket API GatewayでプッシュするとUX向上に繋がります。
5-3. Kubernetes(EKS)との併用パターン
既存のEKSクラスター上のマイクロサービスからLambdaのチャットボットAPIを呼び出す構成も現場でよく使われます。EKSのPodにIRSA(IAM Roles for Service Accounts)を設定し、API GatewayのエンドポイントをKubernetes ExternalNameサービス経由で名前解決する方法が定番です。
6. メリット・デメリット比較(技術選定表)
| 観点 | Lambda構成 | EC2 + FastAPI | ECS Fargate |
|---|---|---|---|
| 初期構築コスト | ◎ 低い(1〜2時間) | △ 中程度(半日) | ○ 中程度(3〜4時間) |
| 月額コスト(低負荷) | ◎ 無料〜数十円 | ✕ 最低3,000円〜(t3.small常時起動) | ○ 数百円〜(タスク数による) |
| スケーラビリティ | ◎ 自動(最大1,000同時実行) | △ 手動スケール要 | ◎ Auto Scaling対応 |
| コールドスタート | ✕ 初回300ms〜1秒の遅延 | ◎ なし | ○ 軽微(タスク起動に数十秒) |
| 実行時間上限 | △ 最大15分(ストリーミング制約あり) | ◎ 制限なし | ◎ 制限なし |
| サーバー管理 | ◎ 不要 | ✕ OS・パッチ管理必要 | ◎ 不要 |
| デバッグのしやすさ | △ CloudWatch必須 | ◎ SSHでリアルタイム確認可 | ○ CloudWatch + ECS Exec |
| 本番向け推奨度 | ◎ 低〜中負荷 | ○ 高負荷・常時接続 | ◎ 中〜高負荷 |
選定指針:月間リクエスト数が10万件未満ならLambda一択。10万〜100万件はFargate、100万件超かつストリーミング必須ならECS on EC2を検討してください。
7. よくあるエラーと対策
エラー①:Task timed out after 3.00 seconds
事象:
[ERROR] Task timed out after 3.00 seconds
原因:LambdaのデフォルトタイムアウトはHTTPリクエストの完了まで3秒であるが、BedrockやOpenAI APIの応答に平均5〜15秒かかることによって、レスポンスが返る前にLambdaが強制終了されています。
対策手順:
- AWSコンソール → Lambda → 該当関数 → 「設定」タブ → 「一般設定」を開く
- 「タイムアウト」を
0分30秒以上(推奨:1分)に変更して保存する - API GatewayもデフォルトタイムアウトがHTTP統合で29秒のため、Lambda側を29秒以内に設定するか、WebSocket APIへ移行する
エラー②:AccessDeniedException on Bedrock
事象:
botocore.exceptions.ClientError: An error occurred (AccessDeniedException)
when calling the InvokeModel operation:
User: arn:aws:sts::123456789:assumed-role/llm-chatbot-role/llm-chatbot
is not authorized to perform: bedrock:InvokeModel
on resource: arn:aws:bedrock:us-east-1::foundation-model/anthropic.claude-3-sonnet-20240229-v1:0
原因:IAMポリシーにBedrockのInvokeModel権限が付与されていないか、Bedrockのモデルアクセスがアカウントで有効化されていないことによって、API呼び出しが拒否されています。
対策手順:
- AWSコンソール → Amazon Bedrock → 「モデルアクセス」を開く
- 使用するモデル(例:Claude 3 Sonnet)の「アクセスをリクエスト」をクリックして有効化する(反映まで最大1分)
- IAM → ロール →
llm-chatbot-role→ アタッチ済みポリシーにbedrock:InvokeModelが含まれているか確認する - リソースARNのリージョンが
us-east-1(Bedrockが利用可能なリージョン)になっているか確認する
エラー③:DynamoDB ValidationException(型不一致)
事象:
botocore.exceptions.ClientError: An error occurred (ValidationException)
when calling the Query operation:
Value provided in ExpressionAttributeValues is incompatible with ConditionExpression operator
原因:DynamoDBテーブル作成時にソートキーtimestampを文字列(String)で定義したが、コードでは数値(Number)型を渡していることによって、型の不一致が発生しています。
対策手順:
- DynamoDBコンソール →
chat_historyテーブル → 「概要」タブでソートキーの型を確認する - 型が「String」になっている場合、テーブルを削除して再作成する(キーの型は変更不可)
- 再作成時にソートキー
timestampの型を必ず 数値(Number) に設定する - コード側でも
int(time.time() * 1000)のように必ず整数型で渡していることを確認する
エラー④:コールドスタートによるレスポンス遅延
事象:初回リクエストが5〜10秒かかりユーザーからタイムアウトと誤認される
原因:Lambdaはアイドル状態から起動する際にコンテナ初期化(コールドスタート)が発生し、Python実行環境のロードとboto3の初期化で最大2〜3秒かかることによって、LLMの応答時間と合算され全体遅延が目立っています。
対策手順:
- Lambda → 「設定」→「プロビジョニングされた同時実行数」を開く
- 同時実行数を
1以上に設定する(コスト:月約15〜30ドル/1インスタンス) - コストを抑えたい場合は、CloudWatch EventsでLambdaを10分ごとにウォームアップするPing関数を設定する
# lambda/warmer.py: ウォームアップ用ダミーハンドラー
def lambda_handler(event, context):
if event.get("source") == "aws.events":
print("Warm-up ping received")
return {"statusCode": 200}
8. まとめ:実務でどう使うか
本記事で構築したLLMチャットボットのポイントを整理します。
| フェーズ | やること | 注意点 |
|---|---|---|
| 開発初期 | Lambda + DynamoDB + Bedrock で最小構成を作る | タイムアウト30秒・IAM最小権限を最初に設定する |
| 本番移行前 | API Gatewayに使用量プラン・APIキー認証を追加する | CORSは * から本番ドメインに変更する |
| スケールアップ時 | Lambda Powertuningでメモリ最適化する | 負荷が月10万超えならFargateへの移行を検討する |
| コスト管理 | CloudWatch Budgetsで月額アラートを設定する | DynamoDBのTTLで古い履歴を自動削除しストレージ費用を抑える |
| 運用監視 | CloudWatch Dashboardでエラー率・レイテンシを可視化する | LLMコストはBedrockのCost Explorerで週次確認する |
LLMチャットボットをAWS Lambdaで構築する最大のメリットは、インフラ管理ゼロ・従量課金・IAMによるセキュアなAPI呼び出しの3点です。まず本記事の最小構成で動かし、ユーザー数の増加に応じてFargate→ECSへ段階的に移行するのが現場では現実的な進め方です。


コメント