Amazon API GatewayのAWS 統合を使い、AWS Lambdaを通さずにAmazon DynamoDBへアクセスする

Amazon API Gateway の「[AWS 統合(AWS integration)](https://docs.aws.amazon.com/apigateway/latest/developerguide/getting-started-aws-proxy.html)」を使うことで、下の図のBのようにAWS Lambdaを通さずAmazon DynamoDBにアクセスができます。
<img src="https://docs.google.com/drawings/d/e/2PACX-1vTh1By9FHbOy_cMtztSIOIOyZDN-VWhBJMWM9kc0dpr_2tR47Opus_520X5nEkvORqeueKAvMSXLnoo/pub?w=458&amp;h=218">
これにより、例えば DynamoDB の中にあるItemを `GET /items/ID` のように読み出す程度の実装を、素早く行うことが可能です。

### 「Lambda 統合」を利用した場合のPros/Cons

Amazon API Gateway から Amazon DynamoDB 等の AWS サービスへのアクセスで利用されるパターンが、AWS Lambdaを経由する「[Lambda プロキシ統合(Lambda proxy integration)](https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-proxy-integrations.html)」でしょう。構成は先の図のAになり、API Gatewayの設定では「統合タイプ = Lambda関数」を使用します。

クライアントからのリクエストや、呼び出し先AWSサービスからのレスポンスの加工をプログラムできるので便利ですが、管理対象が一つ増えるわけです。

### API Gateway の "マッピングテンプレート"

API Gateway の AWS 統合を設定すると、「統合リクエスト」内の "マッピングテンプレート" で作ったJSONをAWSサービスのAPIに投げ込むことができます。また、AWSサービスからのレスポンスJSONを「統合レスポンス」で受けて、"マッピングテンプレート" で整形し直すことができます。

即ち、この "マッピングテンプレート" でLambdaで書くような変換実装ができるわけです。

## DynamoDB の GetItem を API Gateway で実装する

今回は DynamoDBの `TABLE1` に以下のようなデータが入っているとして、これを `GET /items/{iid}` で値を取得するようにします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/22754/680f05cf-c6c1-2013-666a-72e7c4960e0e.png)
<details><summary>DynamoDB JSON 形式で表示</summary><div>

```json
{
  "iid": {
    "S": "a45f4a4998a"
  },
  "payloads": {
    "M": {
      "batteryLevel": {
        "N": "0.75"
      },
      "clickType": {
        "N": "1"
      },
      "clickTypeName": {
        "S": "SINGLE"
      }
    }
  },
  "timestamp": {
    "N": "1610886990671"
  }
}
```
</div></details>

### 全体の流れ

<img src="https://docs.google.com/drawings/d/e/2PACX-1vT1NzDiXAtzHcNBe-YVNFf0dJsOIlf_p3ukcptmkaj6ICfHlMmjMeW4I0OjlhBhOMRo2tLIcCIoK4TR/pub?w=931&amp;h=636">

### API Gateway リソースの作成

`GET /items/{iid}` を作り、パス名をこの後のマッピングテンプレートで参照できるようにします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/22754/d693a8c3-089c-3f3f-95e2-9a2589c5dc45.png)

### GET メソッドのセットアップ

| 項目 | 設定内容 |
|-----|----------|
| 統合タイプ | AWS サービス |
| AWS リージョン | (DynamoDBのあるリージョン) |
| AWS サービス | DynamoDB |
| AWS サブドメイン | <空> |
| HTTP メソッド | POST |
| アクションの種類 | アクション名の使用 |
| アクション | `GetItem` |
| 実行ロール | (後述で作成したロールのARN) |
| コンテンツの処理 | パススルー |
| デフォルトタイムアウトの使用 | チェックをつける |

ここでのポイントはアクションの GetItem と HTTP メソッドの POST です。これらの情報は API リファレンスから得ることができます。
アクションは API 名になります。HTTP メソッドは当該の API リファレンスを参照することで得られますが、一番手っ取り早いのは Examples を見る事です。例えば DynamoDB の GetItem の [Examples](https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_GetItem.html#API_GetItem_Examples) を見れば HTTP メソッドがPOSTであることが判明します。

#### ロールの作成

後日、改めて詳細。ポイントだけ。

- 信頼されたエンティティのID プロバイダーを `apigateway.amazonaws.com`
- ポリシーでAmazon DynamoDB の GetItem に Allow されていること

### 統合リクエストの設定

統合リクエストでは、クライアントからのリクエストをDynamoDBのGetItem APIリクエストに変換します。

マッピングテンプレートで以下のように設定します。

| 項目 | 設定内容 |
|-----|----------|
| リクエスト本文のパススルー | なし |

マッピングテンプレートの追加で Content-Type に `application/json` を入力、その後表示されるテキストエリアに以下を入力して保存します。

```json:マッピングテンプレート
{
  "TableName": "TABLE1",
  "Key": {
    "iid": {
      "S": "$input.params('iid')"
    }
  }
}
```

これは先ほど設定した「アクション(= API)」のリクエストボディです。
そのため、例えば [Scan](https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_Scan.html)なら、以下のような形になります。

```json:Scanの場合
{
  "TableName": "TABLE1",
  "Limit": 10
}
```

#### マッピングテンプレートの開発TIPS

マッピングテンプレートには、テンプレートエンジンである[Apache Velocity](https://velocity.apache.org/engine/devel/user-guide.html)が利用されています。それに加えて、[API Gatewayで独自に設定されている変数](https://docs.aws.amazon.com/ja_jp/apigateway/latest/developerguide/api-gateway-mapping-template-reference.html)を組み合わせてJSONを構築していきます。

クライアントからのリクエストの中身を見る方法は「テスト」を利用します。以下のように入力をしてテストをすると "Method request" に値が表示されます。これをマッピングテンプレートで読み出します。
読み出し方は `$input.params('NAME')` です。パスとQueryStringとで名前が競合した場合はパス側が優先されます。詳しくは [$input変数](https://docs.aws.amazon.com/ja_jp/apigateway/latest/developerguide/api-gateway-mapping-template-reference.html#input-variable-reference)を参照ください。
<img src="https://docs.google.com/drawings/d/e/2PACX-1vRpYtWN_Wtdc3iYZ2RYslHSjP-mbdFBrBcrqRx50zWiYBngn7uqq4YgR04dryk1V0ICx1Hk9TQ4rL1V/pub?w=780&amp;h=444">

DynamoDB 内に Item が存在していれば、テスト結果は以下のようになります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/22754/af5b2d99-2e0f-249a-91a9-a01668429be1.png)

ここまでくれば、リクエスト側は完了です。

### 統合レスポンスの設定

統合レスポンスでは、DynamoDBからのレスポンスをクライアントに返すための整形を行います。

統合レスポンスを開くとすでに "メソッドレスポンスのステータス" = 200 の行があるので、その行を開いた後にマッピングテンプレートを開きます。その中の application/json をクリックすると表示されるテキストエリアに以下を入力して保存します。

```json:マッピングテンプレート
{
  "iid": "$input.path('$').Item.iid.S",
  "timestamp": $input.path('$').Item.timestamp.N
  "payloads": {
    "clickType": $input.path('$').Item.payloads.M.clickType.N,
    "clickTypeName": "$input.path('$').Item.payloads.M.clickTypeName.S",
    "batteryLevel": $input.path('$').Item.payloads.M.batteryLevel.N,
  }
}
```

この状態でテストすると、レスポンス本文が以下のように変化します。今までは何も入力しない = DynamoDB からのレスポンスをパススルーしていたのが、マッピングテンプレートによって整形されたわけです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/22754/2407f55d-332d-625e-16a4-55720b2b9fe7.png)

#### 統合レスポンスにおける `$input`

統合レスポンスでの `$input` は、DynamoDB からのレスポンスです。`$input.path('$')` とすれば、それ以降はJSONのパスを直接指定できます。

何度も書くのは面倒なのでApache Velocity(VTL; Velocity Template Language)の [set](https://velocity.apache.org/engine/devel/user-guide.html#set) を使って、変数に入れることができます。

```json:setで簡略化
#set($inputRoot = $input.path('$'))
{
  "iid": "$inputRoot.Item.iid.S",
  "timestamp": $inputRoot.Item.timestamp.N
  "payloads": {
    "clickType": $inputRoot.Item.payloads.M.clickType.N,
    "clickTypeName": "$inputRoot.Item.payloads.M.clickTypeName.S",
    "batteryLevel": $inputRoot.Item.payloads.M.batteryLevel.N,
  }
}
```

繰り返しには foreach を使う方法もあります。[VLT](https://velocity.apache.org/engine/devel/user-guide.html#set)と[API Gatewayの変数リファレンス](https://docs.aws.amazon.com/ja_jp/apigateway/latest/developerguide/api-gateway-mapping-template-reference.html)を見ながら、[サンプル](https://docs.aws.amazon.com/ja_jp/apigateway/latest/developerguide/example-photos.html#example-photos-output-mapping)を見ることで理解が深まるかと思います。

### VLTの if で 404 対応をしてみる

このマッピングテンプレートだと Item が存在しなかった場合、以下のようにキーだけが存在するJSONとなってしまうため、返す内容を変更します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/22754/9d41ed04-2c04-1f41-5cf6-eab503a06f3e.png)

```json:404対応マッピングテンプレート
#if($input.path('$') == "{}")
{}
#set($context.responseOverride.status = 404)
#else
{
  "iid": "$input.path('$').Item.iid.S",
  "timestamp": $input.path('$').Item.timestamp.N
  "payloads": {
    "clickType": $input.path('$').Item.payloads.M.clickType.N,
    "clickTypeName": "$input.path('$').Item.payloads.M.clickTypeName.S",
    "batteryLevel": $input.path('$').Item.payloads.M.batteryLevel.N,
  }
}
#end
```

Itemが存在しなかった場合、DynamoDBからは `{}` という文字が送られてきます。これを if で判定し、`{}` であれば、改めて `{}` という文字を返し、同時に HTTP レスポンスコードも 404 に変更しています。

テストしてみるとレスポンス本文は `{}` で、HTTP レスポンスコード(ステータス)も404となりました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/22754/61d0404c-4cff-6e93-41bf-368f5a3f9837.png)

## curl からの実行例

デプロイ後にcurlからは以下のように実行できるようになります。

```console
$ curl -v https://XXXXX.execute-api.REGION.amazonaws.com/prod/items/a45f4a4998a
< HTTP/2 200
{
  "iid": "a45f4a4998a",
  "timestamp": 1610886990671
  "payloads": {
    "clickType": 1,
    "clickTypeName": "SINGLE",
    "batteryLevel": 0.75,
  }
}

$ curl -v https://XXXXX.execute-api.REGION.amazonaws.com/prod/items/bar
< HTTP/2 404
{}
```

## まとめと、この後

クライアントからのリクエスト、そして、AWSサービスからのレスポンスが単純であればLambdaを使わずともAPI化が可能です。DynamoDBを例として挙げましたが、基本的にはどのサービスでも対応可能です。

一方で、例えば処理中に他の情報を参照したり、複雑な変換をする場合はLambda統合が有利にもなるため、両方を試しておいて目的に応じて使い分けましょう。Lambda統合への切り替えも容易ですので、API化の第一歩として知っておいて損は無いのではないでしょうか。

### アクセス制御を忘れずに

今後ですが、このままですとAPIエンドポイントが無防備です。[API Gateway での REST API へのアクセスの制御と管理](https://docs.aws.amazon.com/ja_jp/apigateway/latest/developerguide/apigateway-control-access-to-api.html)に沿って、いずれかのアクセス制御を行いましょう。

## 参考資料

- Developers.IO / [Amazon API Gateway で AWS Service Proxy を使って DynamoDB にアクセスする](https://dev.classmethod.jp/articles/api-gateway-aws-service-proxy-dynamodb/#toc-11)
- Amazon API Gateway 開発者ガイド / [API Gateway マッピングテンプレートとアクセスのログ記録の変数リファレンス](https://docs.aws.amazon.com/ja_jp/apigateway/latest/developerguide/api-gateway-mapping-template-reference.html#input-variable-reference)
- Apache Velocity / [Velocity User Guide (VLT)](https://velocity.apache.org/engine/devel/user-guide.html)

EoT