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