API Gateway・Lambda・DynamoDBで簡単にREST APIを作る

大学時代の友人３人とWEBサービス開発記録シリーズ第4弾。

今回作ろうとしているWEBサービスの構成（[こちらの記事](https://qiita.com/charatori/items/d4129cbdc8de143dcf17)で紹介しています）における
バックエンド側（API Gateway＋Lambda＋DynamoDB）のAPI構築方法についてまとめたいと思います。

## 前提
* API Gateway
    * エンドポイントタイプ：リージョン
    * APIキー：必須
* Lambda
    * ランタイム：Python 3.8

## この記事のスコープ
### 構成図
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/241764/dc3a0b3b-3740-dace-f64e-48b988392793.png)

本記事では、下記2点の説明をしたいと思います。

1. 上記構成図のAPI Gateway、Lambda、DynamoDB部分について現状の設定内容
2. APIに新たなメソッドを追加する場合の変更手順

## 現状の設定内容
最初に、データの最終的な終着点であるDynamoDBの状況から解説します。
次に、Flaskアプリからのリクエストを受け取るAPI Gatewayの設定を紹介し、
最後に、DynamoDBとAPI Gatewayの間をつなぐLambdaの内容を見ていきます。

### DynamoDB
DynamoDBの設定状況を確認するために、AWSマネジメントコンソールからDynamoDBを開きます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/241764/637a2bd5-d573-05f7-6f55-6f66c532a42a.png)

DynamoDBの管理コンソールを開いたら、左メニューから「テーブル」を選択してテーブル一覧を表示します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/241764/3b41e673-300d-bc5a-f119-37dd11601659.png)

参照したいテーブル（今回は`ripple_prj`）を選択すると、テーブルの概要や登録されている項目などの詳細情報が表示されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/241764/ae63222f-ea37-8a96-3eb7-69b6b22f79c9.png)

今回使うテーブル`ripple_prj`の名前は、`<サイト名>_<リソース名>`のルールで命名しています。
つまり、`ripple_prj`は、rippleサイトのprjリソースを管理するためのテーブルです。

プライマリパーティションキーは、prj_idとしています。

今回制作しているrippleサイトでは、prj情報を全件画面に返してしまい、画面上の表示制御で検索・絞り込みできるようにするため、ソートキーは設定していません。
もしDynamoDBからデータを取得する時点で検索条件を設定したい場合は、条件設定したい項目をソートキーに指定する必要があります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/241764/20a534a2-3cd7-c586-2b96-ec67c6ebc8a5.png)

項目タブを開くと、`ripple_prj`テーブルに登録されているデータを参照することができます。
各行のプライマリパーティションキー（`prj_id`列で青色表示されている`prj0001`や`prj0002`部分）をクリックすると、各レコードの詳細を確認・編集可能です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/241764/77b4b206-c18c-20f4-8e59-a680d338e8ac.png)

ここまでで、DynamoDB上のテーブルと、テーブル内に格納されているデータの内容について確認できました。

実際にFlaskアプリと連携する際には、このDynamoDBのデータをどのように取得し、どのように登録・更新するかを検討・実装していくことになります。

### API Gateway
AWSマネジメントコンソールからAPI Gatewayを開きます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/241764/c5976321-5f15-240a-1a8e-79666a39b8a7.png)

API Gatewayに設定されているAPI一覧が開きます。
今回制作しているWEBサービスのAPIは`ripple`という名前で作ってあるので、クリックして詳細を開きます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/241764/1615e5b9-3b59-2ebd-a3a7-0a84785f294b.png)


API上に設定されているリソースとメソッドの階層構造が表示されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/241764/c6643b79-b234-11b3-85b3-e92f1ecf72c4.png)

/prj（prjリソース）をクリックするとprjリソースに設定されているメソッドの一覧が表示されます。
GET /prjは、prj一覧の取得処理
POST /prjは、新規prjの登録処理
に対応しています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/241764/33fbcc38-16ec-eb04-ce9f-07018f37f1e6.png)

同様に、/{prjid}（/prj/{prjid}リソース）をクリックすると、このリソースに設定されているメソッドの一覧が表示されます。
GET /prj/{prjid}は、prjidで指定される特定のプロジェクト1件を取得する処理
に対応しています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/241764/5920cdce-e415-7e8f-0ea4-35d43f7a8717.png)

現状、ripple APIには3種類の処理が登録されていることが確認できました。
次は、それぞれの処理の内容についてさらに細かく見ていきたいと思います。

#### GET /prj
/prjリソースのGETをクリックすると、メソッド内の処理フローが表示されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/241764/13c93863-0584-d222-0ecf-bc62ef18fbce.png)

API Gatewayの処理は、下記の流れで行われます。

　　クライアント（今回でいえば、Heroku上のFlaskアプリ）
　　→メソッドリクエスト（API Gatewayがクライアントから受け取るパラメータの設定等を行う）
　　→統合リクエスト（API Gatewayが後続のLambdaへ渡すパラメータの設定を行う）
　　→Lambda
　　→統合レスポンス（API GatewayがLambdaから受け取るパラメータの設定を行う）
　　→メソッドレスポンス（API Gatewayがクライアントに返すパラメータの設定を行う）
　　→クライアント（API Gatewayの処理結果を受け取る）

GET /prjで設定している部分は、メソッドリクエストと統合リクエストの2カ所です。

##### GET /prj メソッドリクエスト
APIキーの必要性をtrueに設定しています。
rippleではすべてのAPIでAPIキー必須としています。
今回APIキー必須としているのは、アクセス元をHeroku上のFlaskアプリのみに制限したいからです。
※なお、オーソライザーを設定すればさらに高度な認証処理を設定することもできますが、今回はそこまでの対応は不要なため、簡単に設定できるAPIキーのみとしています。

URLクエリ文字列パラメータは特に設定していません。
一般に、GETメソッドでは`https://my-api.com/prj?key1=value1&key2=value2`のようにURLの末尾に`?`を付け、`key=value`の形式でパラメータを渡すことができます。
今回は、GET /prjの処理がprjの一覧取得であるため、パラメータの設定は不要でした。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/241764/04dd7c0f-abbd-24e8-68a8-438182fc1ceb.png)

##### 統合リクエスト
今回、後続処理をLambdaで実装しているため、
統合タイプ：Lambda
Lambda関数：ripple_prj
を設定しています。（ripple_prjの実装は後述）

Lambdaに渡すパラメータの設定を行うため、マッピングテンプレートを定義をしています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/241764/ebb3dfaa-36ff-d436-7a72-56501ba01161.png)


マッピングテンプレートの内容は下記のとおりです。
`method`というパラメータに固定文字列`"search"を設定しています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/241764/db5f7a72-bda4-b9ed-bedc-9328608236b7.png)


#### POST /prj
後日追記予定。

* メソッドリクエストの設定内容
    * APIキー認証必須
* 統合リクエストの設定内容
    * 統合タイプ：Lambda
    * Lambda関数：ripple_prj
    * マッピングテンプレート：下記参照
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/241764/c200c79f-a99b-86ac-ed3f-d9153f452331.png)

#### GET /prj/{prjid}
後日追記予定。

* メソッドリクエストの設定内容
    * APIキー認証必須
    * リクエストパス：prjid
* 統合リクエストの設定内容
    * 統合タイプ：Lambda
    * Lambda関数：ripple_prj
    * マッピングテンプレート：下記参照
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/241764/5c79120a-f898-0979-f4d1-772a60b052bd.png)

### Lambda
ripple_rpjというLambda関数を作成しています。

* ランタイム：Python 3.8
* ハンドラ：lambda_function.lambda_handler（デフォルト）
* コード；下記。

```python:ripple_prj/lambda_function.py
import boto3

dynamodb = boto3.resource('dynamodb') #使うリソースを選んでおく
table_name = "ripple_prj" #テーブル名
dynamotable = dynamodb.Table(table_name)

def get(event):
    if "prj_id" not in event:
        return event
    primary_key = {"prj_id": event["prj_id"]} #テーブル名
    res = dynamotable.get_item(Key=primary_key) #指定したprymaryで検索して結果を取得
    item = res["Item"] #要素を指定してないので全部

    return item

def search(event):
    response = dynamotable.scan()
    items = response['Items']
    return items

def create(event):
    print(event)
    response = dynamotable.put_item(
        Item={
            'prj_id': event['prj_id'],
            'name': event['name'],
            'goal': event['goal'],
            'issue': event['issue'],
            'description': event['description'],
            'term': event['term'],
            'term_unit': event['term_unit'],
            'create_date': event['create_date'],
        }
    )
    return response
    

def lambda_handler(event, context):
    print(event)
    return eval(event['method'])(event)
```

実装のポイント

* 下記のようにして、1つのLambda関数で、API Gatewayからのいろいろな呼び出しに対応する
    * API Gatewayから渡されるパラメータはeventの中に入っている。
    * eventの中のmethodパラメータに処理に応じた固定文字列を設定する
    * Lambda側ではmethodパラメータの文字列に応じて、処理を呼び分ける
        * 具体的には`eval(event['method'])(event)`の部分。

## APIに新たなメソッドを追加する場合の変更手順
現状、prjの参照と登録しかないため、prjの更新処理を実装してみましょう。

### DynamoDB
既存のテーブル・項目に対する変更はないので、作業無し

### API Gateway
prjリソースに対するputメソッドを新規追加します。
後日追記予定。

### Lambda
受け取ったprj情報で、既存のprj情報を上書き更新する処理を書き、保存する
後日追記予定

### API Gatewayのデプロイ
すべて完了したらテストして、OKだったらデプロイする。
デプロイすると変更が公開されて、Flaskアプリから新しいメソッドを呼び出せるようになる。
後日追記予定

### Flask側の対応
API呼び出し処理を追加する。
今回のやり方だと、同時にプロジェクトページを開いていた人が複数いて、
それぞれ更新した場合に、先に更新した人のデータが後の人に上書きされて消えてしまうことになる。
プロジェクト名やゴールなど、1つしか保持できない項目は後勝ちでいいが、
タイムラインのように複数の値を持てる項目では後の人が登録する際に、最新のDynamoDB状態を参照してマージする必要がある。
その辺を考慮したロジック実装を行う。
後日追記予定。