0
2

More than 1 year has passed since last update.

API GatewayからLambdaを通さずに直接DynamoDBにデータを入れる処理をCDKで書いたメモ

Posted at

概要

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)を追加した。

参考

Dynamo Developer Guide

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)

0
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
2