【AWS CDK】API GatewayからLambdaを通さずに直接DynamoDBにアクセスしGetItemするAPIを作ってみた


DynamoDBから値を取ってきて返すAPIを作成するにあたり、Lambdaを通さずに直接DynamoDBから値を取れることを知ったので試してみました。

## 作るもの
API GatewayからLambdaを通さずにDynamoDBに直接アクセスし、API Gateway + DynamoDBでgetItemするAPIを作成します。
今回は、`GET books/{title}`を叩くと以下のような形でDynamoDBに保存されている値を整形して返却してくれるようなAPIを作成していきます。

```
{
  "title": "how_to_book",
  "price": 1000
}
```

## 構成
シンプルな構成です。
![architecture.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2699394/f6843e74-d9fa-7672-cdd6-c58da1869fb4.png)

## 環境
```
cdk --version
2.10.0 (build e5b301f)

tsc --v
Version 3.9.10
```

## 完成系

```typescript
import { Stack, StackProps } from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as apigateway from "aws-cdk-lib/aws-apigateway";
import * as dynamodb from 'aws-cdk-lib/aws-dynamodb';
import * as iam from 'aws-cdk-lib/aws-iam';

export class TestBookStack extends Stack {
  constructor(scope: Construct, id: string, props: StackProps) {
    super(scope, id, props);

    // DynamoDB
    const table = new dynamodb.Table(this, "dynamoDB", {
      tableName: "Book",
      partitionKey: { name: 'title', type: dynamodb.AttributeType.STRING }
    });

    // API Gateway
    const api = new apigateway.RestApi(this, "bookApi", {
      deployOptions: {
        stageName: "dev",
        metricsEnabled: true,
        dataTraceEnabled: true,
      }
    });

    // api -> DynamoDBのIAMポリシー
    const credentialRole = new iam.Role(this, 'role', {
      assumedBy: new iam.ServicePrincipal("apigateway.amazonaws.com"),
    });
    credentialRole.attachInlinePolicy(
      new iam.Policy(this, 'getItemPolicy', {
        statements: [
          new iam.PolicyStatement({
            actions: [
              "dynamodb:GetItem"
          ],
            effect: iam.Effect.ALLOW,
            resources: [table.tableArn]
          })
        ]
      })
    );

    // apiのリソースの用意
    const allResources = api.root.addResource('books');
    const bookResource = allResources.addResource('{title}');

    // titleを指定してitemを1件取得するapi
    bookResource.addMethod('GET', new apigateway.AwsIntegration({
      service: 'dynamodb',
      action: 'GetItem',
      options: {
        credentialsRole: credentialRole,
        requestTemplates: {
          'application/json': `{
              "Key": {
                "title": {
                  "S": "$input.params('title')"
                }
              },
              "TableName": "Book"
            }`
        },
        integrationResponses: [
          {
            statusCode: '200',
            responseTemplates: {
              'application/json': `{
                "title": "$input.path('$').Item.title.S",
                "price": $input.path('$').Item.price.N,
              }`
            }
          }
        ]
      }
    }),
    {
      methodResponses: [
        {
          statusCode: '200',
        }
      ]
    });
  }
}
```

## DynamoDB
まずはDynamoDBを作成します。
`Book`という名前のテーブルを作成し、`title`という名前のパーティションキーを用意します。

```typescript
const table = new dynamodb.Table(this, "dynamoDB", {
  tableName: "Book",
  partitionKey: { name: 'title', type: dynamodb.AttributeType.STRING },
});
```

## API Gateway
次にAPI Gatewayを作成します。
少し長くなるので、小分けに説明を書いていきます。

### RestApi
REST APIで実装をしていくので、まずはそれを記述します。
```typescript
const api = new apigateway.RestApi(this, "bookApi", {
  deployOptions: {
    stageName: "dev",
    metricsEnabled: true,
    dataTraceEnabled: true
  }
});
```

### addResource
今回は`GET books/{title}`といった形のAPIを作成したいので、それを記述します。
```typescript
const allResources = api.root.addResource('books');
const bookResource = allResources.addResource('{title}');
```

このように書くと、以下のようなかたちでリソースが用意されます。

![resource.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2699394/3adb8e7b-9994-24c7-8898-ae0d3a0765a1.png)

### addMethod
DynamoDBにGetItemをしに行くAPIなので、それを記述します。
統合タイプに`AWSサービス`、サービスに`dynamodb`を指定することで、API Gatewayから直接DynamoDBへアクセスすることができます。
```typescript
bookResource.addMethod('GET', new apigateway.AwsIntegration({
  service: 'dynamodb',
  action: 'GetItem'
}));
```

ちなみに、全件取得したい場合には以下のような書き方になります。
```typescript
allResources.addMethod('GET', new apigateway.AwsIntegration({
  service: 'dynamodb',
  action: 'Scan'
}));
```

### 統合リクエスト
統合リクエストを利用し、バックエンド(DynamoDB)に情報を渡す際にどのような変換を行うのか定義していきます。
今回は、クライアントからのリクエストをDynamoDBのGetItem APIリクエストに変換します。

`$input.params('title')`と書くことで、`GET books/{title}`の`{title}`の部分を取得することができます。
これを利用し、DynamoDBの`Book`というテーブルから、パーティションキーである`title`の値を指定して`GetItem`します。

```typescript
requestTemplates: {
  'application/json': `{
    "Key": {
      "title": {
        "S": "$input.params('title')"
      }
    },
    "TableName": "Book"
  }`
}
```

全件取得ではこのように書くことができます。
```typescript
 requestTemplates: {
  'application/json': `{
    "TableName": "Book"
  }`
}
```

### 統合レスポンス
今回は、DynamoDB からのレスポンスをそのままクライアントに返すのではなく、整形してから返していきます。
そのために統合レスポンスを利用し、DynamoDBから返ってきた情報の変換をしていきます。

`$input.path('$')`にDynamoDBから取ってきた値が入ってくるため、それを利用して整形を行います。

```typescript
integrationResponses: [
  {
    statusCode: '200',
    responseTemplates: {
      'application/json': `{
         "title": "$input.path('$').Item.title.S",
         "price": $input.path('$').Item.price.N,
      }`
    }
  }
]
```

### メソッドレスポンス
今回は正常終了パターンのみ設定しておきます。

```typescript
methodResponses: [
  {
    statusCode: '200',
  }
]
```

### IAMポリシー
このままではAPI GatewayにDynamoDBを触る権限がないため、ロールを作成しアタッチします。

DynamoDBにGetItemする許可をするIAMポリシーを作成し、ロールに紐付けます。
全件取得したい場合には、`actions`に`"dynamodb:Scan"`を設定します。

```typescript
const credentialRole = new iam.Role(this, 'role', {
  assumedBy: new iam.ServicePrincipal("apigateway.amazonaws.com"),
});
credentialRole.attachInlinePolicy(
  new iam.Policy(this, 'getItemPolicy', {
    statements: [
      new iam.PolicyStatement({
        actions: [
          "dynamodb:GetItem"
        ],
        effect: iam.Effect.ALLOW,
        resources: [table.tableArn]
      })
    ]
  })
);
```
上記で用意したロールを以下のようにしてアタッチします。

```typescript
bookResource.addMethod('GET', new apigateway.AwsIntegration({
  service: 'dynamodb',
  action: 'GetItem',
  options: {
      credentialsRole: credentialRole,
  }
}));
```

## 動作確認
まずは下準備として、スタックのデプロイを行い、作成したDynamoDBにデータを入れておきます。
![dynamoDB.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2699394/751ce274-9fa9-cad2-4c72-66b5c5cbdbbc.png)


データの準備ができたら、実際にAPIを叩いてみたいと思います。
今回はコンソール上から実施していきます。
API Gatewayのテスト呼び出しの画面を表示し、先程用意したデータに存在するtitleを{title}の部分に入力してテストボタンを押下します。
![API_Gateway_test.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2699394/42d73ebe-d731-d3ef-c3de-a28fc584d74f.png)

期待した結果が返ってきました！
![result.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2699394/e66ddb48-5cea-77ef-b851-3653612f665d.png)

## まとめ

API GatewayからLambdaを通さずにDynamoDBに直接アクセスし、API Gateway + DynamoDBでgetItemするAPIを作成しました。
複雑な変換などを行いたい場合はLambdaを利用した方が有効な場合もありますが、今回のような単純なものであればAPI Gateway + DynamoDBの構成で充分だと感じました。

## 参考資料
https://qiita.com/ma2shita/items/351746f2b6c414f9d5ab
https://dev.to/elthrasher/aws-cdk-api-gateway-service-integration-with-dynamodb-2ek0