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

## 概要
dynamodbを使ったことがなかったので、apigatewayを通してデータ作成・取得を行う処理を試してみた。

## cdk

```ts
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を行うと以下の形で返却される。

```json
{
  "Count": 1,
  "Items": [
    {
      "author": {
        "S": "test author"
      },
      "title": {
        "S": "test1"
      }
    }
  ],
  "ScannedCount": 1
}
```

少々読み辛いので、以下の形に`responseTemplates`を使って直している。

```json
[
  {
    "title": "test1",
    "author": "test author"
  }
]
```

## ハマったこと : SerializationException

レスポンスを作成するのに失敗したとき、以下の戻り値が返却された。

```
HTTP/1.1 200 OK
Content-Type: application/json
// 省略

{
  "__type": "com.amazon.coral.service#SerializationException"
}
```

このとき、 [APIGateway](https://docs.aws.amazon.com/ja_jp/apigateway/latest/developerguide/how-to-test-method.html)でテストしたときは、戻り値は期待するものとなっていた。
どうやら、Dynamoに入っている値により発生することがあるらしい。 (`"`がエスケープされていないなど。)
自分は単純にJSONにならない形にしてしまっていた。（ `},]`はjsはOKだがjsonはNG)

正常なJSONを返すようにしたらエラーは消えた。
[stackoverflow](https://stackoverflow.com/questions/52702354/invoking-aws-gateway-api-com-amazon-coral-serviceserializationexception)

また、CloudFrontの後ろに設置していたため、キャッシュが効いてしまい、直ったあともエラーのレスポンスが返り続けていた。
キャッシュの削除を行ったらうまくいったので、今回でキャッシュが効かない設定(defaultTTL)を追加した。



## 参考

[Dynamo Developer Guide](https://docs.aws.amazon.com/ja_jp/amazondynamodb/latest/developerguide/Introduction.html)

[mseeeen - 直接操作](https://mseeeen.msen.jp/method-of-write-directly-to-dynamodb-from-apigateway-part1/)
[zenn - 直接操作](https://zenn.dev/amarelo_n24/articles/ab083b0358e902)
[qiita - 【AWS CDK】API GatewayからLambdaを通さずに直接DynamoDBにアクセスしGetItemするAPIを作ってみた](https://qiita.com/lism/items/ebb20b402644a91d6eb9)
[DEV - AWS CDK - API Gateway Service Integration with DynamoDB](https://dev.to/elthrasher/aws-cdk-api-gateway-service-integration-with-dynamodb-2ek0)

[Query](https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_Query.html)
[クエリの使用](https://docs.aws.amazon.com/ja_jp/amazondynamodb/latest/developerguide/Query.html)
[レガシー条件パラメータ](https://docs.aws.amazon.com/ja_jp/amazondynamodb/latest/developerguide/LegacyConditionalParameters.html)
[AWS Step FunctionsでDynamoDBテーブルからLastEvaluatedKeyによる繰り返し取得をしたアイテムを一つの配列に結合する（AWS CDK v2）](https://dev.classmethod.jp/articles/combine-items-retrieved-repeatedly-by-lastevaluatedkey-from-dynamodb-table-into-one-array-with-aws-step-functions-aws-cdk-v2/)