【サーバーレス初心者向け】Serverless Framework + SwaggerでWeb APIを作る！第3回 Dynamo編(全3回)

# はじめに
こんにちは！
本記事は先日公開した本記事は先日公開した[【サーバーレス初心者向け】Serverless Framework + SwaggerでWeb APIを作る！第2回 WAF適用編](https://qiita.com/nekoze-climber/items/92916700b9375690c1ef)の続きとなります。

前回はWAFを適用し、特定のIPアドレスにのみAPI Gatewayへのアクセスを許可するようにしました。

第3回となる今回は、Lambda関数からDynamoDBにアクセスするAPIのロジック部分を実装したいと思います。

過去の記事はこちら

- [【サーバーレス初心者向け】Serverless Framework + SwaggerでWeb APIを作る！第1回(全3回)](https://qiita.com/nekoze-climber/items/30ca7da8d86629d1e50c)
- [【サーバーレス初心者向け】Serverless Framework + SwaggerでWeb APIを作る！第2回 WAF適用編(全3回)](https://qiita.com/nekoze-climber/items/92916700b9375690c1ef)

# 今回作るもの(再掲)

今回は以下のアーキテクト図のようなWeb APIバックエンドを作っていきます。
API GatewayでクライアントからのAPIリクエストを受信し、該当するLambda関数を呼び出し、必要に応じてDynamoDBからのデータ読み出しおよび書き込みを行います。さらに、WAFを適用することでセキュアにします。

![slsTestAppArchitect.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/671605/484ad570-257d-1e01-de43-13376c137266.png)

APIとしては、ID・名前・身長・体重・年齢の情報を持つPersonモデルを登録・取得・更新・削除するAPIを作りたいと思います。

- GET dev/slsTestApp/v1/api/person/{personId}
- POST dev/slsTestApp/v1/api/person
- PUT dev/slsTestApp/v1/api/person/{personId}
- DELETE dev/slsTestApp/v1/api/person/{personId}

# 実装手順
今回はGET APIを例に以下の手順で実装していきたいと思います。
- DynamoDBのテンプレート実装→デプロイ
- IAM Roleのテンプレート実装→デプロイ
- Lambda関数の実装→デプロイ

# DynamoDBのテンプレート実装→デプロイ

DynamoDBのテンプレートは以下のようになります。


```yml:dynamodb.yml
Resources:
  SlsTestAppPersonTable:
    Type: AWS::DynamoDB::Table
    Properties:
      TableName: SlsTestAppPersonTable-${self:provider.stage}
      AttributeDefinitions:
        - AttributeName: personId
          AttributeType: S
        - AttributeName: age
          AttributeType: S
      KeySchema:
        - AttributeName: personId
          KeyType: HASH
        - AttributeName: age
          KeyType: RANGE
      BillingMode: PAY_PER_REQUEST
```

レコードのIDとなる```PersonId```をパーティションキー、```age```をソートキーにしたテーブルとします。```BillingMode```で課金モードを指定できます。今回はテストアプリのためアクセスはほとんどしないため、使った分だけ課金される```PAY_PER_REQUEST```を指定しています。もしたくさんのアクセスが予想されるシステムへ流用するならあらかじめキャパシティユニットを指定する```PROVISIONED```のほうが安く済む場合もあります。
両モードの料金比較はクラスメソッドさんに記事がありますのでこちらを参照してみてください。

> DynamoDB料金比較
> https://dev.classmethod.jp/articles/reinvent2018-compare-dynamodb-on-demand-price-with-provisioned-price/

その他の記載方法は以下を参照してください。
> DynamoDB CloudFormation
> https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/aws-resource-dynamodb-table.html

テンプレートが完成したら、一度デプロイします。DBはステートフルリソースなので、Lambda関数とはスタックを分けたほうが良いです。そのため、前回の記事で取り上げたWAFのデプロイのように、```dynamodb```フォルダを作り、その下に先ほどの```dynamodb.yml```と```serverless.yml```を作成します。```serverless.yml```は以下のような感じです。

```yml:serverless.yml(dynamodbスタック用)
# Welcome to Serverless!
#
# 中略

service: slsTestAppDynamoDb
# app and org for use with dashboard.serverless.com
#app: your-app-name
# 中略

provider:
  name: aws
  runtime: nodejs12.x

  # you can overwrite defaults here
  stage: dev
  region: ap-northeast-1

 # 中略
# you can add packaging information here
package:
  #  include:
  #    - include-me.js
  #    - include-me-dir/**
  exclude:
    - templates/**

#functions:
#　functionsは記載しない

# you can add CloudFormation resource templates here
resources:
  - ${file(./templates/dynamodb.yml)}

```

ここまでできたら、```dynamodb```フォルダまでコマンドプロンプトを起動し、デプロイコマンドを実行します。

```shell
sls deploy -v
```

# IAM Roleのテンプレート実装→デプロイ

Lambda関数からDynamoDBにアクセスするにはIAM Roleでアクセス権限をつける必要がありますので、そのテンプレートを作成します。

テンプレートは以下のようになります。

```yml:iam.yml
Resources:
  GetPersonRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: "GetPersonRole-${self:provider.stage}"
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: "Allow"
            Principal:
              Service: "lambda.amazonaws.com"
            Action: sts:AssumeRole
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
      Path: "/"
      Policies:
        - PolicyName: "GetPersonPolicy-${self:provider.stage}"
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
              - Effect: "Allow"
                Action:
                  - dynamodb:Query
                Resource:
                  - "arn:aws:dynamodb:${self:provider.region}:${self:provider.environment.ACCOUNT}:table/SlsTestAppPersonTable-${self:provider.stage}"
                  - "arn:aws:dynamodb:${self:provider.region}:${self:provider.environment.ACCOUNT}:table/SlsTestAppPersonTable-${self:provider.stage}/index/*"
```
まず、```ManagedPolicy```として、```AWSLambdaBasicExecutionRole```を付けます。これをつけることで実行結果がCloudWatch Logsに出力されるようになり、デバッグがしやすくなります。
GET APIではバックエンドでDynamoのQueryを使用する想定なので、```dynamodb:Query```を許可する記載にしています。```Resource```にはアクセスしたいDyanmoDBのArnを指定しますが、Queryやセカンダリーインデックスを使ったアクセスの許可の場合には```{DynamoDBのArn}/index/*```も追加する必要があることに注意してください。

それではデプロイしていきます。Lambda関数定義が記載されたsrerverless.ymlの```resources```に```iam.yml```へのパスを指定し、デプロイコマンドを実行してください。
なお、IAM RoleもDynamoDBやWAFと同様にスタックを分けて作成しても構いません。

# Lambda関数の実装→デプロイ
ここまで来たらロジックを実装していきます。
まずはDynamoDBにアクセスする関数を実装していきます。
コードは以下の通りです。

```javascript:personTable.js
"use strict";

module.exports = class PersonTable {
  constructor(serviceClient) {
    this.client = serviceClient;
  }

  getPerson(personId) {
    const params = {
      TableName: process.env.PERSON_TABLE_NAME,
      KeyConditionExpression: "#hash = :personId",
      ExpressionAttributeNames: {
        "#hash": "personId",
      },
      ExpressionAttributeValues: {
        ":personId": personId,
      },
    };
    return new Promise((resolve, reject) => {
      this.client.query(params, (err, data) => {
        if (err) {
          console.log(err);
          reject(err);
        } else {
          console.log("getPerson Success!");
          resolve(data);
        }
      });
    });
  }
};
```
少し特殊かもしれませんが、DynamoDBにアクセスする関数をまとめたクラス```PersonTable```の中にAPI別にメソッドを実装しています。メソッドを使用するときは呼び出し元でDynamoDBクライアントを作りそれをコンストラクターに渡してあげます。

次に、API Gatewayから呼び出されるLambda関数を実装していきます。
[第1回](https://qiita.com/nekoze-climber/items/30ca7da8d86629d1e50c#hello%E9%96%A2%E6%95%B0%E3%81%AE%E4%BF%AE%E6%AD%A3)で作成したindex.jsを以下のように変更します。

```javascript:index.js
"use strict";
var AWS = require("aws-sdk");
AWS.config.update({ region: process.env.region });
//ここでDynamoDBのクライアントを作る
var docClient = new AWS.DynamoDB.DocumentClient({ apiVersion: "2012-08-10" });
//先ほどのPersonTableをインポート
var PersonTable = require("../../aws/personTable");
var Validator = require("../../util/validator");
var Formatter = require("../../util/formatter");

module.exports.handler = async (event, context, callback) => {
//ここでクライアントを渡す
  const personTable = new PersonTable(docClient);
  const validator = new Validator();
  const formatter = new Formatter();
  try {
//DynamoDBからPersonモデルを取得する
    const res = await personTable.getPerson(event.pathParameters.personId);
    if (validator.checkDyanmoQueryResultEmpty(res)) {
      const errorModel = {
        errorCode: "STA00001",
        errorMessage: "Not Found",
      };
      callback(null, {
        statusCode: 404,
        body: JSON.stringify({
          errorModel,
        }),
      });
    }
    callback(null, {
      statusCode: 200,
      body: JSON.stringify(formatter.getPersonFormatter(res)),
    });
  } catch (err) {
    console.log("getPersonTable-index error");
  }
};
```

コメント残している箇所がDynamoDBアクセスに関係する箇所です。
```event```にパスパラメータ(personId)が入っているのでそれを取得して```PersonTable.getPerson```に渡しています。
```validator```や```formatter```は詳しくは述べませんが、それぞれDynamoDBアクセスの結果をバリデーションするクラス/フォーマットするクラスです。

ここまでできたらデプロイしてください。

# 動作確認
では早速動作確認していきましょう。GET APIなので、DynamoDBに手動でレコードを登録しておきます。例えば以下のような内容です。

```json:PersonModel
{
  "personId": "aaa",
  "age": "28",
  "height": "178",
  "name": "tarou",
  "weight": "70"
}
```

GET APIをcurlで実行し、先ほどのjsonが返ってくれば成功です！

```shell
> curl GET https://XXX.execute-api.ap-northeast-1.amazonaws.com/dev/slsTestApp/v1/api/person/aaa

{"personId": "aaa","age": "28","height": "178","name": "tarou","weight": "70"}
```

# おわりに
最後までご覧いただきありがとうございました。
ここまでのサンプルコード一式や、紹介しきれなかったその他のPOST/PUT/DELETEのAPI実装、単体テストなどなど以下のGitHubに置いておきましたので、参考していただければ幸いです。

https://github.com/nekoze-climber/slsTestApp