0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

AWS API Gateway × Lambda × Bedrock 連携でHTMLから画像を送付する手順まとめ

Last updated at Posted at 2025-10-16

AWSのBedrockを、API Gateway経由でLambdaから呼び出す手順を整理しました。
Claude 3.7 Sonnetモデルを使い、日本語応答を得るまでの構成を実際に構築したときのメモです。

image.png

💡 構成概要

  • HTML → API Gateway → Lambda → Bedrock(Claude 3.7 Sonnet)
  • リージョン:ap-northeast-1(東京)
  • モデルID:apac.anthropic.claude-3-7-sonnet-20250219-v1:0

🧾 手順一覧

前提条件:東京リージョンでBedrockが使用できるようになっていること
1 Lambda関数作成
2 IAMロール変更(Bedrockアクセス許可)
3 タイムアウト延長(3秒→30秒)
4 Lambdaコード実装・デプロイ
5 Lambdaテスト
6 API Gateway作成(REST API)
7 リソース 作成
8 ANYメソッド追加
9 CORSを有効化
10 画像送信設定
11 デプロイ&ステージ作成
12 実行URL確認
13 curlでOPTIONS確認
14 HTMLフォームで実行テスト
15 画像送信対応(multipart/form-data対応)
16 CloudWatchでログ確認

手順

1 Lambda関数作成

image.png

2 IAMロール変更(Bedrockアクセス許可)

image.png

image.png

image.png

3 タイムアウト延長(3秒→30秒)

★画像は15分だけど30秒でいいかも

image.png

4 Lambdaコード実装・デプロイ

Lambdaコードを張り付けてデプロイする

image.png

🐍 Lambda関数(Python)

import json
import boto3
import base64
from email.parser import BytesParser
from email.policy import default

def lambda_handler(event, context):
    # --- OPTIONS プリフライト対応 ---
    if event.get("requestContext", {}).get("http", {}).get("method") == "OPTIONS":
        return {
            "statusCode": 200,
            "headers": cors_headers(),
            "body": json.dumps({"message": "CORS OK"})
        }

    bedrock = boto3.client("bedrock-runtime", region_name="ap-northeast-1")
    model_id = "apac.anthropic.claude-3-7-sonnet-20250219-v1:0"
    system_prompt = "必ず日本語で答えてください"
    max_tokens = 1000

    user_input = ""
    images = []

    try:
        headers = event.get("headers", {}) or {}
        content_type = headers.get("content-type") or headers.get("Content-Type")
        body_raw = event.get("body", "")
        is_b64 = event.get("isBase64Encoded", False)

        # --- multipart/form-data の場合 ---
        if content_type and "multipart/form-data" in str(content_type).lower():
            if isinstance(body_raw, bytes):
                body_bytes = body_raw
            elif is_b64:
                body_bytes = base64.b64decode(body_raw)
            else:
                body_bytes = bytes(body_raw, "latin-1", errors="ignore")

            parser = BytesParser(policy=default)
            msg = parser.parsebytes(b"Content-Type: " + content_type.encode() + b"\n\n" + body_bytes)

            for part in msg.iter_parts():
                disp = part.get("Content-Disposition", "")
                if "form-data" not in disp:
                    continue

                name = part.get_param("name", header="content-disposition")
                if name == "message":
                    raw = part.get_payload(decode=True)
                    if raw:
                        user_input = raw.decode(errors="ignore").strip()

                elif name == "images":
                    mime = part.get_content_type()
                    if mime not in ["image/png", "image/jpeg"]:
                        continue
                    data = part.get_payload(decode=True)
                    b64 = base64.b64encode(data).decode().replace("\n", "")
                    images.append({"mime": mime, "data": b64})

        # --- JSON 形式の場合 ---
        elif body_raw:
            body_data = json.loads(body_raw)
            user_input = body_data.get("message", "")
            if "images" in body_data:
                for img in body_data["images"]:
                    images.append({"mime": "image/png", "data": img})

    except Exception as e:
        return error_response(f"リクエスト解析エラー: {str(e)}")

    # --- テキスト未入力対策 ---
    if not user_input.strip():
        user_input = "画像を解析してください。"

    # --- Claude 入力構築 ---
    user_content = [{"type": "text", "text": user_input}]
    for img in images:
        user_content.append({
            "type": "image",
            "source": {
                "type": "base64",
                "media_type": img["mime"],
                "data": img["data"]
            }
        })

    payload = {
        "anthropic_version": "bedrock-2023-05-31",
        "system": system_prompt,
        "max_tokens": max_tokens,
        "messages": [{"role": "user", "content": user_content}]
    }

    # --- デバッグ出力 ---
    print("=== PAYLOAD HEAD ===", json.dumps(payload)[:500])

    try:
        response = bedrock.invoke_model(modelId=model_id, body=json.dumps(payload))
        body = response.get("body")
        if hasattr(body, "read"):
            body = body.read()
        res_json = json.loads(body)
        output_text = res_json["content"][0]["text"]

        return {
            "statusCode": 200,
            "headers": cors_headers(),
            "body": json.dumps({"reply": output_text}, ensure_ascii=False)
        }

    except Exception as e:
        print("Error:", e)
        return {
            "statusCode": 500,
            "headers": cors_headers(),
            "body": json.dumps({"error": str(e)}, ensure_ascii=False)
        }


def error_response(msg):
    return {
        "statusCode": 400,
        "headers": cors_headers(),
        "body": json.dumps({"error": msg}, ensure_ascii=False)
    }


def cors_headers():
    return {
        "Content-Type": "application/json",
        "Access-Control-Allow-Origin": "*",
        "Access-Control-Allow-Methods": "OPTIONS,POST,GET",
        "Access-Control-Allow-Headers": "Content-Type,x-amz-date,authorization,x-api-key,x-amz-security-token"
    }

5 Lambdaテスト

LamdaからBedrockに通信できることを確認
この時点で画像添付はできないので、後で確認する

image.png

"message": "これはテストです。"

image.png

6 API Gateway作成(REST API)

image.png

7 リソース 作成

image.png

8 POSTメソッド追加

ai階層で作成する

image.png

image.png

image.png

9 CORSを有効化

image.png

image.png

10 画像送信設定

image.png

image.png

11 デプロイ&ステージ作成

ここで初めてステージを作成している

image.png

12 実行URL確認

image.png

13 curlでOPTIONS確認

curl -i -X OPTIONS https://XXXXXXXXXX.execute-api.ap-northeast-1.amazonaws.com/dev/ai

image.png

14 HTMLフォームで実行テスト

curlでの確認で「できた」と思ったが
htmlで実行すると、CORS関連エラーと画像添付できない問題が発生していた。

image.png

🌐 HTMLテストページ

★const API_URL に自身のBedrockAPIアドレスを入れる。

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>Bedrock API テスト(複数画像対応)</title>
  <style>
    body { font-family: sans-serif; padding: 20px; }
    textarea { width: 100%; height: 100px; }
    button { padding: 8px 16px; margin-top: 10px; }
    #reply { margin-top: 15px; white-space: pre-wrap; border: 1px solid #ccc; padding: 10px; }
    #preview img { max-width: 150px; margin: 5px; border: 1px solid #ccc; }
  </style>
</head>
<body>
  <h2>Bedrock API テスト(複数画像対応)</h2>

  <textarea id="inputText" placeholder="メッセージを入力..."></textarea><br>
  
  <!-- ✅ 複数画像選択対応 -->
  <input type="file" id="imageInput" multiple accept="image/*"><br>
  
  <!-- ✅ 選択済み画像のプレビュー -->
  <div id="preview"></div>
  
  <button id="sendBtn">送信</button>
  <div id="reply"></div>

  <script>
    const API_URL = "https://xxxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/ステージ/リソース";
    const btn = document.getElementById("sendBtn");
    const input = document.getElementById("inputText");
    const imageInput = document.getElementById("imageInput");
    const reply = document.getElementById("reply");
    const preview = document.getElementById("preview");

    // --- 画像プレビュー ---
    imageInput.onchange = () => {
      preview.innerHTML = "";
      for (const file of imageInput.files) {
        const img = document.createElement("img");
        img.src = URL.createObjectURL(file);
        preview.appendChild(img);
      }
    };

    // --- 送信処理 ---
    btn.onclick = async () => {
      const msg = input.value.trim();
      const files = imageInput.files;

      if (!msg && files.length === 0) {
        reply.textContent = "⚠️ テキストか画像を入力してください。";
        return;
      }

      reply.textContent = "送信中...";
      btn.disabled = true; // 二重送信防止

      const formData = new FormData();
      formData.append("message", msg);

      // ✅ 複数画像を1つずつ追加
      for (let i = 0; i < files.length; i++) {
        formData.append("images", files[i], files[i].name);
      }

      try {
        const res = await fetch(API_URL, {
          method: "POST",
          body: formData // Content-Typeは自動設定(boundary含む)
        });

        const text = await res.text();
        let message = "";
        try {
          const data = JSON.parse(text);
          message = data.reply || data.error || text;
        } catch {
          message = text;
        }

        reply.textContent = message;
      } catch (e) {
        reply.textContent = "通信エラー: " + e.message;
      } finally {
        btn.disabled = false;
      }
    };
  </script>
</body>
</html>

15 画像送信対応(multipart/form-data対応)

通常ローカルでの送信はCORSでlocalhostを認識できないため、
ブラウザ経由ではエラーになるが、これまでの設定で回避している。

image.png

16 CloudWatchでログ確認

エラーが出た場合の確認はClowdWatchでの確認を行う。
(Lamda上で[$LATEST]を記載し、検索しやすくしている)

  • START RequestId 〜 END RequestId の間に、Bedrock応答テキストが出力されているか確認

image.png

image.png

🧩 注意点

  • Bedrockは応答がやや遅いので、Lambdaのタイムアウトを30秒以上に設定すること。
  • API GatewayでCORSを有効化しないと、ブラウザからPOSTできない。
  • multipart/form-dataを扱う場合は、Lambdaをboto3+base64対応に改修する必要あり。
    (今回のLambdaは改修済み)

✅ まとめ

この構成を使えば、HTMLフォームから画像添付し、claude3.7の東京リージョンに
送付が可能となります。

  • CORS・タイムアウト・IAMロールの3点を調整することで安定動作します。
0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?