【AWS SAM】 DynamoDB+Lambda+APIGatewayでAPIを作る

# 内容
DynamoDBのデータを取ってくるAPIを、AWS SAMで構築してみる

要するに、こんな感じのテーブルから、、、

|group(Hash)  |name(Range)  |
|---|---|
|group1  |name1  |
|group1  |name2  |
|group2  |name3  |

こんな感じに取ってこれるようにしたい。

```shell
curl https://hogehoge/Prod/dynamo-api/v1/?group=group1&name=name1

{
    "result": [
        {
            "name": "group1",
            "group": "name1"
        }
    ]
}
```

# 環境
- macOS Mojave -10.14
- python3.7.4
- sam 0.38.0

# 目次
1. アプリケーションの環境構築
2. template.yamlを書く
3. 実行スクリプトを書く
4. デプロイ
5. 検証

# 1. アプリケーションの環境構築

構築は前回の[【AWS SAM】Python版入門](https://qiita.com/al_tarte/items/f43b7cfa70e46e414571) を参考にしてください。
（Pipenvを使ったAWS SAMアプリケーションの初期化手順です。）

# 2. template.yamlを書く

### リソース

今回作成するリソースは以下です

- Table(DynamoDB)
- Lambda用Role(IAM)
- Lambda

### リソース定義

では、上記リソースを作成するtemplate.yamlを書いていきましょう。

```template.yaml
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: >
  dynamo-api

  Sample SAM Template for dynamo-api

# Lambdaのタイムアウト設定
Globals:
  Function:
    Timeout: 60

# テーブル名、関数名などの定義
Parameters:
  DynamoAPIFunctionName:
    Type: String
    Default: dynamo-api
  DynamoTableName:
    Type: String
    Default: DynamoApiTable

# リソースの定義
Resources
  DynamoTable:
    Type: AWS::DynamoDB::Table
    Properties:
      TableName: !Ref DynamoTableName
      # HashキーとRangeキーの定義（カラム名と型）
      AttributeDefinitions:
        - AttributeName: group
          AttributeType: S
        - AttributeName: name
          AttributeType: S
      KeySchema:
        - AttributeName: group
          KeyType: HASH
        - AttributeName: name
          KeyType: RANGE
      # 料金モード指定（PAY_PER_REQUESTは従量課金）
      BillingMode: PAY_PER_REQUEST

  DynamoAPIFunctionRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - 'lambda.amazonaws.com'
            Action:
              - 'sts:AssumeRole'
      ManagedPolicyArns:
        - 'arn:aws:iam::aws:policy/CloudWatchLogsFullAccess'
      # DynamoDBテーブルへの読み取り許可（こうすると、今回作成するテーブルの読み取りのみを許可できます）
      Policies:
        - PolicyName: 'DynamoApiTablePolicy'
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Effect: Allow
                Action:
                  - dynamodb:Get*
                  - dynamodb:Query
                Resource: !GetAtt DynamoTable.Arn

  DynamoAPIFunction:
    Type: AWS::Serverless::Function
    Properties:
      FunctionName: !Ref DynamoAPIFunctionName
      Role: !GetAtt DynamoAPIFunctionRole.Arn
      # app.pyがあるディレクトリパス
      CodeUri: dynamo-api/
      # ハンドラーのパス（むやみに変更するとミスしやすい、、、そっとしておくのが吉）
      Handler: app.lambda_handler
      Runtime: python3.7
      # APIGatewayの設定（SAM独特の記述）
      Events:
        DynamoApi:
          Type: Api
          Properties:
            Path: /dynamo-api/v1/
            Method: get

# Outputsに指定したパラメータは、他のtemplateへデータを渡したりするのが本来の役目ですが
# 今回はデプロイ完了時にAPIのエンドポイントを出力するために使っています。
Outputs:
  DynamoApi:
    Description: "API Gateway endpoint URL for Prod stage for Dynamo API function"
    Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/dynamo-api/v1/"

```

LambdaやAPIGatewayの設定なんかは、ほとんど雛形と同じなので楽に書けます。

# 3. 実行スクリプトを書く

### ライブラリのインストール

まずは、必要なライブラリをインストールします。

```
$ pipenv install requests boto3
```

### APIの処理を書く

次に、実際にDynamoテーブルからデータを取ってくる処理を作っていきます。
GETパラメータに指定された`group`と`name`でDynamoテーブルを検索、その値を返す
という単純な仕組みです。
`name`は前方一ができるようにしてみました。

```app.py
import json
import json
import boto3
from boto3.dynamodb.conditions import Key
from http import HTTPStatus as status


DYNAMODB_TABLE_NAME = 'DynamoApiTable'


def lambda_handler(event, context):

    table = boto3.resource('dynamodb').Table(DYNAMODB_TABLE_NAME)

    params = event.get('queryStringParameters')

    results = dynamo_search(table, params)

    if results is None:
        return {
            "statusCode": status.BAD_REQUEST,
            "body": json.dumps({
                "error": "Bad Request"
            }, ensure_ascii=False),
        }

    return {
        "statusCode": status.OK,
        "body": json.dumps({
            "results": results
        }, ensure_ascii=False),
    }


def dynamo_search(table, params):
    if params.get('group'):
        keyConditionExpression = Key('group').eq(params.get('group'))
        if params.get('name'):
            keyConditionExpression &= Key('name').begins_with(params.get('name'))
        return table.query(
            KeyConditionExpression=keyConditionExpression).get('Items', [])

    return None

```

# 4. デプロイ

### (余談) Pipfile to requirements.txt

`Pipfile`から`requirements.txt`に変換したい、、、
そんな時はこのコマンドで変換可能です。
（boto3もLambdaに最初から用意されてるし、あまり使い所ないかも。。:thinking:）

```
$ pipenv lock -r > requirements.txt
```

今回は、デフォルトの`requirements.txt`のままで大丈夫です:sweat_smile:

### ビルド

```
$ sam build
```

```
Building resource 'DynamoAPIFunction'
Running PythonPipBuilder:ResolveDependencies
Running PythonPipBuilder:CopySource

Build Succeeded

Built Artifacts  : .aws-sam/build
Built Template   : .aws-sam/build/template.yaml

Commands you can use next
=========================
[*] Invoke Function: sam local invoke
[*] Deploy: sam deploy --guided
```

### デプロイ

`~/.aws/credentials`で設定を分けている方は、--profile オプションを付けてください。
`--guided`オプションは初回のみでOKです。（`samconfig.toml`に設定が保存されるので）

```
$ sam deploy --guided
```

```
Stack dynamo-api-stack outputs:
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
OutputKey-Description                                                                                 OutputValue                                                                                         
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
DynamoApi - API Gateway endpoint URL for Prod stage for Dynamo API function                           https://hogehoge/Prod/dynamo-api/v1/                     
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
```

エンドポイントもちゃんと出力されてますね〜

# 5.検証

### Dynamoにデータを登録

今回は(も)、推しのグループ名と名前を登録しました。

|group(Hash)  |name(Range)  |
|---|---|
|ハニーストラップ  |周防パトラ  |
|ハニーストラップ  |堰代ミコ  |
|ホロライブ  |白上フブキ  |
|...  |...  |

### APIを叩いてみる

出力されたエンドポイントにリクエストを送ってみます。見にくいので`jq`で整形しましょう。
（`curl`面倒だよって人はブラウザで:information_desk_person:）

まずは`group`(Hashキー)指定です。

```shell
$ curl https://hogehoge/Prod/dynamo-api/v1/ \
--get \
--data-urlencode 'group=ハニーストラップ' | jq
```

```shell

{
  "results": [
    {
      "name": "周防パトラ",
      "group": "ハニーストラップ"
    },
    {
      "name": "堰代ミコ",
      "group": "ハニーストラップ"
    },
    {
      "name": "島村シャルロット",
      "group": "ハニーストラップ"
    },
    {
      "name": "西園寺メアリ",
      "group": "ハニーストラップ"
    }
  ]
}
```

取れました:clap:

もちろん、前方一致で`name`(Rangeキー)も検索できます。

```shell
$ curl https://hogehoge/Prod/dynamo-api/v1/ \
--get \
--data-urlencode 'group=ハニーストラップ' \
--data-urlencode 'name=周防' | jq
```

```shell
{
  "results": [
    {
      "name": "周防パトラ",
      "group": "ハニーストラップ"
    }
  ]
}
```

取れました!完璧です！

# 終わりに

今回は、単純なAPIを作ってみました。次回は`pytest`でテストをしてみようと思います。
最後まで読んでいただきありがとうございました！

### 前回
[【AWS SAM】Python版入門](https://qiita.com/al_tarte/items/f43b7cfa70e46e414571)