【AWS】【Serverless Framework 入門】 API Gateway + Lambda + DynamoDB サンプル

# はじめに
[こちらの記事](https://qiita.com/sugurutakahashi12345/items/afd14d2939a4a26fefa4)の続編になります。

前回は Serverless Framework を使って hello 関数を Lambda にデプロイしたところまで行いました。
（Lambda をただデプロイできれば前編の記事は見なくても全然大丈夫です。）

今回はそれを API Gateway 経由で API として公開し、さらに DynamoDB との疎通にも試みるものになります。

以下の Serverless Framework の[公式サンプル]([https://www.serverless.com/examples/])を参考にしました。

- [AWS | REST API Example](https://www.serverless.com/examples/aws-node-rest-api)
- [AWS | REST API With DynamoDB](https://www.serverless.com/examples/aws-node-rest-api-with-dynamodb)


# できたこと

以下の図の環境を Serverless Framework で構築しました。

![png.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/259125/1020501a-4e9a-a717-7596-27c986876e2c.png)

ソースは [GitHub](https://github.com/suguruTakahashi-1234/sls-hogehoge) に公開したのでそちらを参照してください。
公式サンプルの[「AWS | REST API With DynamoDB」](https://www.serverless.com/examples/aws-node-rest-api-with-dynamodb) とほとんど同じように実装しました。

一気に解説すると大変なので以下の手順に分けて見ていきたいと思います。

1. API の公開（練習）
2. DynamoDB のテーブル作成
3. API の公開と DynamoDB との疎通

# 1. API の公開（練習）

以下の公式サンプルを参考にしました。
・[AWS | REST API Example](https://www.serverless.com/examples/aws-node-rest-api)

hello 関数を GET メソッドの API として公開してみたいと思います。

## serverless.yml の変更

`serverless.yml` に https リクエストのイベントを追加します。

```diff:serverless.yml
 functions:
   hello:
     handler: handler.hello
+    events:
+      - http:
+          path: hello
+          method: get
```

## デプロイ

変更後にデプロイしてみます。

```shell
$ sls deploy -v
（省略）
Serverless: Stack update finished...
Service Information
service: hogehoge
stage: dev
region: us-east-1
stack: hogehoge-dev
resources: 11
api keys:
  None
endpoints:
  GET - https://xxxxxxxxx.execute-api.us-east-1.amazonaws.com/dev/hello
functions:
  hello: hogehoge-dev-hello
layers:
  None

Stack Outputs
HelloLambdaFunctionQualifiedArn: arn:aws:lambda:us-east-1:xxxxxxxxx:function:hogehoge-dev-hello:5
ServiceEndpoint: https://xxxxxxxxx.execute-api.us-east-1.amazonaws.com/dev
ServerlessDeploymentBucketName: hogehoge-dev-serverlessdeploymentbucket-1q2qo8ezm7dcf
```

https リクエストのイベントを追加したことによって、こちらのエンドポイントが開放されます。

```
endpoints:
  GET - https://xxxxxxxxx.execute-api.us-east-1.amazonaws.com/dev/hello
```

## API の疎通確認

curl で叩いてみます。

```
$ curl https://xxxxxxxx.execute-api.us-east-1.amazonaws.com/dev/hello
{
  "message": "Go Serverless v1.0! Your function executed successfully!",
  "input": {
    "resource": "/hello",
    （省略）
  }
}
```

レスポンスが返ってくることが確認できました。API の公開の成功です。

## AWS コンソールの確認

AWS コンソールの API Gateway を確認してみます。
![gateway.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/259125/6c59f1bd-f9fc-9c0e-0392-65c1a762f350.png)
このように API が登録されていることがわかります。
ちなみに `serverless.yml` に `events.http.path` を記述しないと API Gateway には何も登録されません。

以上で API を公開する方法はわかりました。

# 2. DynamoDB のテーブル作成

以下の公式サンプルを参考にしました。
・ [AWS | REST API With DynamoDB](https://www.serverless.com/examples/aws-node-rest-api-with-dynamodb)

こちらのサンプルの DynamoDB のテーブル作成だけを抜き出したものになります。

## serverless.yml の変更

```diff:serverless.yml
 provider:
   name: aws
   runtime: nodejs12.x
+  environment:
+    DYNAMODB_TABLE: ${self:service}-${opt:stage, self:provider.stage}
+  iamRoleStatements:
+    - Effect: Allow
+      Action:
+        - dynamodb:Query
+        - dynamodb:Scan
+        - dynamodb:GetItem
+        - dynamodb:PutItem
+        - dynamodb:UpdateItem
+        - dynamodb:DeleteItem
+      Resource: "arn:aws:dynamodb:${opt:region, self:provider.region}:*:table/${self:provider.environment.DYNAMODB_TABLE}"

（省略）

+resources:
+  Resources:
+    TodosDynamoDbTable:
+      Type: 'AWS::DynamoDB::Table'
+#     DeletionPolicy: Retain
+      Properties:
+        AttributeDefinitions:
+          -
+            AttributeName: id
+            AttributeType: S
+        KeySchema:
+          -
+            AttributeName: id
+            KeyType: HASH
+        ProvisionedThroughput:
+          ReadCapacityUnits: 1
+          WriteCapacityUnits: 1
+        TableName: ${self:provider.environment.DYNAMODB_TABLE}
```

`provider` と `resources` に分けて簡単に解説します。

### provider

`provider` には共通で用いる環境変数の設定や Lambda から他のリソースに関する IAM ロールなどを設定します。

```yaml:serverless.yml
provider:
  name: aws
  runtime: nodejs12.x
  environment:
    DYNAMODB_TABLE: ${self:service}-${opt:stage, self:provider.stage}
  iamRoleStatements:
    - Effect: Allow
      Action:
        - dynamodb:Query
        - dynamodb:Scan
        - dynamodb:GetItem
        - dynamodb:PutItem
        - dynamodb:UpdateItem
        - dynamodb:DeleteItem
      Resource: "arn:aws:dynamodb:${opt:region, self:provider.region}:*:table/${self:provider.environment.DYNAMODB_TABLE}"
```

- `opt:xxx`
  - コマンドライン引数にアクセスできます。例えばコマンドラインで `sls deploy --stage prd` と実行した場合は ${opt:stage} = prd となります。
- `self:xxx`
  - この記法で自身の serverless.yml で定義している key にアクセスできます。
- `${opt:xxx, self:xxx}`
  - `opt:xxx` が定義されていればその値を優先し、もし、定義されていないければ `self:xxx` の値を使用する書き方です。
- `environment`
  - 環境変数を定義します。今回はここにデーブル名を定義します。
- `iamRoleStatements`
  - 実行時に他の AWS リソースを参照する場合にここで Lambda の IAM ロールを設定します。今回は DynamoDB へのアクセス許可を追加しています。
- `Resource: "arn:aws:dynamodb:${opt:region, self:provider.region}:*:table/${self:provider.environment.DYNAMODB_TABLE}"`
  - 設定した IAM ロールはここの項目で設定したリソースに対して有効になります。DynamoDB の特定のテーブルに対して有効にする場合このようになります。ちなみに `${self:provider.environment.DYNAMODB_TABLE}` は上記で解説しているように `environment` で定義している `DYNAMODB_TABLE` の値になります。

### resources

`resources` には AWS リソースの設定事項を記述します。

今回は DynamoDB のテーブル定義を記述していきます。

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

- `TodosDynamoDbTable`
  - ここはリソースの論理 ID です。一意であれば任意の値で大丈夫です。
- `DeletionPolicy: Retain`
  - リソースの削除ポリシーの設定ができます。`Delete`（デフォルト）, `Retain`, `Snapshot` のいずれか 3 つの設定が可能です。スタックが削除されてもテーブルを保持する場合は `Retain` を採用しますが、今回はスタックが削除されたタイミング（`sls remove -v`）でテーブルも削除してほしいので、何も記述せずにデフォルトの `Delete` 設定とします。（[公式ドキュメント](https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/aws-attribute-deletionpolicy.html)）
- `AttributeDefinitions`, `KeySchema`, `ProvisionedThroughput`
  - このあたりの設定は DynamoDB そのものに関わる設定のため今回は説明を省略いたします。ここではテーブルには `id` という項目を設けましたよ、スループット（1 秒あたりの読み込み/書き込み数）は 1 にしますよ、ということぐらいで十分です。（AWS DynamoDB スループットの設定が料金に跳ねるので注意）
- `TableName`
  - テーブル名です。今回は provider の environment で `DYNAMODB_TABLE` は `${self:service}-${opt:stage, self:provider.stage}` = hogehoge-dev と設定していることになります。詳しくは provider の設定を確認してください。

## デプロイ

serverless.yml を更新したらデプロイします。

```
$ sls deploy -v
（省略）
```

## AWS コンソールの確認

テーブルが作成されているはずなので AWS コンソールで DynamoDB のサービスを開いてみます。
![Dynamo.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/259125/04bd0cc1-f274-c72c-fa77-f6239267fd61.png)
hogehoge-dev テーブルが作成されていることがわかります。

また、provider.iamRoleStatements で IAM ロールを設定しているので、CWL も確認してみます。
![IAM.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/259125/a9753658-b72f-0235-6c5d-386792290b7e.png)
hogehoge-dev テーブルに読み込み/書き込み権限がついていることがわかります。

これで DynamoDB のテーブル作成ができるようになりました。

# 3. API の公開と DynamoDB との疎通

こちらの環境を構築していきます。

![png.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/259125/475e27f7-1b52-eaf8-a86d-0cefce1506bd.png)

## インターフェース設計

以下の API を作成したいと思います。

| やりたいこと | path        | method | data(request body parameter) | DynamoDB の操作 | 用意する関数名 | 
| ------------ | ----------- | ------ | ---------- | -------------- | -------------- | 
| 全件取得     | /todos      | GET    | なし       | Scan           | list           | 
| 単一取得     | /todos/{id} | GET    | なし       | GetItem        | get            | 
| 追加         | /todos      | POST   | あり       | PutItem        | create         | 
| 更新         | /todos/{id} | PUT    | あり       | UpdateItem     | update         | 
| 削除         | /todos/{id} | DELETE | なし       | DeleteItem     | delete         | 

## serverless.yml の変更

設計したインターフェースを serverless.yml に反映させます。

`path` と `method` をこれから実装するであろう JavaScript ファイルとそこに定義する関数（`handler`）に結びつけるように書くだけです。

```diff:serverless.yml
functions:
       - http:
           path: hello
           method: get
+
+  list:
+    handler: todos/list.list
+    events:
+      - http:
+          path: todos
+          method: get
+          cors: true
+
+  get:
+    handler: todos/get.get
+    events:
+      - http:
+          path: todos/{id}
+          method: get
+          cors: true
+
+  create:
+    handler: todos/create.create
+    events:
+      - http:
+          path: todos
+          method: post
+          cors: true
+
+  update:
+    handler: todos/update.update
+    events:
+      - http:
+          path: todos/{id}
+          method: put
+          cors: true
+
+  delete:
+    handler: todos/delete.delete
+    events:
+      - http:
+          path: todos/{id}
+          method: delete
+          cors: true
```

## 必要な npm パッケージのインストール

テーブルへのアイテムの追加の処理時にユニークな id を払い出すのに npm の `uuid` というパッケージを使うので packeage.json を作成して登録します。

```
$ npm init -y
$ npm install uuid
```

```json:package.json
{
  （省略）
  "dependencies": {
    "uuid": "^8.3.1"
  }
}
```

## 内部処理の実装

詳しい実装は [GitHub](https://github.com/suguruTakahashi-1234/sls-hogehoge) に公開したのでそちらを参照してください。
公式サンプルの[「AWS | REST API With DynamoDB」](https://www.serverless.com/examples/aws-node-rest-api-with-dynamodb) と全く同じように実装しました。

## デプロイ

デプロイしてみます。

```
$ sls deploy -v     
（省略）
Serverless: Stack update finished...
Service Information
service: hogehoge
stage: dev
region: us-east-1
stack: hogehoge-dev
resources: 41
api keys:
  None
endpoints:
  GET - https://xxxxxxxx.execute-api.us-east-1.amazonaws.com/dev/hello
  GET - https://xxxxxxxx.execute-api.us-east-1.amazonaws.com/dev/todos
  GET - https://xxxxxxxx.execute-api.us-east-1.amazonaws.com/dev/todos/{id}
  POST - https://xxxxxxxx.execute-api.us-east-1.amazonaws.com/dev/todos
  PUT - https://xxxxxxxx.execute-api.us-east-1.amazonaws.com/dev/todos/{id}
  DELETE - https://xxxxxxxx.execute-api.us-east-1.amazonaws.com/dev/todos/{id}
functions:
  hello: hogehoge-dev-hello
  list: hogehoge-dev-list
  get: hogehoge-dev-get
  create: hogehoge-dev-create
  update: hogehoge-dev-update
  delete: hogehoge-dev-delete
layers:
  None

（省略）
```

以下のようにインターフェース設計どおりに API が公開されたようです。

```
endpoints:
  GET - https://xxxxxxxx.execute-api.us-east-1.amazonaws.com/dev/hello
  GET - https://xxxxxxxx.execute-api.us-east-1.amazonaws.com/dev/todos
  GET - https://xxxxxxxx.execute-api.us-east-1.amazonaws.com/dev/todos/{id}
  POST - https://xxxxxxxx.execute-api.us-east-1.amazonaws.com/dev/todos
  PUT - https://xxxxxxxx.execute-api.us-east-1.amazonaws.com/dev/todos/{id}
  DELETE - https://xxxxxxxx.execute-api.us-east-1.amazonaws.com/dev/todos/{id}
```

## API の疎通確認

curl コマンドを叩いて API を呼び出してもいいのですが、Postman の方がいろいろわかりやすいので、Postman から確認してみます。

アイテムを POST メソッドを呼んで追加してみます。
![2 2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/259125/49ff741e-16df-e6eb-2162-507141d667ab.png)
うまく行っているみたいですね。

今度は GET で Scan してみます。
![9 2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/259125/383052d6-d4eb-3827-c105-e36986aea70f.png)
ちゃんと取得できました。

## AWS コンソールの確認

この結果を AWS の DynamoDB からも確認してみます。
![10 2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/259125/5c2781bf-db04-c09e-3c69-b635ed29066d.png)
ちゃんと反映されているみたいですね。

これでやりたいことができました。

## リソースの削除

リソースを削除するまでが Serverless Framework です。

```
$ sls remove -v
（省略）
```

一応、念のため AWS コンソールを見に行って削除されていることは確認した方がいいです。

リソースを削除してもすぐにデプロイできるので、本当に気軽に削除できますね。

# まとめ

Serverless Framework を使って、以下のことができるようになりました。

- API の公開
- 環境変数の設定
- IAM ロールの設定
- DynamoDB のテーブル定義の設定
- インターフェースの定義の設定
- API と DynamoDB の疎通

その結果、以下の図のような環境を Serverless Framework で構築することができました。

![png.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/259125/5dac1a97-7fa3-233e-d5da-1b4e380fc6be.png)

【続編はこちら】
・[【Serverless Framework】カスタムドメイン設定方法（開発環境と本番環境の分離も考慮）](https://qiita.com/sugurutakahashi12345/items/c4e0cf5708a792703b9d)