Amazon API Gateway から Amazon DynamoDB API を直接呼び出す (AWS Lambda 不使用)

こんにちは、広野です。

以下の記事と同じ理由で、AWS Summit Japan 2024 のセッションを聞いてインスピーレーションを受けまして、Amazon API Gateway と Amazon DynamoDB を AWS Lambda 関数抜きで直接連携しました。似たような記事は世の中に多いのですが、本記事では AWS CloudFormation でデプロイしています。

やりたいこと

以下の Amazon DynamoDB テーブルから、GetItem API を使用して指定のデータを 1件取得します。パーティションキーは pkey、ソートキーは skey です。属性として attr があります。

  • Amazon API Gateway REST API を使用します。AWS Lambda 関数は使用しません。
  • AWS Lambda 関数で行っていた Amazon DynamoDB API の呼び出しは、Amazon API Gateway の統合リクエストマッピングテンプレートで代用します。Amazon API Gateway の統合タイプは、AWS になります。
  • Amazon API Gateway の認証はありません。CORS 設定はあります。本記事では簡略化のため “*” を使用して設定します。
  • テストでは、”pkey”: “test1”, “skey”: 1 のパラメータを指定してデータ取得してみます。

マッピングテンプレートをどう書くか

AWS Lambda 関数でやっていたことを Amazon API Gateway のマッピングテンプレートに肩代わりさせるので、そこが肝になります。イメージしてもらいやすくするため、AWS Lambda 関数と対比して紹介します。

AWS Lambda 関数

書き方はいくつかありますが、以下のような AWS Lambda 関数 (Python) で Amazon DynamoDB GetItem API を呼び出すと思います。

import json
import boto3
dynamodb = boto3.resource('dynamodb')
table = dynamodb.Table('extable-xxxxxxxxxx-hirono')

def lambda_handler(event, context):
  input = json.loads(event['body'])
  res = table.get_item(
    Key={
      'pkey': input['pkey'],
      'skey': input['skey']
    }
  )
  return {
    "isBase64Encoded": False,
    "statusCode": 200,
    "body": json.dumps(res)
  }

マッピングテンプレート

マッピングテンプレートだと、以下のように VTL を使用して書きます。統合リクエストマッピングテンプレートに定義します。

## API Gateway が受け取った引数を $inputRoot に格納する
#set($inputRoot = $input.path('$'))
{
  "TableName": "extable-xxxxxxxxxx-hirono",
  "Key": {
    "pkey":{
      "S": "$inputRoot.pkey"
    },
    "skey":{
      "N": "$inputRoot.skey"
    }
  },
  "ConsistentRead" : false
}

Amazon DynamoDB GetItem API に渡すフォーマットは同じだと思いました。

冒頭で、Amazon API Gateway が受け取ったパラメータを $inputRoot という変数に格納しています。面倒なのは項目ごとに “N” や “S” などデータ型を明示的に書かないといけないことです。

Amazon DynamoDB テーブルに渡すパラメータの仕様は以下の公式リファレンス通りだったので、Amazon DynamoDB の他の API でも同様に応用できるものと想像します。

実際の動作

実際に API へのリクエストを AWS CloudShell からコマンドを打って試してみます。以下のコマンドを打ちます。

curl -X POST 'https://xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/prod/Dynamodb' -H 'Content-Type: application/json' -d '{"pkey": "test1", "skey": "1"}'

以下の結果が返ってきます。

{"Item":{"attr":{"S":"attr1"},"pkey":{"S":"test1"},"skey":{"N":"1"}}}

↓整形すると

{
  "Item": {
    "attr": {
      "S":"attr1"
    },
    "pkey": {
      "S":"test1"
    },
    "skey": {
      "N":"1"
    }
  }
}

今回、Amazon API Gateway REST API の統合レスポンスマッピングテンプレートをパススルーにしたので、Amazon DynamoDB から返ってきたデータがそのまま表示されました。

レスポンスから余計な “N” や “S” を消そうと思ったら、統合レスポンスマッピングテンプレートで戻りを加工する必要がありますが、属性が固定ではない場合は難しいかもしれません。

本記事の例ではソートキーのデータ型が Number になっています。このケースでは、API に数値を文字列として渡さないとエラーになります。Amazon DynamoDB API の仕様です。”skey”:{“N”:”1″} となっている部分のことです。また、戻ってくる値も数値は文字列として返されます。

他にも Amazon API Gateway の設定はあるのですが、メインはマッピングテンプレートでしたので、AWS CloudFormation テンプレートから読み取るか、それを実行してデプロイされた現物をご確認頂けたらと思います。

AWS CloudFormation テンプレート

本記事で紹介した Amazon DynamoDB テーブルと Amazon API Gateway REST API を一式デプロイする AWS CloudFormation を貼り付けます。執筆時点で動いたことは確認済みです。

AWSTemplateFormatVersion: 2010-09-09
Description: The CloudFormation template that creates a DynamoDB table and an API Gateway.

# ------------------------------------------------------------#
# Input Parameters
# ------------------------------------------------------------#
Parameters:
  SubName:
    Type: String
    Description: System sub name. (e.g. example)
    Default: example
    MaxLength: 10
    MinLength: 1

Resources:
# ------------------------------------------------------------#
# DynamoDB
# ------------------------------------------------------------#
  DynamodbExample:
    Type: AWS::DynamoDB::Table
    Properties:
      TableName: !Sub extable-${AWS::AccountId}-${SubName}
      AttributeDefinitions:
        - AttributeName: pkey
          AttributeType: S
        - AttributeName: skey
          AttributeType: N
      BillingMode: PAY_PER_REQUEST
      KeySchema:
        - AttributeName: pkey
          KeyType: HASH
        - AttributeName: skey
          KeyType: RANGE
      PointInTimeRecoverySpecification:
        PointInTimeRecoveryEnabled: false
      Tags:
        - Key: Cost
          Value: !Ref SubName

# ------------------------------------------------------------#
# API Gateway
# ------------------------------------------------------------#
  RestApiDynamodb:
    Type: AWS::ApiGateway::RestApi
    Properties:
      Name: !Sub dynamodb-${SubName}
      Description: !Sub REST API to call DynamoDB GetItem API for ${SubName}
      EndpointConfiguration:
        Types:
          - REGIONAL
      Tags:
        - Key: Cost
          Value: !Ref SubName

  RestApiDeploymentDynamodb:
    Type: AWS::ApiGateway::Deployment
    Properties:
      RestApiId: !Ref RestApiDynamodb
    DependsOn:
      - RestApiMethodDynamodbPost
      - RestApiMethodDynamodbOptions

  RestApiStageDynamodb:
    Type: AWS::ApiGateway::Stage
    Properties:
      StageName: prod
      Description: production stage
      RestApiId: !Ref RestApiDynamodb
      DeploymentId: !Ref RestApiDeploymentDynamodb
      MethodSettings:
        - ResourcePath: "/*"
          HttpMethod: "*"
          LoggingLevel: INFO
          DataTraceEnabled : true
      TracingEnabled: false
      Tags:
        - Key: Cost
          Value: !Ref SubName

  RestApiResourceDynamodb:
    Type: AWS::ApiGateway::Resource
    Properties:
      RestApiId: !Ref RestApiDynamodb
      ParentId: !GetAtt RestApiDynamodb.RootResourceId
      PathPart: Dynamodb

  RestApiMethodDynamodbPost:
    Type: AWS::ApiGateway::Method
    Properties:
      RestApiId: !Ref RestApiDynamodb
      ResourceId: !Ref RestApiResourceDynamodb
      HttpMethod: POST
      AuthorizationType: NONE
      Integration:
        Type: AWS
        IntegrationHttpMethod: POST
        Credentials: !GetAtt ApigDynamodbInvocationRole.Arn
        Uri: !Sub "arn:aws:apigateway:${AWS::Region}:dynamodb:action/GetItem"
        PassthroughBehavior: WHEN_NO_TEMPLATES
        RequestTemplates:
          application/json: !Sub |
            #set($inputRoot = $input.path('$'))
            {
              "TableName": "${DynamodbExample}",
              "Key": {
                "pkey":{
                  "S": "$inputRoot.pkey"
                },
                "skey":{
                  "N": "$inputRoot.skey"
                }
              },
              "ConsistentRead" : false
            }
        IntegrationResponses:
          - StatusCode: 200
            ResponseParameters:
              method.response.header.Access-Control-Allow-Headers: "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token,Cache-Control'"
              method.response.header.Access-Control-Allow-Methods: "'POST,OPTIONS'"
              method.response.header.Access-Control-Allow-Origin: "'*'"
          - StatusCode: 400
            ResponseParameters:
              method.response.header.Access-Control-Allow-Headers: "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token,Cache-Control'"
              method.response.header.Access-Control-Allow-Methods: "'POST,OPTIONS'"
              method.response.header.Access-Control-Allow-Origin: "'*'"
          - StatusCode: 403
            ResponseParameters:
              method.response.header.Access-Control-Allow-Headers: "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token,Cache-Control'"
              method.response.header.Access-Control-Allow-Methods: "'POST,OPTIONS'"
              method.response.header.Access-Control-Allow-Origin: "'*'"
          - StatusCode: 404
            ResponseParameters:
              method.response.header.Access-Control-Allow-Headers: "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token,Cache-Control'"
              method.response.header.Access-Control-Allow-Methods: "'POST,OPTIONS'"
              method.response.header.Access-Control-Allow-Origin: "'*'"
          - StatusCode: 500
            ResponseParameters:
              method.response.header.Access-Control-Allow-Headers: "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token,Cache-Control'"
              method.response.header.Access-Control-Allow-Methods: "'POST,OPTIONS'"
              method.response.header.Access-Control-Allow-Origin: "'*'"
          - StatusCode: 503
            ResponseParameters:
              method.response.header.Access-Control-Allow-Headers: "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token,Cache-Control'"
              method.response.header.Access-Control-Allow-Methods: "'POST,OPTIONS'"
              method.response.header.Access-Control-Allow-Origin: "'*'"
      MethodResponses:
        - StatusCode: 200
          ResponseModels:
            application/json: Empty
          ResponseParameters:
            method.response.header.Access-Control-Allow-Origin: true
            method.response.header.Access-Control-Allow-Headers: true
            method.response.header.Access-Control-Allow-Methods: true
        - StatusCode: 400
          ResponseModels:
            application/json: Empty
          ResponseParameters:
            method.response.header.Access-Control-Allow-Origin: true
            method.response.header.Access-Control-Allow-Headers: true
            method.response.header.Access-Control-Allow-Methods: true
        - StatusCode: 403
          ResponseModels:
            application/json: Empty
          ResponseParameters:
            method.response.header.Access-Control-Allow-Origin: true
            method.response.header.Access-Control-Allow-Headers: true
            method.response.header.Access-Control-Allow-Methods: true
        - StatusCode: 404
          ResponseModels:
            application/json: Empty
          ResponseParameters:
            method.response.header.Access-Control-Allow-Origin: true
            method.response.header.Access-Control-Allow-Headers: true
            method.response.header.Access-Control-Allow-Methods: true
        - StatusCode: 500
          ResponseModels:
            application/json: Empty
          ResponseParameters:
            method.response.header.Access-Control-Allow-Origin: true
            method.response.header.Access-Control-Allow-Headers: true
            method.response.header.Access-Control-Allow-Methods: true
        - StatusCode: 503
          ResponseModels:
            application/json: Empty
          ResponseParameters:
            method.response.header.Access-Control-Allow-Origin: true
            method.response.header.Access-Control-Allow-Headers: true
            method.response.header.Access-Control-Allow-Methods: true
    DependsOn:
      - ApigDynamodbInvocationRole

  RestApiRequestModelDynamodb:
    Type: AWS::ApiGateway::Model
    Properties:
      ContentType: application/json
      RestApiId: !Ref RestApiDynamodb
      Schema: |-
        {
          "$schema": "http://json-schema.org/draft-04/schema#",
          "title": "Dynamodb",
          "type": "object",
          "properties": {
            "pkey": {
              "type": "string"
            },
            "skey": {
              "type": "integer"
            }
          },
          "required": ["pkey", "skey"]
        }

  RestApiMethodDynamodbOptions:
    Type: AWS::ApiGateway::Method
    Properties:
      RestApiId: !Ref RestApiDynamodb
      ResourceId: !Ref RestApiResourceDynamodb
      HttpMethod: OPTIONS
      AuthorizationType: NONE
      Integration:
        Type: MOCK
        Credentials: !GetAtt ApigDynamodbInvocationRole.Arn
        IntegrationResponses:
          - ResponseParameters:
              method.response.header.Access-Control-Allow-Headers: "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token,Cache-Control'"
              method.response.header.Access-Control-Allow-Methods: "'POST,OPTIONS'"
              method.response.header.Access-Control-Allow-Origin: "'*'"
            ResponseTemplates:
              application/json: ''
            StatusCode: 200
        PassthroughBehavior: WHEN_NO_MATCH
        RequestTemplates:
          application/json: '{"statusCode": 200}'
      MethodResponses:
        - ResponseModels:
            application/json: Empty
          ResponseParameters:
            method.response.header.Access-Control-Allow-Headers: true
            method.response.header.Access-Control-Allow-Methods: true
            method.response.header.Access-Control-Allow-Origin: true
          StatusCode: 200

# ------------------------------------------------------------#
# API Gateway DynamoDB Invocation Role (IAM)
# ------------------------------------------------------------#
  ApigDynamodbInvocationRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Sub ApigDynamodbInvocationRole-${SubName}
      Description: This role allows API Gateways to invoke DynamoDB GetItem API.
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service:
              - apigateway.amazonaws.com
            Action:
              - sts:AssumeRole
      Path: /
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/AWSXRayDaemonWriteAccess
      Policies:
        - PolicyName: !Sub ApigDynamodbInvocationPolicy-${SubName}
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              - Effect: Allow
                Action:
                  - "dynamodb:GetItem"
                Resource:
                  - !GetAtt DynamodbExample.Arn
    DependsOn:
      - DynamodbExample

# ------------------------------------------------------------#
# Output Parameters
# ------------------------------------------------------------#
Outputs:
  APIGatewayEndpointDynamodb:
    Value: !Sub https://${RestApiDynamodb}.execute-api.${AWS::Region}.${AWS::URLSuffix}/${RestApiStageDynamodb}/Dynamodb

本テンプレートは、Amazon API Gateway のログを Amazon CloudWatch Logs に push するために必要な IAM ロールがアカウントに登録済みでないとエラーになります。それについては以下の記事をご確認ください。

まとめ

いかがでしたでしょうか?

Amazon DynamoDB の API をそのまま使うだけの要件であれば、十分実用的であると思いました。VTL では対応しきれない文字列加工やビジネスロジックが必要になる場合は、やはり AWS Lambda 関数を間に挟む必要があります。

AWS AppSync のマッピングテンプレートでは便利な独自関数が用意されているのですが、Amazon API Gateway では標準 VTL が用意しているものしか使えないようなので (公式ドキュメントを見た限りでは) 、AWS AppSync の方がやや融通は利きやすいという印象でした。

本記事が皆様のお役に立てれば幸いです。

著者について
広野 祐司

AWS サーバーレスアーキテクチャを駆使して社内クラウド人材育成アプリとコンテンツづくりに勤しんでいます。React で SPA を書き始めたら快適すぎて、他の言語には戻れなくなりました。サーバーレス & React 仲間を増やしたいです。AWSは好きですが、それよりもフロントエンド開発の方が好きでして、バックエンド構築を簡単にしてくれたAWSには感謝の気持ちの方が強いです。
取得資格:AWS 認定は13資格、ITサービスマネージャ、ITIL v3 Expert 等
2020 - 2024 Japan AWS Top Engineer 受賞
2022 - 2024 AWS Ambassador 受賞
2023 当社初代フルスタックエンジニア認定
好きなAWSサービス:AWS Amplify / AWS AppSync / Amazon Cognito / AWS Step Functions / AWS CloudFormation

広野 祐司をフォローする
クラウドに強いによるエンジニアブログです。
SCSKは専門性と豊富な実績を活かしたクラウドサービス USiZE(ユーサイズ)を提供しています。
USiZEサービスサイトでは、お客様のDX推進をワンストップで支援するサービスの詳細や導入事例を紹介しています。
AWSアプリケーション開発クラウドソリューション
シェアする
タイトルとURLをコピーしました