Amazon API Gateway → Step Functions→ DynamoDBという構成で、データの新規登録や取得処理を試してみた

Amazon API Gateway → Step Functions→ DynamoDBという構成で、データの新規登録や取得処理を試してみた

2026.01.16

はじめに

Amazon API Gateway、AWS Step Functions、Amazon DynamoDBを組み合わせたサーバーレス構成で、データの新規登録や取得処理を試しました。

通常、API GatewayのバックエンドにはAWS Lambdaを採用することが多いですが、今回はLambdaを使用せず、Step Functionsを直接呼び出す構成としています。この構成にはLambdaとは異なる設定上の注意点がいくつか存在するため、その解決策を含めて解説します。

なお、本記事では設定方法の解説に重点を置いているため、レストラン予約システムを題材としていますが、データベース設計などは検証用に簡略化しています。

DynamoDBテーブル作成

AWS CloudShellを開き、以下のコマンドを実行してDynamoDBテーブルを作成します。

$ aws dynamodb create-table \
    --table-name RestaurantReservations \
    --attribute-definitions \
        AttributeName=PhoneNumber,AttributeType=S \
        AttributeName=ReservationTime,AttributeType=S \
    --key-schema \
        AttributeName=PhoneNumber,KeyType=HASH \
        AttributeName=ReservationTime,KeyType=RANGE \
    --billing-mode PAY_PER_REQUEST

Step Functions の作成

ステートマシンのタイプは Express を選択して作成します。

cm-hirai-screenshot 2026-01-13 9.27.16

API GatewayからStep Functionsを同期的に呼び出し、結果を受け取るためには、StandardタイプではなくExpressタイプが必須となるためです。

同期 Express ワークフローはワークフローを開始して、それが完了するまで待機してから、結果を返します。同期 Express ワークフローはマイクロサービスのオーケストレーションに使用できます。同期 Express ワークフローを使用すると、エラー処理、再試行、並列タスクの実行のための追加のコードを開発しなくても、アプリケーションを開発できます。Amazon API Gateway から呼び出された同期 Express ワークフローを実行するかAWS Lambda、 StartSyncExecution API コールを使用して実行できます。
https://docs.aws.amazon.com/ja_jp/step-functions/latest/dg/choosing-workflow-type.html

ステートマシンのワークフロー定義(ASL)は以下の通りです。

cm-hirai-screenshot 2026-01-13 13.31.56

{
  "Comment": "レストラン予約フロー",
  "QueryLanguage": "JSONata",
  "StartAt": "RouteRequest",
  "States": {
    "RouteRequest": {
      "Type": "Choice",
      "Choices": [
        {
          "Condition": "{% $states.input.action = 'create' %}",
          "Next": "CreateReservation"
        },
        {
          "Condition": "{% $states.input.action = 'check' %}",
          "Next": "CheckReservation"
        }
      ],
      "Default": "InvalidAction"
    },
    "CreateReservation": {
      "Type": "Task",
      "Resource": "arn:aws:states:::aws-sdk:dynamodb:putItem",
      "Arguments": {
        "TableName": "RestaurantReservations",
        "Item": {
          "PhoneNumber": {
            "S": "{% $states.input.phone %}"
          },
          "ReservationTime": {
            "S": "{% $states.input.time %}"
          },
          "CustomerName": {
            "S": "{% $states.input.name %}"
          },
          "PeopleCount": {
            "N": "{% $string($states.input.count) %}"
          },
          "Status": {
            "S": "RESERVED"
          }
        },
        "ConditionExpression": "attribute_not_exists(PhoneNumber)"
      },
      "Output": {
        "status": "success",
        "message": "ご予約を承りました。"
      },
      "Catch": [
        {
          "ErrorEquals": [
            "DynamoDB.ConditionalCheckFailedException"
          ],
          "Next": "AlreadyExists"
        }
      ],
      "End": true
    },
    "CheckReservation": {
      "Type": "Task",
      "Resource": "arn:aws:states:::aws-sdk:dynamodb:query",
      "Arguments": {
        "TableName": "RestaurantReservations",
        "KeyConditionExpression": "PhoneNumber = :p AND ReservationTime >= :now",
        "ExpressionAttributeValues": {
          ":p": {
            "S": "{% $states.input.phone %}"
          },
          ":now": {
            "S": "{% $now() %}"
          }
        }
      },
      "Output": {
        "status": "success",
        "found": "{% $count($states.result.Items) > 0 %}",
        "reservations": "{% $states.result.Items.{ 'name': CustomerName.S, 'time': ReservationTime.S, 'count': PeopleCount.N, 'status': Status.S } %}",
        "message": "{% $count($states.result.Items) > 0 ? 'これからのご予約が見つかりました。' : '現在、これからのご予約は承っておりません。' %}"
      },
      "End": true
    },
    "AlreadyExists": {
      "Type": "Pass",
      "Output": {
        "status": "error",
        "message": "お客様は既に同じ日時でご予約されています。重複予約はできません。"
      },
      "End": true
    },
    "InvalidAction": {
      "Type": "Pass",
      "Output": {
        "status": "error",
        "message": "不正な操作です。"
      },
      "End": true
    }
  }
}

このステートマシンの処理内容は以下の通りです。

  1. RouteRequest: 入力された action パラメータの値(create または check)に基づいて処理を分岐させます。
  2. CreateReservation: 新規予約の場合、dynamodb:putItem を使用してデータを登録します。この際、同じ電話番号のデータが存在しないことを条件としています。
  3. CheckReservation: 予約確認の場合、dynamodb:query を使用して、指定された電話番号かつ現在時刻以降の予約データを検索します。JSONataを使用して、検索結果を整形して返します。

作成時、IAMロールと一部のIAMポリシーは自動作成されますが、一部、DynamoDBへの操作権限が不足しているため、今回は検証用として AmazonDynamoDBFullAccess_v2 を追加します。なお、本記事では検証をスムーズに行うためFullAccessポリシーを使用していますが、本番環境ではセキュリティの観点から、必要なリソースとアクションのみを許可した最小権限のポリシーを設定することを推奨します。

cm-hirai-screenshot 2026-01-13 9.29.02

API Gateway用IAMロールの作成

AWS CloudShellを開き、以下のコマンドを実行して、API GatewayがStep Functionsを実行するために必要なIAMロールを作成します。

# 信頼ポリシー作成
echo '{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Service": "apigateway.amazonaws.com"
      },
      "Action": "sts:AssumeRole"
    }
  ]
}' > trust-policy.json

# ロール作成
aws iam create-role \
    --role-name APIGatewayToStepFunctionsRole \
    --assume-role-policy-document file://trust-policy.json

# 権限付与
aws iam attach-role-policy \
    --role-name APIGatewayToStepFunctionsRole \
    --policy-arn arn:aws:iam::aws:policy/AWSStepFunctionsFullAccess

# ARNの確認 (★これを控える)
aws iam get-role \
    --role-name APIGatewayToStepFunctionsRole \
    --query 'Role.Arn' \
    --output text

API Gatewayの作成

以下の設定でAPI Gatewayを作成します。

cm-hirai-screenshot 2026-01-13 9.44.00

予約確認API (GET) の作成

/reservations リソースを作成し、GETメソッドを追加します。

以下の設定でセットアップを行います。

  • 統合タイプ: AWS サービス
  • AWS リージョン: (利用するリージョン)
  • AWS サービス: Step Functions
  • HTTP メソッド: POST
  • アクション名: StartSyncExecution
  • 実行ロール: 先ほど作成したAPI Gateway用のIAMロールのARN
  • URL クエリ文字列パラメータ: phone (必須)

cm-hirai-screenshot 2026-01-13 9.59.09

注記: API GatewayからStep Functionsを呼び出す場合、統合リクエストのHTTPメソッドは必ず POST を指定する必要があります。

注記
すべての Step Functions API アクションは HTTP POST メソッドを使用します。
https://docs.aws.amazon.com/ja_jp/step-functions/latest/dg/tutorial-api-gateway.html#api-gateway-step-2

ステートマシンの実行には同期実行可能な StartSyncExecution アクションを使用しますが、これはExpressタイプでのみ利用可能です。

Starts a Synchronous Express state machine execution. StartSyncExecution is not available for STANDARD workflows.
https://docs.aws.amazon.com/step-functions/latest/apireference/API_StartSyncExecution.html

作成後、「統合リクエスト」の設定を編集します。

  • リクエスト本文のパススルー: 「テンプレートが定義されていない場合 (推奨)」を選択します。
  • マッピングテンプレート:
    • Content-Type: application/json
    • テンプレート本文: 以下のコードを設定します(YOUR_ACCOUNT_ID とステートマシン名は環境に合わせて書き換えてください)。
#set($phone = $input.params('phone'))
{
  "stateMachineArn": "arn:aws:states:ap-northeast-1:YOUR_ACCOUNT_ID:stateMachine:RestaurantReservationFlow",
  "input": "{\"action\": \"check\", \"phone\": \"$util.escapeJavaScript($phone)\"}"
}

gcrqyudqb9tjkjpwn1ji

URLクエリパラメータから電話番号を取得し、"action": "check" を付与してStep Functionsに渡すことで、予約確認処理を実行させます。

なお、マッピングテンプレートでは、以下のAWSドキュメント「例 3」などにも記載がある通り、入力値に予期せぬ文字が含まれていてもシステムが停止しない(400エラーにならない)ようにするため、またJSON構造を破壊されないために、変数を埋め込む際は $util.escapeJavaScript() を使用しています。

https://docs.aws.amazon.com/ja_jp/apigateway/latest/developerguide/api-gateway-mapping-variable-examples.html#input-example-json-mapping-template

各設定項目について

  • リクエスト本文のパススルー
    「テンプレートが定義されていない場合 (推奨)」を選択します。
    この設定にすることで、クライアントから送信された Content-Type ヘッダー(今回は application/json)に一致するマッピングテンプレートが存在する場合、必ずそのテンプレートを適用して変換を行うようになります。これにより、変換前の生データが誤ってバックエンドに送信されるのを防ぎます。

  • マッピングテンプレート
    API Gateway(HTTPリクエスト)と Step Functions(AWS API)をつなぐための変換定義です。
    Step Functions の同期実行 API (StartSyncExecution) は、リクエストボディとして以下の特定の JSON 構造を要求します。

    {
      "stateMachineArn": "ステートマシンのARN",
      "input": "{\"key\": \"value\"}" // ※inputの中身はエスケープされたJSON文字列である必要がある
    }
    

    このテンプレートでは、URLクエリパラメータとして受け取った phone の値を抽出し、Step Functions が要求する上記のフォーマット(stateMachineArninput 文字列)に変換して渡す処理を記述しています。

新規予約API (POST) の作成

同様に、/reservations リソースにPOSTメソッドを作成します。

  • メソッドタイプ:POST
  • 統合タイプ: AWSのサービス
  • AWS サービス: Step Functions
  • HTTP メソッド: POST
  • アクション名: StartSyncExecution
  • 実行ロール: 先程作成したAPI Gateway用のIAMロールのARN

cm-hirai-screenshot 2026-01-13 10.12.18

作成後、「統合リクエスト」のマッピングテンプレートを以下のように設定します。

  • Content-Type: application/json
  • テンプレート本文: 以下のコードを設定します(YOUR_ACCOUNT_ID とステートマシン名は環境に合わせて書き換えてください)。
{
  "stateMachineArn": "arn:aws:states:ap-northeast-1:YOUR_ACCOUNT_ID:stateMachine:RestaurantReservationFlow",
  "input": "$util.escapeJavaScript($input.json('$').replaceFirst('^\s*\{', '{ \"action\": \"create\", '))"
}

ここでは、クライアントから送信されたJSONデータに "action": "create" という項目を強制的に追加し、Step Functions側で処理を分岐できるように加工しています。

統合レスポンスの設定

Step Functionsの StartSyncExecution APIのレスポンスは、デフォルトでは実行ID(executionArn)などのメタデータを含んでおり、実際の処理結果は output フィールド内に文字列として格納されています。

このままではクライアント側で再度パース処理が必要になるため、実運用ではAPI Gatewayの「統合レスポンス」機能を使って output の中身を展開し、必要なデータのみを返却します。

以下のマッピングテンプレートを設定することで、不要なメタデータを除去し、整形されたJSONのみをレスポンスとして返却できます。

マッピングテンプレート (application/json)

#set($result = $input.path('$'))

#if($result.status == 'SUCCEEDED')
  ## 成功時は output の中身(JSON文字列)をそのまま展開してレスポンス本文にする
  $result.output
#else
  ## 失敗時は500エラーとし、エラー内容を返す
  #set($context.responseOverride.status = 500)
  {
    "error": "$result.error",
    "cause": "$result.cause"
  }
#end

POSTとGETメソッドどちらの統合レスポンスにも設定します。

cm-hirai-screenshot 2026-01-16 9.05.24

デプロイ

設定が完了したらAPIをデプロイします。

cm-hirai-screenshot 2026-01-13 14.10.10

※画面上部に This action is not available while the API is updating. と表示され、「APIのデプロイ」ボタンがクリックできない場合があります。その際はブラウザをリロードすると解消されることがあります。

「API のデプロイ」を選択し、ステージ名(例: dev)を指定してデプロイします。

cm-hirai-screenshot 2026-01-13 14.11.03

動作確認

AWS CloudShellから curl コマンドを実行して動作を確認します。

新規予約 (POST)

curl -X POST "https://{api-id}.execute-api.ap-northeast-1.amazonaws.com/dev/reservations" \
     -H "Content-Type: application/json" \
     -d '{
           "phone": "09011111111",
           "time": "2026-01-14T12:00",
           "name": "適当な名前",
           "count": 2
         }'
{
  "status": "success",
  "message": "ご予約を承りました。"
}

なお統合レスポンスを設定しない場合、以下になります。

{"billingDetails":{"billedDurationInMilliseconds":100,"billedMemoryUsedInMB":64},"executionArn":"arn:aws:states:ap-northeast-1:111111111111:express:RestaurantReservationFlow:4542c1e7-a5ce-46ba-80c4-78ca35d7730b:3168f3c7-436e-4154-8d76-3b39a3f0bc05","input":"{ \"action\": \"create\", \"phone\":\"09011111111\",\"time\":\"2026-01-14T12:00\",\"name\":\"適当な名前\",\"count\":2}","inputDetails":{"__type":"com.amazonaws.swf.base.model#CloudWatchEventsExecutionDataDetails","included":true},"name":"4542c1e7-a5ce-46ba-80c4-78ca35d7730b","output":"{\"status\":\"success\",\"message\":\"ご予約を承りました。\"}","outputDetails":{"__type":"com.amazonaws.swf.base.model#CloudWatchEventsExecutionDataDetails","included":true},"startDate":1.768268689381E9,"stateMachineArn":"arn:aws:states:ap-northeast-1:111111111111:stateMachine:RestaurantReservationFlow","status":"SUCCEEDED","stopDate":1.768268689451E9,"traceHeader":"Root=1-6965a391-ec956495d41a159d021c0498;Sampled=1"}

DynamoDBを確認すると、データが保存されていることがわかります。
cm-hirai-screenshot 2026-01-13 13.42.02

予約確認 (GET)

curl "https://{api-id}.execute-api.ap-northeast-1.amazonaws.com/dev/reservations?phone=09011111111"
{
  "status": "success",
  "found": true,
  "reservations": [
    {
      "name": "佐藤 次郎",
      "time": "2026-01-14T12:00",
      "count": "2",
      "status": "RESERVED"
    }
    ,
    {
      "name": "佐藤 次郎",
      "time": "2026-01-16T12:00",
      "count": "3",
      "status": "RESERVED"
    }
  ],
  "message": "これからのご予約が見つかりました。"
}

最後に

本記事では、Lambdaを使用せずにAPI GatewayとStep Functionsを直接連携させる構成で、データの新規登録や取得処理を試しました。

この構成には、Lambdaを利用する場合とは異なる以下の注意点があります。

  • Step Functionsのタイプ: 同期実行のために「Express」タイプを選択する必要がある
  • API Gatewayの統合設定: Step FunctionsのAPIアクションはすべて「POST」メソッドで呼び出す必要がある

これらのポイントを押さえることで、Lambda関数のコード管理が不要になり、Step Functionsによる処理の流れを視覚的に把握しやすいサーバーレスアーキテクチャを構築できます。

参考

https://docs.aws.amazon.com/ja_jp/step-functions/latest/dg/tutorial-api-gateway.html#api-gateway-step-2

この記事をシェアする

FacebookHatena blogX

関連記事