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

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

はじめに

「チャットボットの会話ログをAPIで受け取り、AWSに蓄積したい」——そんな要件を実現する構成が API Gateway + Lambda + CloudWatch Logs の組み合わせです。

この記事では、AWSコンソールのみを使って、チャットボットの会話ログを記録・取得するREST APIをゼロから構築するハンズオンを紹介します。

クライアント
  ↓ POST /logs(会話ログを送信)
API Gateway(REST API)
  ↓ Lambda プロキシ統合
Lambda(ChatLogFunction / Python 3.12)
  ↓ boto3 logs.put_log_events()
CloudWatch Log Group(/chatbot/chatbot-logs)
  ↓ Metric Filter(level = "error" のログを検出)
CloudWatch カスタムメトリクス(ChatErrorCount)

このハンズオンで体験できること:

  • API Gateway REST API のリソース・メソッド作成と Lambda プロキシ統合
  • CloudWatch Logs カスタムロググループへのログ書き込み(put_log_events
  • Metric Filter によるエラーログの自動メトリクス変換
  • CloudWatch Logs Insights でのログクエリ実行

この記事は SAM版ハンズオン の比較記事です。
コンソール操作でAPI Gateway・Lambda・CloudWatch Logsの連携を視覚的に学び、SAM と何が違うのかを比較したい方向けです。


Amazon検索[本 AWS 開発]

スポンサーリンク

キーワード解説

用語意味
CloudWatch Log Groupログの論理的なまとまり。今回はチャットログ専用グループを作成する
Log StreamLog Group の中の個別のログストリーム。今回はセッションIDごとに作成する
put_log_eventsboto3 でカスタムロググループにログを書き込む API
Metric Filterロググループに届いたログを JSON フィルターでマッチさせ、カスタムメトリクスを自動生成する仕組み
カスタムメトリクスCloudWatch に独自のメトリクスを記録する機能。アラーム設定やダッシュボードに使える
Lambda プロキシ統合API Gateway が HTTP リクエスト全体を Lambda に渡す最もシンプルな統合方式

スポンサーリンク

使用するAWSサービス

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

全体の作業順序

① CloudWatch Log Group を作成する
      ↓
② CloudWatch Metric Filter を作成する(error ログ → メトリクス)
      ↓
③ Lambda 関数を作成する(コード・環境変数・タイムアウト設定)
      ↓
④ Lambda の実行ロールに権限を追加する
  (CloudWatch Logs: 書き込み・読み取り / CloudWatch: PutMetricData)
      ↓
⑤ API Gateway REST API を作成する(POST /logs, GET /logs)
      ↓
⑥ 動作テスト(curl でログ送信 → コンソールで確認)
      ↓
⑦ リソースの削除

Log Group を先に作る理由:
Metric Filter の作成にはロググループが必要なため、最初に作成します。


① CloudWatch Log Group を作成する

AWSコンソール → CloudWatch → ロググループ → 「ロググループの作成」

設定項目
ロググループ名/chatbot/chatbot-logs
保持期間7日間

「作成」をクリック。

控えておく情報:

  • ロググループ名: /chatbot/chatbot-logs

② CloudWatch Metric Filter を作成する

エラーログを自動的にカスタムメトリクスに変換するフィルターを設定します。

CloudWatch → ロググループ → /chatbot/chatbot-logs → 「メトリクスフィルター」タブ → 「メトリクスフィルターを作成」

フィルターパターンの設定

設定項目
フィルターパターン{ $.level = "error" }

「パターンのテスト」でテストデータを入力して確認できます(任意)。「次へ」をクリック。

メトリクスの設定

設定項目
フィルター名ChatErrorFilter
メトリクスの名前空間ChatBot/chatbot-logs
メトリクス名ChatErrorCount
メトリクス値1
デフォルト値0

「次へ」→「メトリクスフィルターを作成」。

Metric Filter の仕組み:
CloudWatch Logs に届いたログが { $.level = "error" } にマッチするたびに、
ChatBot/chatbot-logs 名前空間の ChatErrorCount1 が加算されます。
ログに {"level": "error", ...} という JSON が含まれていればマッチします。


③ Lambda 関数を作成する

AWSコンソール → Lambda → 「関数の作成」

設定項目
作成方法一から作成
関数名ChatLogFunction
ランタイムPython 3.12
アーキテクチャx86_64
実行ロール「基本的な Lambda アクセス権限で新しいロールを作成」(デフォルト)

「関数の作成」をクリック。

タイムアウトの設定

「設定」タブ → 「一般設定」→「編集」

設定項目
タイムアウト10

「保存」をクリック。

環境変数の設定

「設定」タブ → 「環境変数」→「編集」→「環境変数を追加」

キー
LOG_GROUP_NAME/chatbot/chatbot-logs

「保存」をクリック。

コードの入力

「コード」タブ → lambda_function.py を開いて既存の内容を全て置き換えます。

import json
import os
import time
import uuid

import boto3
from botocore.exceptions import ClientError

logs_client = boto3.client("logs")
cloudwatch = boto3.client("cloudwatch")

LOG_GROUP_NAME = os.environ["LOG_GROUP_NAME"]
METRIC_NAMESPACE = "ChatBot/" + LOG_GROUP_NAME.split("/")[-1]  # "ChatBot/chatbot-logs"


def lambda_handler(event, context):
    http_method = event.get("httpMethod", "")
    resource = event.get("resource", "/")

    if http_method == "POST" and resource == "/logs":
        return post_log(event)
    elif http_method == "GET" and resource == "/logs":
        return get_logs(event)
    else:
        return _response(404, {"error": "Not Found"})


def post_log(event):
    try:
        body = json.loads(event.get("body") or "{}")
    except json.JSONDecodeError:
        return _response(400, {"error": "Invalid JSON"})

    session_id = body.get("session_id") or str(uuid.uuid4())
    user_id = body.get("user_id", "anonymous")
    message = body.get("message", "")
    response_text = body.get("response", "")
    level = body.get("level", "info")

    log_entry = {
        "session_id": session_id,
        "user_id": user_id,
        "message": message,
        "response": response_text,
        "level": level,
    }

    log_stream_name = f"session/{session_id}"
    _ensure_log_stream(log_stream_name)  # ストリームを作成(既存なら無視)
    logs_client.put_log_events(
        logGroupName=LOG_GROUP_NAME,
        logStreamName=log_stream_name,
        logEvents=[
            {
                "timestamp": int(time.time() * 1000),  # ミリ秒単位の Unix 時間
                "message": json.dumps(log_entry, ensure_ascii=False),
            }
        ],
    )

    cloudwatch.put_metric_data(
        Namespace=METRIC_NAMESPACE,
        MetricData=[
            {
                "MetricName": "MessageCount",
                "Dimensions": [{"Name": "Level", "Value": level}],
                "Value": 1,
                "Unit": "Count",
            }
        ],
    )

    return _response(200, {
        "session_id": session_id,
        "log_stream": log_stream_name,
        "message": "ログを記録した",
    })


def get_logs(event):
    params = event.get("queryStringParameters") or {}
    session_id = params.get("session_id")
    limit = min(int(params.get("limit", "20")), 100)

    if not session_id:
        return _response(400, {"error": "session_id クエリパラメーターは必須"})

    log_stream_name = f"session/{session_id}"

    try:
        resp = logs_client.get_log_events(
            logGroupName=LOG_GROUP_NAME,
            logStreamName=log_stream_name,
            limit=limit,
            startFromHead=False,  # 最新のログから取得
        )
        events = []
        for e in resp["events"]:
            try:
                msg = json.loads(e["message"])
            except json.JSONDecodeError:
                msg = e["message"]
            events.append({"timestamp_ms": e["timestamp"], "log": msg})
    except ClientError as e:
        if e.response["Error"]["Code"] == "ResourceNotFoundException":
            events = []  # ログストリームが存在しない場合は空リストを返す
        else:
            raise

    return _response(200, {
        "session_id": session_id,
        "log_count": len(events),
        "logs": events,
    })


def _ensure_log_stream(log_stream_name: str) -> None:
    try:
        logs_client.create_log_stream(
            logGroupName=LOG_GROUP_NAME,
            logStreamName=log_stream_name,
        )
    except ClientError as e:
        if e.response["Error"]["Code"] != "ResourceAlreadyExistsException":
            raise  # 既存ストリームは無視、それ以外のエラーは再送出


def _response(status_code: int, body: dict) -> dict:
    return {
        "statusCode": status_code,
        "headers": {"Content-Type": "application/json"},
        "body": json.dumps(body, ensure_ascii=False),
    }

「Deploy」ボタンをクリックしてコードを保存します。


④ Lambda の実行ロールに権限を追加する

デフォルトの実行ロールは CloudWatch Logs への書き込み(Lambdaの実行ログ出力)のみです。
今回はカスタムロググループへの書き込み・読み取りと、カスタムメトリクスへの書き込み権限を追加します。

Lambda → ChatLogFunction → 「設定」タブ → 「アクセス権限」→ 実行ロール名のリンクをクリック

(例: ChatLogFunction-role-XXXX)→ IAM コンソールのロール画面が開きます。

CloudWatch Logs と CloudWatch の権限をまとめて追加する

「許可を追加」→「インラインポリシーを作成」→「JSON」タブを選択して以下を入力します。

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "logs:CreateLogStream",
        "logs:PutLogEvents",
        "logs:GetLogEvents",
        "logs:DescribeLogStreams"
      ],
      "Resource": [
        "arn:aws:logs:ap-northeast-1:YOUR_ACCOUNT_ID:log-group:/chatbot/chatbot-logs",
        "arn:aws:logs:ap-northeast-1:YOUR_ACCOUNT_ID:log-group:/chatbot/chatbot-logs:*"
      ]
    },
    {
      "Effect": "Allow",
      "Action": "cloudwatch:PutMetricData",
      "Resource": "*"
    }
  ]
}

YOUR_ACCOUNT_ID を自分の AWS アカウント ID(12桁)に置き換えてください。
アカウント ID は aws sts get-caller-identity で確認できます。

「次へ」→ ポリシー名: ChatLogPolicy → 「ポリシーを作成」。

なぜ 2 種類の権限が必要か:

  • logs:PutLogEventsput_log_events() でカスタムロググループにログを書き込む
  • logs:GetLogEventsget_log_events() でログを読み取る
  • cloudwatch:PutMetricDataput_metric_data() でカスタムメトリクスを記録する

⑤ API Gateway REST API を作成する

5-1. API の作成

AWSコンソール → API Gateway → 「APIを作成」→ 「REST API」→「構築」

設定項目
API タイプREST API(WebSocket ではない方)
API 名ChatLogAPI
エンドポイントタイプリージョン

「API を作成」をクリック。

5-2. /logs リソースを作成する

「リソース」→「リソースを作成」

設定項目
リソース名logs
リソースパス/logs

「リソースを作成」をクリック。

5-3. POST メソッドを作成する(ログ書き込み)

/logs リソースを選択した状態で「メソッドを作成」。

設定項目
メソッドタイプPOST
統合タイプLambda 関数
Lambda プロキシ統合有効(チェックを入れる)
Lambda 関数ChatLogFunction

「メソッドを作成」をクリック。

5-4. GET メソッドを作成する(ログ取得)

同様に GET メソッドを追加します。設定は POST と同じ(Lambda 関数: ChatLogFunction)。

5-5. API をデプロイする

「API をデプロイ」→

設定項目
ステージ「新しいステージ」
ステージ名Prod

「デプロイ」をクリック。

デプロイ後に表示される URL を控えておきます:

https://XXXXXXXXXX.execute-api.ap-northeast-1.amazonaws.com/Prod

⑥ 動作テスト

事前準備: API URL を変数に設定する

set API_URL=https://XXXXXXXXXX.execute-api.ap-northeast-1.amazonaws.com/Prod

XXXXXXXXXX を ⑤ でデプロイ後に表示された URL の値に置き換えてください。


テスト 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\"}"

このリクエスト後、CloudWatch メトリクス ChatBot/chatbot-logs > 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"
      }
    }
  ]
}


CloudWatch でログを確認する

CloudWatch → ロググループ → /chatbot/chatbot-logs → ログストリーム

  • session/session-001 ストリームをクリック
  • 送信した会話ログが JSON 形式で記録されていることを確認します

CloudWatch Logs Insights でクエリを実行する(任意)

CloudWatch → Logs Insights

ロググループに /chatbot/chatbot-logs を選択し、時間範囲を「直近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

エラーログだけを抽出して確認できます。

カスタムメトリクスを確認する(任意)

CloudWatch → メトリクス → すべてのメトリクス → カスタム名前空間 → ChatBot/chatbot-logs

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


⑦ リソースの削除

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

1. API Gateway を削除する

API Gateway → API → ChatLogAPI → 「削除」→ API 名を入力 → 「削除」

2. Lambda 関数を削除する

Lambda → 関数 → ChatLogFunction → 「アクション」→「削除」

3. CloudWatch Log Group を削除する

CloudWatch → ロググループ → /chatbot/chatbot-logs → 「アクション」→「ロググループを削除」

Metric Filter はロググループを削除すると自動的に削除されます。

4. IAM ロールを削除する(任意)

IAM → ロール → ChatLogFunction-role-XXXX を削除

5. CloudWatch Lambda ロググループを削除する(任意)

CloudWatch → ロগগループ → /aws/lambda/ChatLogFunction → 削除


SAM との対比

SAMの記述コンソールでやること
AWS::Logs::LogGroupCloudWatch → ロググループを作成
AWS::Logs::MetricFilterロググループ → メトリクスフィルターを作成
Statement: logs:PutLogEventsIAM → ロールにインラインポリシーを追加
Statement: cloudwatch:PutMetricData同上(同じポリシーに含める)
Events: Type: Api(POST + GET)API Gateway → リソース・メソッドを作成してデプロイ

コンソール版のつまずきポイント:
IAM のインラインポリシーに YOUR_ACCOUNT_ID を自分のアカウントIDに書き換えるのを忘れると AccessDeniedException が発生します。
SAM版では !GetAtt ChatLogGroup.Arn で自動解決されるため、この手作業が不要です。


トラブルシューティング

症状原因対処
AccessDeniedException: logs:PutLogEventsLambda ロールに権限がない④ の手順でインラインポリシーを追加する
ResourceNotFoundException (Log Group)環境変数のロググループ名が誤っているLOG_GROUP_NAME 環境変数と ① で作成したグループ名を確認する
POST で 500 エラーLambda 実行エラーLambda → 「モニタリング」→「CloudWatch Logs を表示」でエラー詳細を確認する
GET で {"log_count": 0}ログがない or session_id が異なるPOST で使った session_id と一致しているか確認する
Metric Filter が反映されない数分かかる5〜10 分後に CloudWatch メトリクスを再確認する
InvalidParameterException (put_log_events)タイムスタンプが古すぎる or 未来すぎるint(time.time() * 1000) でミリ秒タイムスタンプを生成しているか確認する
API Gateway から 403 が返るステージのデプロイ忘れAPI Gateway → 「API をデプロイ」を再実行する

まとめ

今回のハンズオンで体験できたこと:

確認項目内容
API Gateway + Lambda 連携Lambda プロキシ統合で REST API の POST / GET を1関数で処理
カスタムロググループへの書き込みput_log_events() でセッション単位のログストリームに記録
Metric Filter{ $.level = "error" } でエラーログを自動的にメトリクスへ変換
CloudWatch Logs Insightsクエリでエラーログだけを抽出・確認

コンソール版で実感できたポイント

  • API Gateway のリソース・メソッド・デプロイという3段階の設定が視覚的に理解できる
  • IAM ポリシーのリソース ARN を手動で記述することで、権限スコープの意味が身につく
  • Metric Filter の仕組みを GUI で設定することで、JSON フィルター構文の動作が理解できる

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

コンソールで API Gateway + Lambda + CloudWatch Logs の連携を理解したら、SAM で同じ構成をコードで定義することで「SAM が何を自動化しているか」が明確になります。IAM ポリシーの ARN 解決や Log Group の保持期間設定など、コンソールでの手動操作がコードに対応しています。


関連記事

Amazon検索[本 AWS 開発]

コメント