システム構成図
AWS

SQS×Lambda×DynamoDBで構築 AWS非同期処理システム

NEW 2026年8月24日

目次Category


このハンズオンの目的

急激なアクセスに対応できる、AWSの非同期処理(イベント駆動)
アーキテクチャを構築します
Amazon SQS をバッファとして挟むことで、DynamoDB等の負荷を分散し、
システムの耐障害性を高める設計です

システム構成図

SQS + APIGatewayシステム構成図

 

事前準備(AWSリソースの作成)

ハンズオンを始める前に、データを保存・転送するための土台を作成

■ 1. SQSキューの作成
  • アクセス手順: AWSコンソール > SQS > キューを作成
  • タイプ: 標準キュー
  • キュー名: OJT_OrderQueue_[name]
  • メモ事項: 発行された「URL」(例: https://sqs.ap-northeast-1.amazonaws.com/123456789012/OJT_OrderQueue_name)をメモ
■ 2. DynamoDBテーブルの作成
  • アクセス手順: AWSコンソール > DynamoDB > テーブルを作成
  • テーブル名: OJT_Orders_[name]
  • パーティションキー: order_id(文字列)

ハンズオン手順

Step 1: 受付Lambdaの作成と単体テスト

APIからのリクエストを受け取り、データを作成してSQSにメッセージを投入する

  • 1.1 関数の作成: OJT_OrderReceiverFunction_[name]
  • 1.2 IAMロールの設定: 受付Lambdaの実行ロールに
    AmazonSQSFullAccessを付与
  • 1.3 環境変数の設定: QUEUE_URLに作成した SQS の URL を設定
Python – OJT_OrderReceiverFunction
import json
import os
import uuid
import boto3

sqs = boto3.client('sqs')
# 環境変数から設定
QUEUE_URL = os.environ.get('QUEUE_URL')

def lambda_handler(event, context):
    try:
        # API Gateway または単体テストからの入力を受け取る
        body = json.loads(event.get('body', '{}')) if isinstance(event.get('body'), str) else event.get('body', {})

        # データの生成
        order_id = str(uuid.uuid4())
        order_data = {
            'order_id': order_id,
            'item': body.get('item', 'Unknown'),
            'price': body.get('price', 0),
            'status': 'PENDING'
        }

        # SQSへメッセージ送信
        response = sqs.send_message(
            QueueUrl=QUEUE_URL,
            MessageBody=json.dumps(order_data)
        )

        return {
            'statusCode': 200,
            'headers': {
                'Access-Control-Allow-Origin': '*',
                'Access-Control-Allow-Headers': 'Content-Type',
                'Access-Control-Allow-Methods': 'OPTIONS,POST'
            },
            'body': json.dumps({
                'message': 'Order accepted',
                'order_id': order_id,
                'message_id': response['MessageId']
            })
        }
    except Exception as e:
        return {
            'statusCode': 500,
            'body': json.dumps({'error': str(e)})
        }

単体テスト手順:

  1. Lambdaコンソールの「テスト」タブから新しいテストイベントを作成
  2. 以下のJSONを入力して「テスト」を実行
JSON – テストイベント
{
  "body": "{\"item\": \"Apple\", \"price\": 150}"
}

statusCode: 200が返ることを確認し
SQSコンソールで「利用可能なメッセージ」が1に増えているか確認

Step 2: 処理Lambdaの作成と単体テスト

SQSから自動でメッセージを受け取り、DynamoDBに保存する処理

  • 2.1 関数の作成: OJT_OrderRegisterFunction_[name]
  • 2.2 IAMロールの設定: 処理Lambdaの実行ロールに以下を付与
    • AWSLambdaSQSQueueExecutionRole(SQS受信用)
    • AmazonDynamoDBFullAccess(DynamoDB書き込み用)
  • 2.3 環境変数の設定: TABLE_NAMEOJT_Orders_[name]を設定
Python – OJT_OrderRegisterFunction
import json
import os
import time  # ※テスト用遅延処理の呼び出しに必要
import boto3

dynamodb = boto3.resource('dynamodb')
TABLE_NAME = os.environ.get('TABLE_NAME')
table = dynamodb.Table(TABLE_NAME)

def lambda_handler(event, context):
    for record in event.get('Records', []):
        body = record['body']

        # SQSからの文字列、または手動テストの辞書型(dict)双方に対応
        order_data = json.loads(body) if isinstance(body, str) else body

        # ステータスを完了に変更
        order_data['status'] = 'COMPLETED'

        # DynamoDBへ書き込み
        table.put_item(Item=order_data)
        print(f"Successfully processed order: {order_data.get('order_id')}")

    return {'statusCode': 200, 'body': 'Success'}

SQSトリガーの設定:
Lambdaの画面で「トリガーを追加」> SQS を選択し、
OJT_OrderQueue_[name]を割り当て

単体テスト手順:
テストイベントのイベントテンプレートで「SQS」を選択し、
以下のように body をJSON文字列で囲んでテスト実行。
(文字列にしないと TypeErrorになります)

JSON – テストイベント
{
  "Records": [
    {
      "body": "{\"order_id\": \"test-123\", \"item\": \"Orange\", \"price\": 200}"
    }
  ]
}

テスト実行後、DynamoDBの OJT_Orders_[name]
テーブルにデータが書き込まれたか確認

Step 3: API Gatewayの統合と結合テスト

受付LambdaをHTTP経由で呼び出せるようAPIを作成

■ API Gatewayの構築
  • API Gateway >「HTTP API」を作成
  • 名前: OJT_OrderAPI_[name]
  • ルートに POST /ordersを追加
  • 統合先に ステップ1の OJT_OrderReceiverFunction_[name]を指定
  • CORS設定 で Access-Control-Allow-Origin: *を設定
■ 結合テスト(Postmanなど)
  • Method: POST
  • URL: https://<your-api-id>.execute-api.ap-northeast-1.amazonaws.com/orders
  • Header: Content-Type: application/json
  • Body (raw JSON):
JSON – Request Body
{
  "item": "Postmanテスト商品",
  "price": 3000
}

送信後、200 OKが返ることと、
DynamoDB にアイテムが追加されることを確認

Step 4: HTMLフロントエンドとの結合および負荷テスト

クライアント画面から連続リクエストを送り、
SQSの「バッファ(緩衝材)」としての挙動を検証

⚠️ 注意: index.html をそのままダブルクリックで開かない

この index.html を file://形式で直接開くと、
ブラウザのCORSポリシーやオリジン制限により Failed to fetch
接続拒否(Connection Refused)のエラーが発生し
API Gatewayへのリクエストが失敗する場合があります

VS Codeの拡張機能「Live Server」などを使い、http://127.0.0.1:5500
のようなローカルサーバー経由(http://オリジン)で
起動してからアクセスしてください

動作確認用フロントエンド
(index.html)

HTML – index.html
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>注文処理テスト (API Gateway + SQS + DynamoDB)</title>
    <style>
        :root {
            --primary-color: #232f3e; /* AWS Dark Blue */
            --accent-color: #ff9900;  /* AWS Orange */
            --bg-color: #f2f3f3;
            --card-bg: #ffffff;
            --text-color: #16191f;
        }

        body {
            font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
            background-color: var(--bg-color);
            color: var(--text-color);
            margin: 0;
            padding: 40px 20px;
            display: flex;
            justify-content: center;
        }

        .container {
            background-color: var(--card-bg);
            border-radius: 8px;
            box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
            padding: 30px;
            width: 100%;
            max-width: 500px;
        }

        h1 {
            font-size: 1.5rem;
            color: var(--primary-color);
            margin-top: 0;
            margin-bottom: 20px;
            border-bottom: 2px solid var(--accent-color);
            padding-bottom: 8px;
        }

        .form-group {
            margin-bottom: 18px;
        }

        label {
            display: block;
            font-weight: 600;
            margin-bottom: 6px;
            font-size: 0.9rem;
        }

        input[type="text"],
        input[type="number"],
        input[type="url"] {
            width: 100%;
            padding: 10px;
            border: 1px solid #ccc;
            border-radius: 4px;
            box-sizing: border-box;
            font-size: 1rem;
        }

        input:focus {
            outline: none;
            border-color: var(--accent-color);
            box-shadow: 0 0 0 2px rgba(255, 153, 0, 0.2);
        }

        .btn-group {
            display: flex;
            gap: 10px;
            margin-bottom: 15px;
        }

        button {
            flex: 1;
            background-color: var(--accent-color);
            color: #ffffff;
            border: none;
            border-radius: 4px;
            padding: 12px;
            font-size: 0.95rem;
            font-weight: bold;
            cursor: pointer;
            transition: background-color 0.2s ease;
        }

        button.burst-btn {
            background-color: #d9534f; /* 赤系のボタン */
        }

        button.burst-btn:hover {
            background-color: #c9302c;
        }

        button:hover {
            background-color: #e68a00;
        }

        button:disabled {
            background-color: #cccccc;
            cursor: not-allowed;
        }

        .response-box {
            margin-top: 25px;
            padding: 15px;
            border-radius: 4px;
            font-size: 0.9rem;
            display: none;
        }

        .response-box.success {
            display: block;
            background-color: #e8f5e9;
            border: 1px solid #a5d6a7;
            color: #1b5e20;
        }

        .response-box.error {
            display: block;
            background-color: #ffebee;
            border: 1px solid #ef9a9a;
            color: #b71c1c;
        }

        .field-title {
            font-weight: bold;
            margin-top: 8px;
        }

        pre {
            background: #f8f9fa;
            padding: 10px;
            border-radius: 4px;
            overflow-x: auto;
            white-space: pre-wrap;
            word-wrap: break-word;
            max-height: 250px;
        }

        .note {
            font-size: 0.8rem;
            color: #666;
            margin-top: 15px;
            line-height: 1.4;
        }
    </style>
</head>
<body>

<div class="container">
    <h1>注文発行テスト</h1>

    <div class="form-group">
        <label for="apiUrl">API Gateway URL</label>
        <input type="url" id="apiUrl" placeholder="https://xxxx.execute-api.ap-northeast-1.amazonaws.com/orders">
    </div>

    <div class="form-group">
        <label for="item">商品名 (item)</label>
        <input type="text" id="item" value="連打テスト商品">
    </div>

    <div class="form-group">
        <label for="price">価格 (price)</label>
        <input type="number" id="price" value="1000">
    </div>

    <div class="btn-group">
        <button id="singleBtn" onclick="sendOrders(1)">1件送信</button>
        <button id="burstBtn" class="burst-btn" onclick="sendOrders(20)">🚀 20件連続送信</button>
    </div>

    <div id="responseBox" class="response-box">
        <div id="statusMessage"></div>
        <div class="field-title">処理結果ログ:</div>
        <pre id="jsonResult"></pre>
    </div>

    <div class="note">
        ※ 「20件連続送信」を押すと、20リクエストが並列で送信されます。<br>
        ※ SQSコンソールで「利用可能なメッセージ (Messages Available)」に一時的にキューが溜まることを確認できます。
    </div>
</div>

<script>
    async function sendOrders(count) {
        const apiUrl = document.getElementById('apiUrl').value.trim();
        const itemBase = document.getElementById('item').value.trim();
        const priceBase = Number(document.getElementById('price').value);

        const singleBtn = document.getElementById('singleBtn');
        const burstBtn = document.getElementById('burstBtn');
        const responseBox = document.getElementById('responseBox');
        const statusMessage = document.getElementById('statusMessage');
        const jsonResult = document.getElementById('jsonResult');

        if (!apiUrl) {
            alert('API Gateway の URL を入力してください');
            return;
        }

        // ボタンの無効化と画面初期化
        singleBtn.disabled = true;
        burstBtn.disabled = true;
        burstBtn.innerText = `送信中 (0/${count})...`;
        responseBox.className = 'response-box';
        responseBox.style.display = 'block';
        statusMessage.innerHTML = '<strong>送信処理中...</strong>';
        jsonResult.innerText = '';

        let successCount = 0;
        let failCount = 0;
        let logs = [];

        // 1件分のPOSTを行う関数
        const sendSingleRequest = async (index) => {
            try {
                const res = await fetch(apiUrl, {
                    method: 'POST',
                    headers: { 'Content-Type': 'application/json' },
                    body: JSON.stringify({
                        item: `${itemBase}-${index}`,
                        price: priceBase + index
                    })
                });

                const data = await res.json();

                if (res.ok) {
                    successCount++;
                    logs.push(`[#${index}] 成功 (200) - order_id: ${data.order_id}`);
                } else {
                    failCount++;
                    logs.push(`[#${index}] エラー (${res.status}) - ${JSON.stringify(data)}`);
                }
            } catch (err) {
                failCount++;
                logs.push(`[#${index}] 通信失敗 - ${err.message}`);
            }
        };

        // 並列でリクエストを生成
        const requests = [];
        for (let i = 1; i <= count; i++) {
            requests.push(sendSingleRequest(i));
        }

        // 全リクエストの完了を待機
        await Promise.all(requests);

        // 結果の出力
        if (failCount === 0) {
            responseBox.classList.add('success');
            statusMessage.innerHTML = `<strong>✅ 全 ${successCount} 件の送信が完了しました!(SQSへキューイング済)</strong>`;
        } else {
            responseBox.classList.add('error');
            statusMessage.innerHTML = `<strong>⚠️ 処理完了: 成功 ${successCount} 件 / 失敗 ${failCount} 件</strong>`;
        }

        jsonResult.innerText = logs.join('\n');

        // ボタン復元
        singleBtn.disabled = false;
        burstBtn.disabled = false;
        burstBtn.innerText = '🚀 20件連続送信';
    }
</script>

</body>
</html>

SQSバッファ実験

演習: 「処理遅延とSQSの滞留」を体験する
  1. 処理Lambdaに遅延を追加: OJT_OrderRegisterFunction_[name]内の先頭に
    import timeを追加し、書き込み処理の前に time.sleep(2)
    を挿入してデプロイを実行
  2. 20件の連続リクエストを送信: HTMLの「🚀 20件連続送信」をクリック。
  3. 結果の観察:
    • フロント側: 20件のリクエスト自体は数秒で「すべて成功(200 OK)」で完了
    • SQSコンソール:
      「利用可能なメッセージ」の数字が一時的に数件〜20件に留まり、
        数秒ごとに1件ずつメッセージが減っていく挙動を確認
    • DynamoDBコンソール: 数十秒後、すべてのデータが1件も欠けることなく
      COMPLETEDで保存完了したことを確認

つまずきやすいポイントとトラブルシューティング(FAQ)

⚠️ TypeError: the JSON object must be str…

原因: Consumer Lambdaの単体テストでbodyを辞書型(dict)で書いてしまっている。
解決策: テストイベント内のbodyの値をダブルクォーテーションで囲む

⚠️ NameError: name ‘time’ is not defined

原因: 擬似遅延処理で time.sleep()を使ったが import timeが漏れている。
解決策: コード先頭に import timeを追記

⚠️ メッセージが「処理中」に留まり動かなくなる

原因: Consumer Lambdaの実行中に例外が発生し、
Ackされずに再試行ループになっている。
解決策: CloudWatch Logs でエラーログを確認してコードを修正するか、
SQS画面から「メッセージをパージ」して初期化

⚠️ HTMLから送信すると CORS エラーになる

原因: API Gateway や Producer Lambda
のレスポンスヘッダー設定が不足している。
解決策: Producer Lambda の返却ヘッダーに
'Access-Control-Allow-Origin': '*'
が入っているか確認

おわりに / 後片付け(リソース削除)

学習が終了したら、不要な課金を防ぐために、必ず以下の順番でリソースを削除してください

  1. API Gatewayの削除(作成したHTTP APIを削除)
  2. Lambda関数の削除(OJT_OrderReceiverFunction_[name]
    OJT_OrderRegisterFunction_[name] の両方)
  3. SQSキューの削除(OJT_OrderQueue_[name]
  4. DynamoDBテーブルの削除(OJT_Orders_[name]
  5. IAMロールの確認(Lambda関数の削除に伴い
    自動生成されたロールが残っている場合は、IAM > ロール から削除)

以上で、Step 1からStep 4までの一連のハンズオンは完了です

参考サイト

Amazon Simple Queue Service とは – AWS公式ドキュメント

Amazon API Gateway とは – AWS公式ドキュメント

 

 

この記事を書いた人

村守俊一

shunichi_muramori

26卒 ITソリューション部ビジネスソリューションセクション