AWS SAM でチャットボットログAPIを構築しよう【API Gateway + Lambda + CloudWatch Logs ハンズオン / コンソール版との比較付き】

AWS Basic
スポンサーリンク
スポンサーリンク

はじめに

「チャットボットのログ収集基盤をコードで管理して、同じ環境を何度でも再現したい」という要件に、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 deploy2コマンドで全リソースをデプロイ

このハンズオンの特徴:

  • API Gateway + Lambda × 1 + CloudWatch Log Group + Metric Filter + IAM ロールを template.yaml 1ファイルで管理
  • sam delete全リソースを一括削除(コンソール版では API Gateway / Lambda / Log Group / IAM を個別に削除)

Amazon検索[本 AWS 開発]

スポンサーリンク

SAM vs コンソール:どれだけ違うか

比較項目SAM(コード)コンソール(手動)
Log Group の保持期間RetentionInDays: 7 で定義作成時に手動設定
Metric FilterAWS::Logs::MetricFilter で定義ロググループのタブから手動作成
IAM 権限Statement で直接記述(ARN は !GetAtt で自動解決)インラインポリシーに ARN を手動入力
API GatewayEvents: Type: Api で POST / GET を1行ずつ定義リソース・メソッド・デプロイを個別に操作
削除sam delete 1コマンドAPI Gateway / Lambda / Log Group / IAM を個別に削除
再現性チームで同じ環境を即座に再現できる手順書が必要

スポンサーリンク

キーワード解説

用語意味
CloudWatch Log Groupログの論理的なまとまり。SAM では RetentionInDays を設定できる
Log StreamLog Group の中の個別のログストリーム。今回はセッションIDごとに作成する
Metric Filterログの JSON フィールドをマッチさせ、カスタムメトリクスを自動生成する仕組み
Lambda プロキシ統合API Gateway が HTTP リクエスト全体を Lambda に渡す最もシンプルな統合方式
!GetAttCloudFormation 組み込み関数。リソースの属性値(ARN など)を参照する
!SubCloudFormation 組み込み関数。文字列の中に変数を埋め込む

前提条件

ツール確認コマンド最低バージョン目安
AWS CLI v2aws --version2.x
AWS SAM CLIsam --version1.x
Python 3.12python --version3.12

AWS認証確認:

aws sts get-caller-identity

アカウントIDが表示されれば認証設定済みです。


使用するAWSサービス

サービス役割料金
API GatewayREST APIのエンドポイント(POST /logs, GET /logs)月100万リクエストまで無料
Lambdaログの書き込み・取得処理(Python 3.12)月100万リクエスト・400,000 GB-秒まで無料
CloudWatch Logsチャットログの保存・クエリ月5GBまで無料
CloudWatch メトリクスエラーログのカスタムメトリクス化月10件まで無料
IAMLambda の実行権限管理無料
S3SAM デプロイパッケージ置き場少量のため実質無料

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: get

template.yaml のポイント:

ポイント説明
AWS::Logs::LogGroupSAMでロググループを明示定義することで RetentionInDays を設定できる。未定義だと Lambda が自動作成するが保持期間は無期限
AWS::Logs::MetricFilterJSON フィルターパターンで特定フィールドのログを検出してメトリクスに変換する
FilterPattern: '{ $.level = "error" }'CloudWatch Logs の JSON フィルター構文。$ で JSON ルートを表す
Statement: logs:PutLogEventsSAM組み込みポリシーに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]: y

y を入力して進めます。数分でデプロイが完了します。

デプロイ完了の確認

ターミナルに 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

メトリクス名説明発生元
ChatErrorCounterror ログが書き込まれるたびに +1Metric Filter(自動)
MessageCountPOST /logs が呼ばれるたびに +1Lambda の put_metric_data


Step 9: リソースの削除

課金を止めるために、ハンズオン完了後は必ずリソースを削除してください。

sam delete --stack-name chatbot-log-api-stack --region ap-northeast-1
Are 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-1

Stack 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 カスタムメトリクス(MessageCountChatErrorCount)は独立して管理されます。
    不要な場合は CloudWatch → メトリクス → アクション → 削除で個別に削除します(コストはかかりにくいため省略可)。

トラブルシューティング

症状原因対処
sam deployROLLBACK_COMPLETEtemplate.yaml の構文エラーsam validate でテンプレートを検証する
POST で {"message": "Internal server error"}Lambda 実行エラーaws logs tail /aws/lambda/chatbot-log-api-stack-ChatLogFunction-XXXX --follow でエラーを確認
AccessDeniedException: logs:PutLogEventsIAM ポリシーが適用されていない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 buildBuild Failedsrc/app.py が存在しないchatbot-log-api/src/app.py が存在するか確認する

まとめ

今回のハンズオンで実現したこと:

確認項目内容
API Gateway + Lambda 連携Events: Type: Api でPOST / GETを定義し、Lambda プロキシ統合で一元処理
Log Group の保持期間管理RetentionInDays: 7 で7日間保持を確実に設定
Metric FilterFilterPattern: '{ $.level = "error" }' でエラーログを自動メトリクス化
IAM 権限のスコープ制限!GetAtt ChatLogGroup.Arn で特定ロググループのみに権限を絞り込み
一括削除sam delete でAPI Gateway / Lambda / Log Group / IAM を一括クリーンアップ

SAMのメリットを実感できたポイント

  • !GetAtt ChatLogGroup.Arn により、IAM ポリシーの ARN をハードコードせず動的に解決できる
  • RetentionInDaysMetric Filter をコードで管理することで、設定漏れ・設定ミスを防げる
  • sam delete でコンソール版では手動削除が必要な複数リソースを1コマンドで削除できる

コンソール版と比較してみる

SAMが裏で何をやっているか、同じ構成をAWSコンソールのみで構築する手順をまとめました。コンソール版では IAM インラインポリシーの ARN を手動入力する必要がある仕組みや、Metric Filter の GUI 設定方法など、コンソールならではの操作を解説しています。


関連記事

Amazon検索[本 AWS 開発]

コメント