[AWS]Serverless Frameworkを使ってAPI Gateway×Lambda×DynamoDBを使用したWebAPIを構築する

## インフラ構成
WebAPIの作成に当たって今回使用するAWSの各サービスを簡単にまとめてみます。

・API Gateway
HTTPリクエストを受け付けて、他のAWSサービス(Lambda等)にルーティングする機能です。REST APIやWebSocket APIを簡単に作成できます。API Gatewayでは、リクエストのパスやクエリパラメータ、ヘッダー、ボディなどをLambda関数に渡すことができます。
https://aws.amazon.com/jp/api-gateway/

・Lambda
サーバレスコンピューティングのサービスです。コードをアップロードするだけで、イベントやトリガーに応じて自動的に実行されます。サーバの管理やスケーリングを気にする必要がありません。Lambdaは、Node.js、Python、Java、Go、Rubyなどの言語に対応しています
https://aws.amazon.com/jp/lambda/

・DynamoDB
DynamoDBは、NoSQLデータベースのサービスで、高速でスケーラブルなキーバリューストアやドキュメントストアを提供します。DynamoDBでは、テーブルを作成して、パーティションキーとオプションでソートキーを指定して、データを保存することができます。
https://aws.amazon.com/jp/dynamodb/

これらのサービスを組み合わせることで、HTTPリクエストをAPI Gatewayで受けて、そこからLambdaにリクエストデータを渡してプログラムを起動させ、LambdaからDynamoDBにデータを保存するといったサーバーレスなAPIのインフラを構成することができます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/273779/080950dc-cb06-6630-9997-dd149239aa28.png)


## API要件
作成するAPIの機能としては、CRUD操作を有するシンプルなタスク管理APIを実装していこうと思います。今回は主にインフラ構築が目的ですので、ソースコードは最小限に抑えて実装していきます。

<table>
    <thead>
        <tr>
            <th>HTTP Method</th>
            <th>パス</th>
            <th>概要</th>
        </tr>
    </thead>
    <tbody>
        <tr>
            <td>POST</td>
            <td>/tasks</td>
            <td>タスクを作成</td>
        </tr>
        <tr>
            <td>GET</td>
            <td>/tasks</td>
            <td>タスクを取得</td>
        </tr>
        <tr>
            <td>PUT</td>
            <td>/tasks/:id</td>
            <td>タスクを更新</td>
        </tr>
        <tr>
            <td>DELETE</td>
            <td>/tasks/:id</td>
            <td>タスクを削除</td>
        </tr>
    </tbody>
</table>

## 環境準備
AWSのリソースを作成するにあたって今回はServerless Frameworkを使用してリソースを作成していこうと思います。
まずは、Serverless Frameworkをインストールします。

`npm install -g serverless@3.24.1`

インストールが完了しましたら、次にServerless Frameworkのテンプレートを作成します。

`serverless create --template aws-nodejs-ecma-script`

テンプレートファイル一式が作成されましたら必要なライブラリをインストールします。
`npm install`

最後に後の実装でプログラムからAWSのリソースを操作するためにAWSのSDKが必要になるのでインストールしておきます。

`npm install aws-sdk@2`


## DynamoDBテーブルの作成
DynamoDBでタスクを管理するためのテーブルを作成していきます。
作成するにあたって必要な設定値をserverless.ymlファイルのresources要素の下に追記していきます。
https://www.serverless.com/framework/docs/providers/aws/guide/serverless.yml#aws-resources

```diff_yaml:serverless.yml
+resources:
+  Resources:
+    TasksDynamoDBTable:
+      Type: AWS::DynamoDB::Table
+      Properties:
+        TableName: tasks
+        AttributeDefinitions:
+          - AttributeName: id
+            AttributeType: S
+        KeySchema:
+          - AttributeName: id
+            KeyType: HASH
+        ProvisionedThroughput:
+          ReadCapacityUnits: 5
+          WriteCapacityUnits: 5
```

ファイルの編集が完了しましたら、`npx serverless deploy`コマンドを実行してデプロイを行います。
`npx serverless deploy`

## IAM Roleプラグインの導入
後々API実装の中でDynamoDBとの通信でIAM設定を入れる必要になりますので
Serverless frameworkでIAM設定を行うためのプラグインを導入していきます。

`npm install --save-dev serverless-iam-roles-per-function@3.2.0`

コマンドの実行が完了しましたら、serverless.ymlファイルのplugins要素の下に設定を追記します。

```diff_yaml:serverless.yml
plugins:
  - serverless-webpack
+  - serverless-iam-roles-per-function
```

## タスク作成APIの実装
API Gatewayを利用してAPIサーバーとしてLambdaを利用するので、APIとして動かす為に必要な設定値をserverless.ymlファイルに追記していきます。
また、DynamoDBにデータ作成を許可するIAM設定を追記します。

https://www.serverless.com/framework/docs/providers/aws/events/http-api

```diff_yaml:serverless.yml
+functions:
+  taskPost:
+    handler: src/taskHandler.post
+    events:
+      - httpApi:
+          method: post
+          path: /tasks
+    iamRoleStatements:
+      - Effect: Allow
+        Action:
+          - dynamodb:PutItem
+        Resource: 'arn:aws:dynamodb:*:*:table/tasks'
```

taskHandler.jsファイルにリクエストボディー情報をもとにDynamoDBのテーブルにタスクを登録するAPIのプログラム処理を追記します。

```diff_javascript:taskHandler.js
+import {DynamoDB} from 'aws-sdk'
+import crypto from 'crypto'
+
+export async function post(event, context) {
+  const requestBody = JSON.parse(event.body)
+  const item = {
+    id: {
+      S: crypto.randomUUID()
+    },
+    title: {
+      S: requestBody.title
+    }
+  }
+  const dynamodb = new DynamoDB({
+    region: 'ap-south-1'
+  })
+  await dynamodb.putItem({
+    TableName: 'tasks',
+    Item: item
+  }).promise()
+  return item
+}
```
ファイルの編集が完了しましたら、`npx serverless deploy`コマンドを実行してデプロイを行います。
```
ec2-user:~/environment/lambda-demo $ npx serverless deploy
(node:3722) NOTE: We are formalizing our plans to enter AWS SDK for JavaScript (v2) into maintenance mode in 2023.

Please migrate your code to use AWS SDK for JavaScript (v3).
For more information, check the migration guide at https://a.co/7PzMCcy
(Use `node --trace-warnings ...` to show where the warning was created)

Deploying lambda-demo to stage dev (ap-south-1)

✔ Service deployed to stack lambda-demo-dev (103s)

endpoint: POST - https://nbf2t81za3.execute-api.ap-south-1.amazonaws.com/tasks
functions:
  taskPost: lambda-demo-dev-taskPost (2 MB)

Need a faster logging experience than CloudWatch? Try our Dev Mode in Console: run "serverless dev"
```
コマンドの戻り値のendpointにデプロイ先のURLが記載されいるのいるので実際にそのURLに対してリクエストを投げてみます。
想定通りのレスポンスが帰ってくるのが確認できました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/273779/166fcdef-3f83-eb04-d717-1a422f4d7814.png)


## タスク取得APIの実装
serverless.ymlファイルのfunctions要素の配下にタスク取得APIの設定情報、及びDynamoDBからデータ取得を許可するIAM設定を追記します。


```diff_yaml:serverless.yml
functions:
+  taskList:
+    handler: src/taskHandler.list
+    events:
+      - httpApi:
+          method: get
+          path: /tasks
+    iamRoleStatements:
+      - Effect: Allow
+        Action:
+          - dynamodb:Scan
+        Resource: 'arn:aws:dynamodb:*:*:table/tasks'    
+           
```

taskHandler.jsファイルにDynamoDBのテーブルに登録済みのタスク一覧を出力するAPIのプログラム処理を追記します。
```diff_javascript:difftaskHandler.js
+export async function list(event, context) {
+  const dynamodb = new DynamoDB({
+    region: 'ap-south-1'
+  });
+  const result = await dynamodb.scan({
+    TableName: 'tasks'
+  }).promise();
+  const tasks = result.Items.map((item) => {
+    return {
+      id: item.id.S,
+      title: item.title.S
+    };
+  });
+  return {
+    tasks
+  };
+}
```

ファイルの編集が完了しましたら、`npx serverless deploy`コマンドを実行してデプロイを行います。

```
ec2-user:~/environment/lambda-demo $ npx serverless deploy(node:5254) NOTE: We are formalizing our plans to enter AWS SDK for JavaScript (v2) into maintenance mode in 2023.

Please migrate your code to use AWS SDK for JavaScript (v3).
For more information, check the migration guide at https://a.co/7PzMCcy
(Use `node --trace-warnings ...` to show where the warning was created)

Deploying lambda-demo to stage dev (ap-south-1)

✔ Service deployed to stack lambda-demo-dev (62s)

endpoints:
  POST - https://nbf2t81za3.execute-api.ap-south-1.amazonaws.com/tasks
  GET - https://nbf2t81za3.execute-api.ap-south-1.amazonaws.com/tasks
functions:
  taskPost: lambda-demo-dev-taskPost (2 MB)
  taskList: lambda-demo-dev-taskList (2 MB)

Need a faster logging experience than CloudWatch? Try our Dev Mode in Console: run "serverless dev"
```

コマンドの戻り値のendpointにデプロイ先のURLが記載されいるのいるので実際にそのURLに対してリクエストを投げてみます。
想定通りのレスポンスが帰ってくるのが確認できました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/273779/49dacfaa-9635-0ebf-3941-9581f7d39837.png)

## タスク更新APIの実装
serverless.ymlファイルのfunctions要素の配下にタスク更新APIの設定情報、及びDynamoDBのデータの更新を許可するIAM設定を追記します。

```diff_yaml:serverless.yml
functions:
+  taskPut:
+    handler: src/taskHandler.put
+    events:
+      - httpApi:
+          method: put
+          path: /tasks/{taskId}
+    iamRoleStatements:
+      - Effect: Allow
+        Action:
+          - dynamodb:updateItem
+        Resource: 'arn:aws:dynamodb:*:*:table/tasks'
```

taskHandler.jsファイルにパスパラメータで指定されたタスクをリクエストボディー情報をもとにDynamoDBのテーブルに登録されているタスクを更新するAPIのプログラム処理を追記します。

```diff_javascript:taskHandler.js
+export async function put(event, context) {
+  let taskId = event.pathParameters.taskId;
+  const requestBody = JSON.parse(event.body);
+  const dynamodb = new DynamoDB({
+    region: 'ap-south-1'
+  });
+  const params = {
+    TableName: 'tasks',
+    Key: {
+      id: {
+        S: taskId
+      },
+    },
+    UpdateExpression: 'set title = :x',
+    ExpressionAttributeValues: {
+      ':x': {
+        S: requestBody.title
+      },
+    },
+    ReturnValues: 'ALL_NEW',
+  };
+  const result = await dynamodb.updateItem(params).promise();
+  return {
+    task: result
+  };
+}
```

ファイルの編集が完了しましたら、`npx serverless deploy`コマンドを実行してデプロイを行います。
```
ec2-user:~/environment/lambda-demo $ npx serverless deploy
(node:5945) NOTE: We are formalizing our plans to enter AWS SDK for JavaScript (v2) into maintenance mode in 2023.

Please migrate your code to use AWS SDK for JavaScript (v3).
For more information, check the migration guide at https://a.co/7PzMCcy
(Use `node --trace-warnings ...` to show where the warning was created)

Deploying lambda-demo to stage dev (ap-south-1)

✔ Service deployed to stack lambda-demo-dev (62s)

endpoints:
  POST - https://nbf2t81za3.execute-api.ap-south-1.amazonaws.com/tasks
  GET - https://nbf2t81za3.execute-api.ap-south-1.amazonaws.com/tasks
  PUT - https://nbf2t81za3.execute-api.ap-south-1.amazonaws.com/tasks/{taskId}
functions:
  taskPost: lambda-demo-dev-taskPost (2 MB)
  taskList: lambda-demo-dev-taskList (2 MB)
  taskPut: lambda-demo-dev-taskPut (2 MB)

Need a faster logging experience than CloudWatch? Try our Dev Mode in Console: run "serverless dev"
```

コマンドの戻り値のendpointにデプロイ先のURLが記載されいるのいるので実際にそのURLに対してリクエストを投げてみます。
想定通りのレスポンスが帰ってくるのが確認できました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/273779/42ed6813-f348-c2c9-89da-eec769a929c7.png)


## タスク削除APIの実装
serverless.ymlファイルのfunctions要素の配下にタスク削除APIの設定情報、及びDynamoDBからデータ削除を許可するIAM設定を追記します。

```diff_yaml:serverless.yml
functions:
+  taskDelete:
+    handler: src/taskHandler.remove
+    events:
+      - httpApi:
+          method: delete
+          path: /tasks/{taskId}
+    iamRoleStatements:
+      - Effect: Allow
+        Action:
+          - dynamodb:DeleteItem
+        Resource: 'arn:aws:dynamodb:*:*:table/tasks'
```

taskHandler.jsファイルにパスパラメータで指定されたタスクをDynamoDBのテーブルから削除するAPIのプログラム処理を追記します。
```diff_javascript:taskHandler.js
+export async function remove(event, context) {
+  let taskId = event.pathParameters.taskId;
+  const dynamodb = new DynamoDB({
+    region: 'ap-south-1'
+  });
+  const params = {
+    TableName: 'tasks',
+    Key: {
+      id: {
+        S: taskId
+      },
+    },
+    ReturnValues: 'ALL_OLD'
+  };
+  const result = await dynamodb.deleteItem(params).promise();
+  return {
+    task: result
+  };
+}
```

ファイルの編集が完了しましたら、`npx serverless deploy`コマンドを実行してデプロイを行います。
```
ec2-user:~/environment/lambda-demo $ npx serverless deploy
(node:6657) NOTE: We are formalizing our plans to enter AWS SDK for JavaScript (v2) into maintenance mode in 2023.

Please migrate your code to use AWS SDK for JavaScript (v3).
For more information, check the migration guide at https://a.co/7PzMCcy
(Use `node --trace-warnings ...` to show where the warning was created)

Deploying lambda-demo to stage dev (ap-south-1)

✔ Service deployed to stack lambda-demo-dev (62s)

endpoints:
  POST - https://nbf2t81za3.execute-api.ap-south-1.amazonaws.com/tasks
  GET - https://nbf2t81za3.execute-api.ap-south-1.amazonaws.com/tasks
  PUT - https://nbf2t81za3.execute-api.ap-south-1.amazonaws.com/tasks/{taskId}
  DELETE - https://nbf2t81za3.execute-api.ap-south-1.amazonaws.com/tasks/{taskId}
functions:
  taskPost: lambda-demo-dev-taskPost (2 MB)
  taskList: lambda-demo-dev-taskList (2 MB)
  taskPut: lambda-demo-dev-taskPut (2 MB)
  taskDelete: lambda-demo-dev-taskDelete (2 MB)

Need a faster logging experience than CloudWatch? Try our Dev Mode in Console: run "serverless dev"
```

コマンドの戻り値のendpointにデプロイ先のURLが記載されいるのいるので実際にそのURLに対してリクエストを投げてみます。
想定通りのレスポンスが帰ってくるのが確認できました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/273779/e2f2bc34-1a10-1c39-50b4-e2b51de001de.png)

## リソースの削除
最後に今回作成したリソースをまとめて削除します。

`npx serverless remove`

## 完成コード
```js:taskHandler.js
import {
  DynamoDB
} from 'aws-sdk';
import crypto from 'crypto';

export async function post(event, context) {
  const requestBody = JSON.parse(event.body);
  const item = {
    id: {
      S: crypto.randomUUID()
    },
    title: {
      S: requestBody.title
    }
  };
  const dynamodb = new DynamoDB({
    region: 'ap-south-1'
  });
  await dynamodb.putItem({
    TableName: 'tasks',
    Item: item
  }).promise();
  return item;
}

export async function list(event, context) {
  const dynamodb = new DynamoDB({
    region: 'ap-south-1'
  });
  const result = await dynamodb.scan({
    TableName: 'tasks'
  }).promise();
  const tasks = result.Items.map((item) => {
    return {
      id: item.id.S,
      title: item.title.S
    };
  });
  return {
    tasks
  };
}

export async function put(event, context) {
  let taskId = event.pathParameters.taskId;
  const requestBody = JSON.parse(event.body);
  const dynamodb = new DynamoDB({
    region: 'ap-south-1'
  });
  const params = {
    TableName: 'tasks',
    Key: {
      id: {
        S: taskId
      },
    },
    UpdateExpression: 'set title = :x',
    ExpressionAttributeValues: {
      ':x': {
        S: requestBody.title
      },
    },
    ReturnValues: 'ALL_NEW',
  };
  const result = await dynamodb.updateItem(params).promise();
  return {
    task: result
  };
}

export async function remove(event, context) {
  let taskId = event.pathParameters.taskId;
  const dynamodb = new DynamoDB({
    region: 'ap-south-1'
  });
  const params = {
    TableName: 'tasks',
    Key: {
      id: {
        S: taskId
      },
    },
    ReturnValues: 'ALL_OLD'
  };
  const result = await dynamodb.deleteItem(params).promise();
  return {
    task: result
  };
}
```

```yaml:serverless.yml
service: lambda-demo
# app and org for use with dashboard.serverless.com
#app: your-app-name
#org: your-org-name
frameworkVersion: "3"

# Add the serverless-webpack plugin
plugins:
  - serverless-webpack
  - serverless-iam-roles-per-function


provider:
  name: aws
  runtime: nodejs16.x
  region: ap-south-1

functions:
  taskPost:
    handler: src/taskHandler.post
    events:
      - httpApi:
          method: post
          path: /tasks
    iamRoleStatements:
      - Effect: Allow
        Action:
          - dynamodb:PutItem
        Resource: 'arn:aws:dynamodb:*:*:table/tasks'
  taskList:
    handler: src/taskHandler.list
    events:
      - httpApi:
          method: get
          path: /tasks
    iamRoleStatements:
      - Effect: Allow
        Action:
          - dynamodb:Scan
        Resource: 'arn:aws:dynamodb:*:*:table/tasks'
  taskPut:
    handler: src/taskHandler.put
    events:
      - httpApi:
          method: put
          path: /tasks/{taskId}
    iamRoleStatements:
      - Effect: Allow
        Action:
          - dynamodb:updateItem
        Resource: 'arn:aws:dynamodb:*:*:table/tasks'
  taskDelete:
    handler: src/taskHandler.remove
    events:
      - httpApi:
          method: delete
          path: /tasks/{taskId}
    iamRoleStatements:
      - Effect: Allow
        Action:
          - dynamodb:DeleteItem
        Resource: 'arn:aws:dynamodb:*:*:table/tasks'
        
resources:
  Resources:
    TasksDynamoDBTable:
      Type: AWS::DynamoDB::Table
      Properties:
        TableName: tasks
        AttributeDefinitions:
          - AttributeName: id
            AttributeType: S
        KeySchema:
          - AttributeName: id
            KeyType: HASH
        ProvisionedThroughput:
          ReadCapacityUnits: 5
          WriteCapacityUnits: 5
```