こんにちは、広野です。
以下の記事と同じ理由で、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 の方がやや融通は利きやすいという印象でした。
本記事が皆様のお役に立てれば幸いです。