AWS Lambda入門②(Node編)〜DynamoDBにアクセスする〜

## 概要

- [AWS Lambda入門①(Node編)〜関数をデプロイして動かす〜](https://qiita.com/ozaki25/items/883aec0ab4398aa4abbe)の続編です
- 今回はLambdaからDynamoDBにアクセスしてデータを保存したり取得したりしてみます
    - テーブルから全件取得、1件取得、1件登録の3つの関数を作成します

## DynamoDBとは
- AWSが提供するマネージドなデータベースサービスです
- RDBとは異なりkey-value形式なドキュメントデータベースです

## LambdaからDynamoDBにアクセスしてみる

### DynamoDBの設定

- まずはDynamoDBを使うためにServerlesFrameworkの設定をします

#### DynamoDBのテーブル定義の設定

- DynamoDBはデータベースなのでテーブルの作成からはじめます
- これまでと同じようにこれもServerlessFrameworkの機能で行うことができます
- 今回はHelloテーブルを作ってみます
- `serverless.yml`を修正します
    - 見づらいのでデフォルトで記載されていたコメントアウトは全て削除しています

```yml:serverless.yml
service: sls-sample
# AWS周りの設定
provider:
  name: aws
  runtime: nodejs12.x
  region: ap-northeast-1
  stage: dev
  environment:
    DYNAMODB_TABLE: ${self:service}-${self:provider.stage}
  iamRoleStatements:
    - Effect: Allow
      Action:
        - dynamodb:Query
        - dynamodb:Scan
        - dynamodb:GetItem
        - dynamodb:PutItem
        - dynamodb:UpdateItem
        - dynamodb:DeleteItem
      Resource: 'arn:aws:dynamodb:${self:provider.region}:*:table/${self:provider.environment.DYNAMODB_TABLE}*'
# Lambdaの設定
functions:
  hello:
    handler: handler.hello
# DynamoDBの設定
resources:
  Resources:
    Hello:
      Type: AWS::DynamoDB::Table
      Properties:
        TableName: ${self:provider.environment.DYNAMODB_TABLE}-hello
        AttributeDefinitions:
          - AttributeName: id
            AttributeType: S
        KeySchema:
          - AttributeName: id
            KeyType: HASH
        ProvisionedThroughput:
          ReadCapacityUnits: 1
          WriteCapacityUnits: 1
```

- 大きく分けて前段と後段の2つの定義を追加しています

```yml:serverless.yml
  environment:
    DYNAMODB_TABLE: ${self:service}-${self:provider.stage}
  iamRoleStatements:
    - Effect: Allow
      Action:
        - dynamodb:Query
        - dynamodb:Scan
        - dynamodb:GetItem
        - dynamodb:PutItem
        - dynamodb:UpdateItem
        - dynamodb:DeleteItem
      Resource: 'arn:aws:dynamodb:${self:provider.region}:*:table/${self:provider.environment.DYNAMODB_TABLE}*'
```

- environmentはファイル内で扱える変数のような感じです
    - テーブル名につけるprefixが何度か登場することになるのでenvironmentに定義しています
    - `self:service`は一行目の`service`の値で`self:provider.stage`は7行目あたりの`stage`の値
- iamRoleStatementsはLambdaを実行するユーザの権限にDynamoDBへのアクセス許可を追加しています

```yml:serverless.yml
resources:
  Resources:
    Hello:
      Type: AWS::DynamoDB::Table
      Properties:
        TableName: ${self:provider.environment.DYNAMODB_TABLE}-hello
        AttributeDefinitions:
          - AttributeName: id
            AttributeType: S
        KeySchema:
          - AttributeName: id
            KeyType: HASH
        ProvisionedThroughput:
          ReadCapacityUnits: 1
          WriteCapacityUnits: 1
```

- テーブルの定義をしています
- テーブル名は上で定義したprefixにつなげてhelloという名前にしています

#### テーブルを作成する

- `serverless.yml`に設定を追加した状態でデプロイすると自動でテーブルが作成されます

```zsh
sls deploy
```

- Webコンソールにアクセスしてテーブルができていることを確認してみましょう

<img width="490" alt="スクリーンショット 2020-03-03 0.38.26.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/175213/30d8c447-c3d0-3f0f-460b-795bca562be5.png">

### DynamoDBへのアクセス処理の実装

#### ライブラリの追加

- DynamoDBにアクセスするために必要なライブラリを追加します

```bash
npm i -D aws-sdk
# or
yarn add -D aws-sdk
```

#### DynamoDBへの接続処理

- 今回は以下の3つの処理を実装しようと思います
    - 全量取得
    - IDで検索して1件取得
    - 1件登録
- 全部一気にやると量が多いのでまずは全量取得からいきます
- `handler.js`を修正します

```js:handler.js
'use strict';

// AWS SDKをimport
const AWS = require('aws-sdk');
// DynamoDBにアクセスするためのクライアントの初期化
const dynamo = new AWS.DynamoDB.DocumentClient();
// 環境変数からテーブル名を取得(あとでserverless.ymlに設定します)
const tableName = process.env.tableName;

// 全量取得
module.exports.getAll = async () => {
  const params = {
    TableName: tableName,
  };
  try {
    // DynamoDBにscanでアクセス
    const result = await dynamo.scan(params).promise();
    // 正常に取得できたらその値を返す
    return {
      statusCode: 200,
      body: JSON.stringify(result.Items),
    };
  } catch (error) {
    // エラーが発生したらエラー情報を返す
    return {
      statusCode: error.statusCode,
      body: error.message,
    };
  }
};

module.exports.hello = async event => {
  return {
    statusCode: 200,
    body: JSON.stringify({ message: event }),
  };
};
```

- 説明はだいたいコードのコメントに書いておきました
- 今回はテーブルの内容を全量取得するので`.scan()`を使いましたがDynamoDB Clientは以下のようなAPIを提供します

> - 複数件取得
>    - scan: 全件取得
>    - query: 条件に該当した項目を全件取得
> - 単項目操作
>     - get: 1件取得
>     - put: 1件置換
>     - update: 1件部分更新
>     - delete: 1件削除

- 新しく`module.export`を追加したので`serverless.yml`も修正します
    - functionの項目を修正します

```yml:serverless.yml
# ...省略

functions:
  hello:
    handler: handler.hello
  getAll:
    handler: handler.getAll
    environment:
      tableName: ${self:provider.environment.DYNAMODB_TABLE}-hello

# ...省略
```

- `handler.js`で`module.export.getAll`としたので`handler: handler.getAll`を追加しました
- `environment`は`handler.js`に対して環境変数としてテーブル名を渡しています

### ローカルで動作確認

- Lambdaのコードができたので動かしてみます
- デプロイする前にローカルで動作確認しましょう

#### DynamoDBをローカルで動かすための設定

- `serverless-dynamodb-local`というDynamoDBをローカルで動かすライブラリがあるのでインストールします

```zsh
yarn add -D serverless-dynamodb-local
```

- DynamoDBをローカルで動かすための設定も追加します
- `serverless.yml`の一番下に追加してください

```yml:serverless.yml
# ...省略

plugins:
  - serverless-dynamodb-local
custom:
  dynamodb:
    stages: dev
    start:
      port: 8082
      inMemory: true
      migrate: true
      seed: true
    seed:
      hello:
        sources:
          - table: ${self:provider.environment.DYNAMODB_TABLE}-hello
            sources: [./seeds/hello.json]
```

- `plugins`は先程インストールした`serverless-dynamodb-local`を使うことを宣言しています
- `custom.dynamodb`でローカルでDBを起動する時に使う設定をしています
    - `port`は何でも大丈夫です(デフォルトは8000)
    - `seed`は事前に登録しておくテストデータの設定です
        - 登録するデータは`./seeds/hello.json`に定義しておきます
- テストデータとして`seeds/hello.json`を作成します

```json:seeds/hello.json
[
  {
    "id": "1",
    "message": "Hello"
  },
  {
    "id": "2",
    "message": "Hello！!!"
  },
  {
    "id": "3",
    "message": "Hello World"
  }
]
```

- ServerlessFrameworkを使ってDBをインストールしセットアップを完了させます

```zsh
sls dynamodb install
```


#### Lambda関数にローカルDB用の処理を追加

- ローカルのDBを使うために`handler.js`に少し手を加えます

```js:handler.js
'use strict';

const AWS = require('aws-sdk');

// 環境変数にLOCALが設定されていたらローカルDB用の設定を使う(portはymlで定義したものを設定)
const options = process.env.LOCAL
  ? { region: 'localhost', endpoint: 'http://localhost:8082' }
  : {};

const dynamo = new AWS.DynamoDB.DocumentClient(options);

// ...省略
```

#### ローカルでDBにアクセスする

- 準備が長くなりましたがいよいよアクセスしてみます
- まずはDBを起動します

```sh
sls dynamodb start
```

- 以下のようなログが出ればOKです

```
$ sls dynamodb start
Dynamodb Local Started, Visit: http://localhost:8082/shell
Serverless: DynamoDB - created table sls-sample-dev-hello
Seed running complete for table: sls-sample-dev-hello
```

- LambdaのgetAll関数を叩きます
    - `LOCAL=true`は環境変数としてLOCALにtrueを設定しています(handler.jsでローカルのDBを見に行く判定で使っていたやつ)

```sh
LOCAL=true sls invoke local --function getAll
```

- 以下のようなログがでればOKです！

```json
{
    "statusCode": 200,
    "body": "[{\"message\":\"Hello\",\"id\":\"1\"},{\"message\":\"Hello！\",\"id\":\"2\"},{\"message\":\"Hello World\",\"id\":\"3\"}]"
}
```

### AWSにデプロイして動作確認

- ローカルで確認できたらAWSにデプロイしましょう

```sh
sls deploy
```

- コマンド一発でデプロイできて便利ですね
- serverlessコマンドでアクセスしてみましょう

```sh
sls invoke --function getAll
```

- 現状データがないのでデータは0件ですが200が返ってきていれば成功です

```json
{
    "statusCode": 200,
    "body": "[]"
}
```

- この時点でデータがとれることを確認したい人はWebコンソール上でデータを追加した上で叩いてみてください
    - あとでもいい人は次でデータ登録処理も追加するのでそのあとに取得できることは確認できます

<img width="592" alt="スクリーンショット 2020-03-05 1.35.36.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/175213/0ec4e057-016b-d47b-201c-923fec2e7a28.png">

### 残りの関数を追加する

- 同じ要領で1件取得と1件登録の処理を追加してみましょう

#### 関数と設定の追加

- `handler.js`の一番下に関数を追加する

```js:handler.js
// ...省略

// 1件取得
module.exports.get = async event => {
  // パラメータで渡されたidを取得
  const { id } = event;

  // 検索条件のidを指定
  const params = {
    TableName: tableName,
    Key: { id },
  };

  try {
    const result = await dynamo.get(params).promise();
    return {
      statusCode: 200,
      body: JSON.stringify(result.Item),
    };
  } catch (error) {
    return {
      statusCode: error.statusCode,
      body: error.message,
    };
  }
};

// 1件登録
module.exports.put = async event => {
  // 一意な値を作るためにタイムスタンプを取得
  const id = String(Date.now());
  const { message } = event;

  const params = {
    TableName: tableName,
    Item: { id, message },
  };

  try {
    const result = await dynamo.put(params).promise();
    return {
      statusCode: 200,
      body: JSON.stringify(result),
    };
  } catch (error) {
    return {
      statusCode: error.statusCode,
      body: error.message,
    };
  }
};
```

- 全件取得の時は`.scan()`を使いましたが1件取得は`.get()`1件登録は`put()`を使っています
- `serverless.yml`のfunctionの項目に設定を追加します

```yml:servreless.yml
# ...省略

functions:
  hello:
    handler: handler.hello
  getAll:
    handler: handler.getAll
    environment:
      tableName: ${self:provider.environment.DYNAMODB_TABLE}-hello
  get:
    handler: handler.get
    environment:
      tableName: ${self:provider.environment.DYNAMODB_TABLE}-hello
  put:
    handler: handler.put
    environment:
      tableName: ${self:provider.environment.DYNAMODB_TABLE}-hello
```

#### ローカルで動作確認

- 1件登録

```sh
LOCAL=true sls invoke local --function put --data '{"message": "Hello!!!!!"}'
```

- 以下のようなログが出ればOK

```json
{
    "statusCode": 200,
    "body": "{\"id\":\"1583340674414\",\"message\":\"Hello!!!!!\"}"
}
```

- 1件取得

```sh
LOCAL=true sls invoke local --function get --data '{"id": "1"}'
```

- 以下のようなログが出ればOK

```json
{
    "statusCode": 200,
    "body": "{\"message\":\"Hello\",\"id\":\"1\"}"
}
```

#### AWSで動作確認

- ServerlessFrameworkのコマンドでデプロイします

```sh
sls deploy
```

- 1件登録

```sh
sls invoke --function put --data '{"message": "Hello!!!!!"}'
```

- 以下のようなログが出ればOK

```json
{
    "statusCode": 200,
    "body": "{\"id\":\"1583340839287\",\"message\":\"Hello!!!!!\"}"
}
```

- 1件取得
    - 直前の1件登録で追加したデータのIDを指定してみましょう


```sh
sls invoke --function get --data '{"id": "1583340839287"}'
```

- 以下のようなログが出ればOK

```json
{
    "statusCode": 200,
    "body": "{\"id\":\"1583340839287\",\"message\":\"Hello!!!!!\"}"
}
```

#### おまけ

- 今回はscanとgetとputを作成しましたがquery(検索条件を指定して複数件取得)の場合は以下のような感じになります
    - パラメータで指定したIDに合致するレコードが複数ある場合に全部取得できるような感じです

```js
module.exports.query = async event => {
  const { id } = event;

  const params = {
    TableName: tableName,
    KeyConditionExpression: 'id = :id',
    ExpressionAttributeValues: { ':id': id },
  };

  try {
    const result = await dynamo.query(params).promise();
    return {
      statusCode: 200,
      body: JSON.stringify(result.Items),
    };
  } catch (error) {
    return {
      statusCode: error.statusCode,
      body: error.message,
    };
  }
};
```

## 感想

- LambdaからDynamoDBへのアクセスはDynamoDB Clientを使うと扱いやすいですね
- ServerlessFrameworkや周辺ライブラリを使うとローカルでも動作確認できるので環境周りもとても充実しています
- けっこう長くなってしまいましたがここまで読んでいただきありがとうございました