はじめに
「チャットボットのログ収集基盤をコードで管理して、同じ環境を何度でも再現したい」という要件に、AWS SAM(Serverless Application Model) は最適な選択肢のひとつです。
この記事では、AWS SAM を使って、API Gateway + Lambda + CloudWatch Logs によるチャットボットログAPIをゼロから構築するハンズオンを紹介します。
クライアント
↓ POST /logs(会話ログを送信)
API Gateway(REST API / Prod ステージ)
↓ Lambda プロキシ統合
Lambda(ChatLogFunction / Python 3.12)
↓ boto3 logs.put_log_events() boto3 cloudwatch.put_metric_data()
CloudWatch Log Group CloudWatch カスタムメトリクス
(/chatbot/STACK_NAME) (ChatBot/STACK_NAME)
↓ Metric Filter($.level = "error") ↑ 自動変換
CloudWatch ChatErrorCount メトリクス ─────────┘このハンズオンで体験できること:
AWS::Logs::LogGroup/AWS::Logs::MetricFilterの SAM 定義- SAM 組み込みポリシーがない場合の
Statement直接記述による権限付与 !Sub/!GetAtt/!Refを使ったリソース間参照sam build+sam deployの 2コマンドで全リソースをデプロイ
このハンズオンの特徴:
- API Gateway + Lambda × 1 + CloudWatch Log Group + Metric Filter + IAM ロールを
template.yaml1ファイルで管理 sam deleteで全リソースを一括削除(コンソール版では API Gateway / Lambda / Log Group / IAM を個別に削除)
SAM vs コンソール:どれだけ違うか
| 比較項目 | SAM(コード) | コンソール(手動) |
|---|---|---|
| Log Group の保持期間 | RetentionInDays: 7 で定義 | 作成時に手動設定 |
| Metric Filter | AWS::Logs::MetricFilter で定義 | ロググループのタブから手動作成 |
| IAM 権限 | Statement で直接記述(ARN は !GetAtt で自動解決) | インラインポリシーに ARN を手動入力 |
| API Gateway | Events: Type: Api で POST / GET を1行ずつ定義 | リソース・メソッド・デプロイを個別に操作 |
| 削除 | sam delete 1コマンド | API Gateway / Lambda / Log Group / IAM を個別に削除 |
| 再現性 | チームで同じ環境を即座に再現できる | 手順書が必要 |
キーワード解説
| 用語 | 意味 |
|---|---|
| CloudWatch Log Group | ログの論理的なまとまり。SAM では RetentionInDays を設定できる |
| Log Stream | Log Group の中の個別のログストリーム。今回はセッションIDごとに作成する |
| Metric Filter | ログの JSON フィールドをマッチさせ、カスタムメトリクスを自動生成する仕組み |
| Lambda プロキシ統合 | API Gateway が HTTP リクエスト全体を Lambda に渡す最もシンプルな統合方式 |
!GetAtt | CloudFormation 組み込み関数。リソースの属性値(ARN など)を参照する |
!Sub | CloudFormation 組み込み関数。文字列の中に変数を埋め込む |
前提条件
| ツール | 確認コマンド | 最低バージョン目安 |
|---|---|---|
| AWS CLI v2 | aws --version | 2.x |
| AWS SAM CLI | sam --version | 1.x |
| Python 3.12 | python --version | 3.12 |
AWS認証確認:
aws sts get-caller-identityアカウントIDが表示されれば認証設定済みです。
使用するAWSサービス
| サービス | 役割 | 料金 |
|---|---|---|
| API Gateway | REST APIのエンドポイント(POST /logs, GET /logs) | 月100万リクエストまで無料 |
| Lambda | ログの書き込み・取得処理(Python 3.12) | 月100万リクエスト・400,000 GB-秒まで無料 |
| CloudWatch Logs | チャットログの保存・クエリ | 月5GBまで無料 |
| CloudWatch メトリクス | エラーログのカスタムメトリクス化 | 月10件まで無料 |
| IAM | Lambda の実行権限管理 | 無料 |
| S3 | SAM デプロイパッケージ置き場 | 少量のため実質無料 |
Step 1: プロジェクトフォルダを開く
cd C:\my-aws\aws-learning-projects\chatbot-log-apiフォルダ構造
chatbot-log-api/
├── template.yaml # SAMテンプレート(API Gateway + Lambda + CloudWatch)
├── samconfig.toml # デプロイ設定(gitignore 対象・毎回手動作成が必要)
├── docs/
│ ├── 1_console.md # AWSコンソール版手順
│ └── 2_sam.md # SAM版手順
└── src/
└── app.py # Lambda 関数(POST /logs, GET /logs)Step 2: SAMテンプレートの確認(template.yaml)
template.yaml は全 AWSリソースの設計図です。コンソール版との違いに注目しながらポイントを確認します。
Log Group と Metric Filter の定義
Resources:
ChatLogGroup:
Type: AWS::Logs::LogGroup
Properties:
LogGroupName: !Sub "/chatbot/${AWS::StackName}"
RetentionInDays: 7
# Metric Filter: error ログを ChatErrorCount メトリクスに自動変換
ErrorMetricFilter:
Type: AWS::Logs::MetricFilter
Properties:
LogGroupName: !Ref ChatLogGroup
FilterPattern: '{ $.level = "error" }'
MetricTransformations:
- MetricName: ChatErrorCount
MetricNamespace: !Sub "ChatBot/${AWS::StackName}"
MetricValue: "1"
DefaultValue: 0
AWS::Logs::LogGroupを明示定義する理由:
未定義のまま Lambda を実行すると、Lambda が自動でロググループを作成しますが保持期間が無期限になります。
SAM で明示的に定義することでRetentionInDays: 7が確実に適用されます。
Lambda 関数の定義(IAMポリシーとAPIトリガー)
ChatLogFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: src/
Handler: app.lambda_handler
Runtime: python3.12
Timeout: 10
Environment:
Variables:
LOG_GROUP_NAME: !Ref ChatLogGroup # "/chatbot/スタック名"
Policies:
- Statement:
# カスタムロググループへの書き込み・読み取り(スコープを特定グループに限定)
- Action: [logs:CreateLogStream, logs:PutLogEvents, logs:GetLogEvents, logs:DescribeLogStreams]
Effect: Allow
Resource: [!GetAtt ChatLogGroup.Arn, !Sub "${ChatLogGroup.Arn}:*"]
# カスタムメトリクスの書き込み
- Action: cloudwatch:PutMetricData
Effect: Allow
Resource: "*"
Events:
PostLog:
Type: Api
Properties:
Path: /logs
Method: post
GetLogs:
Type: Api
Properties:
Path: /logs
Method: gettemplate.yaml のポイント:
| ポイント | 説明 |
|---|---|
AWS::Logs::LogGroup | SAMでロググループを明示定義することで RetentionInDays を設定できる。未定義だと Lambda が自動作成するが保持期間は無期限 |
AWS::Logs::MetricFilter | JSON フィルターパターンで特定フィールドのログを検出してメトリクスに変換する |
FilterPattern: '{ $.level = "error" }' | CloudWatch Logs の JSON フィルター構文。$ で JSON ルートを表す |
Statement: logs:PutLogEvents | SAM組み込みポリシーにCloudWatch Logs書き込みポリシーはないため、IAMポリシー文を直接記述する |
!Sub "${ChatLogGroup.Arn}:*" | arn:...:log-group:/chatbot/STACK:* の形式でログストリームにアクセスする権限を付与 |
Step 3: Lambda コードの確認(src/app.py)
コードはすでに作成済みです。各関数の役割とポイントを確認しておきます。
LOG_GROUP_NAME = os.environ["LOG_GROUP_NAME"] # "/chatbot/スタック名"
METRIC_NAMESPACE = "ChatBot/" + LOG_GROUP_NAME.split("/")[-1] # "ChatBot/スタック名"
def post_log(event):
# リクエストボディを解析
body = json.loads(event.get("body") or "{}")
session_id = body.get("session_id") or str(uuid.uuid4())
# セッション単位のログストリームに書き込む
log_stream_name = f"session/{session_id}"
_ensure_log_stream(log_stream_name) # ストリームを作成(既存なら無視)
logs_client.put_log_events(...) # ログを書き込む
# カスタムメトリクスを記録
cloudwatch.put_metric_data(
Namespace=METRIC_NAMESPACE,
MetricData=[{"MetricName": "MessageCount", "Dimensions": [...], "Value": 1}],
)
def get_logs(event):
# session_id でログストリームを指定して取得
logs_client.get_log_events(logGroupName=..., logStreamName=f"session/{session_id}")app.py のポイント:
_ensure_log_stream():create_log_stream()は既存ストリームに対してResourceAlreadyExistsExceptionを返します。botocore.exceptions.ClientErrorでキャッチして無視することで冪等性を確保しますint(time.time() * 1000): CloudWatch Logs のtimestampはミリ秒単位の Unix 時間(整数)ですstartFromHead=False: 最新のログから取得します(デフォルトは古いものから)METRIC_NAMESPACE: 環境変数のLOG_GROUP_NAMEからスタック名を抽出して名前空間に使います
Step 4: samconfig.toml を作成する
samconfig.toml は .gitignore で管理外のため、毎回手動で作成する必要があります。
chatbot-log-api/samconfig.toml を新規作成して以下を貼り付けます。
version = 0.1
[default.deploy.parameters]
stack_name = "chatbot-log-api-stack"
resolve_s3 = true
s3_prefix = "chatbot-log-api-stack"
region = "ap-northeast-1"
confirm_changeset = true
capabilities = "CAPABILITY_IAM"
disable_rollback = true
image_repositories = []
[default.global.parameters]
region = "ap-northeast-1"
samconfig.tomlの置き場所:chatbot-log-api/フォルダの直下に置く必要があります。
Step 5: sam build(ビルド)
cd C:\my-aws\aws-learning-projects\chatbot-log-api
sam build成功すると以下のように表示されます。
Build Succeeded
Built Artifacts : .aws-sam/build
Built Template : .aws-sam/build/template.yaml
sam buildが行っていること:src/フォルダを ZIP パッケージ化して.aws-sam/build/に配置し、Lambda デプロイの準備を整えます。
Step 6: sam deploy(デプロイ)
sam deploy変更内容が表示されて確認を求められます。
Deploy this changeset? [y/N]: yy を入力して進めます。数分でデプロイが完了します。
デプロイ完了の確認
ターミナルに Outputs が表示されます。
Outputs
----------------------------------------------------------------------
Key ApiUrl
Value https://XXXXXXXXXX.execute-api.ap-northeast-1.amazonaws.com/Prod
Key LogGroupName
Value /chatbot/chatbot-log-api-stack
Key MetricNamespace
Value ChatBot/chatbot-log-api-stack
----------------------------------------------------------------------ApiUrl を控えておきます。
デプロイされるリソース一覧
API Gateway REST API × 1 : chatbot-log-api-stack
Lambda 関数 × 1 : chatbot-log-api-stack-ChatLogFunction-XXXX
CloudWatch Log Group × 1 : /chatbot/chatbot-log-api-stack
CloudWatch Metric Filter : ChatErrorFilter(error ログ検出)
IAM ロール × 1 : Lambda 用
S3 : SAM デプロイパッケージStep 7: 動作テスト
事前準備: API URL を変数に設定する
set API_URL=https://XXXXXXXXXX.execute-api.ap-northeast-1.amazonaws.com/Prod
XXXXXXXXXXを Outputs のApiUrlの値に置き換えてください。
テスト 1: ログを記録する(POST /logs)
curl -X POST "%API_URL%/logs" ^
-H "Content-Type: application/json" ^
-d "{\"session_id\": \"session-001\", \"user_id\": \"user001\", \"message\": \"今日の天気は?\", \"response\": \"東京は晴れです\", \"level\": \"info\"}"期待するレスポンス:
{
"session_id": "session-001",
"log_stream": "session/session-001",
"message": "ログを記録した"
}テスト 2: エラーログを記録する(Metric Filter のテスト)
curl -X POST "%API_URL%/logs" ^
-H "Content-Type: application/json" ^
-d "{\"session_id\": \"session-001\", \"user_id\": \"user001\", \"message\": \"注文履歴を見せて\", \"response\": \"エラーが発生しました\", \"level\": \"error\"}"
level: "error"のログが CloudWatch Logs に書き込まれると、
Metric Filter が反応してChatErrorCountメトリクスが +1 されます(数分後に反映)。
テスト 3: ログを取得する(GET /logs)
curl "%API_URL%/logs?session_id=session-001"期待するレスポンス:
{
"session_id": "session-001",
"log_count": 2,
"logs": [
{
"timestamp_ms": 1700000000000,
"log": {
"session_id": "session-001",
"user_id": "user001",
"message": "今日の天気は?",
"response": "東京は晴れです",
"level": "info"
}
},
{
"timestamp_ms": 1700000010000,
"log": {
"session_id": "session-001",
"user_id": "user001",
"message": "注文履歴を見せて",
"response": "エラーが発生しました",
"level": "error"
}
}
]
}Step 8: AWSコンソールで確認(任意)
SAMでデプロイしたリソースはコンソールでも確認できます。
CloudWatch Logs でログを確認する
CloudWatch → ロググループ → /chatbot/chatbot-log-api-stack → ログストリーム
session/session-001ストリームに会話ログが JSON で記録されています
CloudWatch Logs Insights でクエリを実行する
CloudWatch → Logs Insights → ロググループに /chatbot/chatbot-log-api-stack を選択
時間範囲を「直近1時間」など幅のある範囲に設定してからクエリを実行します。
注意: 開始時刻と終了時刻が同じだとエラー
Query's end date and time is either before the log groups creation time...が出ます。カレンダーアイコンで時間幅を確保してください。
エラーログだけを抽出するクエリ:
fields @timestamp, session_id, user_id, level, message
| filter level = "error"
| sort @timestamp desc
| limit 20セッション別メッセージ数を集計するクエリ:
stats count(*) as message_count by session_id
| sort message_count descカスタムメトリクスを確認する
CloudWatch → メトリクス → すべてのメトリクス → カスタム名前空間 → ChatBot/chatbot-log-api-stack
| メトリクス名 | 説明 | 発生元 |
|---|---|---|
ChatErrorCount | error ログが書き込まれるたびに +1 | Metric Filter(自動) |
MessageCount | POST /logs が呼ばれるたびに +1 | Lambda の put_metric_data |
Step 9: リソースの削除
課金を止めるために、ハンズオン完了後は必ずリソースを削除してください。
sam delete --stack-name chatbot-log-api-stack --region ap-northeast-1Are you sure you want to delete the stack chatbot-log-api-stack? [y/N]: y
Are you sure you want to delete the folder chatbot-log-api-stack in S3? [y/N]: y削除完了の確認:
aws cloudformation describe-stacks --stack-name chatbot-log-api-stack --region ap-northeast-1Stack with id chatbot-log-api-stack does not exist が表示されれば削除完了。
削除されるリソース一覧(
sam deleteで自動削除):
- API Gateway(REST API + Prod ステージ)
- Lambda 関数
- CloudWatch Log Group(
/chatbot/chatbot-log-api-stack)— ログも含めて削除- CloudWatch Metric Filter
- IAM ロール
手動削除が必要なリソース:
- Lambda 実行ログ(
/aws/lambda/chatbot-log-api-stack-ChatLogFunction-xxxx)
Lambda が初回実行時に自動作成するロググループで、CloudFormation の管理外のためsam deleteでは削除されません。
CloudWatch → ログ管理 → 該当ロググループを選択 → アクション → 削除 で手動削除します。- CloudWatch カスタムメトリクス(
MessageCount、ChatErrorCount)は独立して管理されます。
不要な場合は CloudWatch → メトリクス → アクション → 削除で個別に削除します(コストはかかりにくいため省略可)。
トラブルシューティング
| 症状 | 原因 | 対処 |
|---|---|---|
sam deploy で ROLLBACK_COMPLETE | template.yaml の構文エラー | sam validate でテンプレートを検証する |
POST で {"message": "Internal server error"} | Lambda 実行エラー | aws logs tail /aws/lambda/chatbot-log-api-stack-ChatLogFunction-XXXX --follow でエラーを確認 |
AccessDeniedException: logs:PutLogEvents | IAM ポリシーが適用されていない | sam deploy を再実行する |
GET で {"log_count": 0} | session_id が一致していない | POST レスポンスの session_id をそのまま GET に使う |
| Metric Filter が反映されない | 数分かかる | 5〜10 分後に CloudWatch メトリクスを再確認する |
InvalidParameterException (put_log_events) | timestamp が不正 | ミリ秒 Unix タイム(int(time.time() * 1000))を使っているか確認 |
sam build で Build Failed | src/app.py が存在しない | chatbot-log-api/src/app.py が存在するか確認する |
まとめ
今回のハンズオンで実現したこと:
| 確認項目 | 内容 |
|---|---|
| API Gateway + Lambda 連携 | Events: Type: Api でPOST / GETを定義し、Lambda プロキシ統合で一元処理 |
| Log Group の保持期間管理 | RetentionInDays: 7 で7日間保持を確実に設定 |
| Metric Filter | FilterPattern: '{ $.level = "error" }' でエラーログを自動メトリクス化 |
| IAM 権限のスコープ制限 | !GetAtt ChatLogGroup.Arn で特定ロググループのみに権限を絞り込み |
| 一括削除 | sam delete でAPI Gateway / Lambda / Log Group / IAM を一括クリーンアップ |
SAMのメリットを実感できたポイント
!GetAtt ChatLogGroup.Arnにより、IAM ポリシーの ARN をハードコードせず動的に解決できるRetentionInDaysやMetric Filterをコードで管理することで、設定漏れ・設定ミスを防げるsam deleteでコンソール版では手動削除が必要な複数リソースを1コマンドで削除できる
コンソール版と比較してみる
SAMが裏で何をやっているか、同じ構成をAWSコンソールのみで構築する手順をまとめました。コンソール版では IAM インラインポリシーの ARN を手動入力する必要がある仕組みや、Metric Filter の GUI 設定方法など、コンソールならではの操作を解説しています。
コメント