概要
dynamodbを使ったことがなかったので、apigatewayを通してデータ作成・取得を行う処理を試してみた。
cdk
import * as core from 'aws-cdk-lib'
import * as apigateway from 'aws-cdk-lib/aws-apigateway'
import * as iam from 'aws-cdk-lib/aws-iam'
import * as s3 from 'aws-cdk-lib/aws-s3'
import { Construct } from 'constructs'
import * as dynamodb from 'aws-cdk-lib/aws-dynamodb'
interface Props extends core.StackProps {
projectId: string
}
export class AWSCartaGraphRESTAPIStack extends core.Stack {
constructor(scope: Construct, id: string, props: Props) {
super(scope, id, props)
// APIGateway作成
const apiName = `${props.projectId}-rest-api`
const restApi = this.createRestAPIGateway(apiName)
const apiRoot = restApi.root.addResource('api')
this.createUsagePlan(restApi, apiName)
// Dynamoの直接操作
this.createDynamoRest(props, apiRoot)
}
private createDynamoRest(props: Props, apiRoot: apigateway.Resource) {
const dynamoTableProp = {
partitionKeyName: 'title',
tableName: 'Book',
}
const dynamo = this.createDynamoTable(props, dynamoTableProp)
const dynamoRole = this.createDynamoCredentionalRole(dynamo)
this.createRest(apiRoot, dynamoRole, dynamoTableProp)
}
// Dynamo作成
private createDynamoTable(
props: Props,
{ partitionKeyName, tableName }: Record<string, string>,
) {
const table = new dynamodb.Table(
this,
`${props.projectId}-books`,
{
partitionKey: {
name: partitionKeyName,
type: dynamodb.AttributeType.STRING,
},
tableName: tableName,
billingMode: dynamodb.BillingMode.PROVISIONED,
removalPolicy: core.RemovalPolicy.DESTROY,
},
)
return table
}
private createRest(
apiRoot: apigateway.Resource,
role: iam.Role,
{ partitionKeyName, tableName }: Record<string, string>,
) {
const booksResource = apiRoot.addResource('books')
const putIntegration = new apigateway.AwsIntegration({
service: 'dynamodb',
action: 'PutItem',
options: {
credentialsRole: role,
requestTemplates: {
'application/json': `{
"TableName": "${tableName}",
"Item": {
"${partitionKeyName}": {
"S": $input.json('$.${partitionKeyName}')
},
"author": {
"S": $input.json('$.author')
}
}
}`,
},
integrationResponses: [
{
statusCode: '200',
responseTemplates: {
'application/json': `{
"requestId": "$context.requestId"
}`,
},
},
],
},
})
booksResource.addMethod('PUT', putIntegration, [{ statusCode: '200' },{ statusCode: '400' },{ statusCode: '500' }])
const errorResponses = [
{
selectionPattern: '400',
statusCode: '400',
responseTemplates: {
'application/json': `{
"error": "Bad input!"
}`,
},
},
{
selectionPattern: '5\\d{2}',
statusCode: '500',
responseTemplates: {
'application/json': `{
"error": "Internal Service Error!"
}`,
},
},
]
const getItnegration = new apigateway.AwsIntegration({
service: 'dynamodb',
action: 'Scan',
options: {
credentialsRole: role,
requestTemplates: {
'application/json': `{
"TableName": "${tableName}"
}`,
},
integrationResponses: [
{
statusCode: '200',
responseTemplates: {
'application/json': `#set($inputRoot = $input.path("$"))
[
#foreach($elem in $inputRoot.Items) {
"${partitionKeyName}": "$elem.${partitionKeyName}.S",
"author": "$elem.author.S"
}#if($foreach.hasNext),#end
#end
]
`,
},
},
...errorResponses,
],
},
})
booksResource.addMethod('GET', getItnegration, {
methodResponses: [{ statusCode: '200' }],
})
}
private createDynamoCredentionalRole(table: dynamodb.Table) {
// api -> DynamoDBのIAMポリシー
const credentialRole = new iam.Role(this, 'role', {
assumedBy: new iam.ServicePrincipal('apigateway.amazonaws.com'),
})
table.grantReadWriteData(credentialRole)
return credentialRole
}
private createRestAPIGateway(restApiName: string) {
const restApi = new apigateway.RestApi(this, restApiName, {
description: 'DynamoRESTAPI',
restApiName,
endpointTypes: [apigateway.EndpointType.REGIONAL],
deployOptions: {
stageName: 'v1',
},
})
return restApi
}
private createUsagePlan(restApi: apigateway.RestApi, apiName: string) {
// apiKeyを設定
const apiKey = restApi.addApiKey('defaultKeys')
const usagePlan = restApi.addUsagePlan(`${apiName}-usage-plan`, {
quota: { limit: 30, period: apigateway.Period.DAY },
throttle: { burstLimit: 2, rateLimit: 1 },
})
usagePlan.addApiKey(apiKey)
usagePlan.addApiStage({ stage: restApi.deploymentStage })
// ------------------------------------------------------------
// APIキーのIDを出力
new core.CfnOutput(this, 'APIKey', {
value: apiKey.keyId,
})
}
}
レスポンスのテンプレートについて
DynamoのScanを行うと以下の形で返却される。
{
"Count": 1,
"Items": [
{
"author": {
"S": "test author"
},
"title": {
"S": "test1"
}
}
],
"ScannedCount": 1
}
少々読み辛いので、以下の形にresponseTemplates
を使って直している。
[
{
"title": "test1",
"author": "test author"
}
]
ハマったこと : SerializationException
レスポンスを作成するのに失敗したとき、以下の戻り値が返却された。
HTTP/1.1 200 OK
Content-Type: application/json
// 省略
{
"__type": "com.amazon.coral.service#SerializationException"
}
このとき、 APIGatewayでテストしたときは、戻り値は期待するものとなっていた。
どうやら、Dynamoに入っている値により発生することがあるらしい。 ("
がエスケープされていないなど。)
自分は単純にJSONにならない形にしてしまっていた。( },]
はjsはOKだがjsonはNG)
正常なJSONを返すようにしたらエラーは消えた。
stackoverflow
また、CloudFrontの後ろに設置していたため、キャッシュが効いてしまい、直ったあともエラーのレスポンスが返り続けていた。
キャッシュの削除を行ったらうまくいったので、今回でキャッシュが効かない設定(defaultTTL)を追加した。
参考
mseeeen - 直接操作
zenn - 直接操作
qiita - 【AWS CDK】API GatewayからLambdaを通さずに直接DynamoDBにアクセスしGetItemするAPIを作ってみた
DEV - AWS CDK - API Gateway Service Integration with DynamoDB
Query
クエリの使用
レガシー条件パラメータ
AWS Step FunctionsでDynamoDBテーブルからLastEvaluatedKeyによる繰り返し取得をしたアイテムを一つの配列に結合する(AWS CDK v2)