Vue.js + AWS Lambda + dynamo DBでチャット機能を作ってみる【AWSの設定編】

今回はチャット機能を実装したので、備忘録として記事を残していきます。

AWS Lambdaの作成を行った後にフロント側の設定の記事を上げていこうかと思います。
AWS側は`API Gateway + Lambda + DynamoDB`の構成で作成していきます。
※ 2020/08/29時点での作成画面

## API Gatewayの設定

### ①API GatewayからAPI作成

API Gatewayから `APIの作成`を押下し、
WebSocket APIを作成していきます。
![1_API_gateway.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/305412/14c0f24e-72a4-7648-3b59-6e31206d2117.png)
![2_API_gateway.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/305412/556b0e7c-ad20-e8d1-5917-02dd47cc64cf.png)

### ②APIとルート選択式を設定
下記の内容でプロトコルを設定していきます。

プロトコル：WebSocket

|項目|内容|
|---|---|
|API名|vue_chat_test|
|ルート選択式|$request.body.action|
|説明|vue_chat_test|

![3_プロトコル-2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/305412/0768ec30-7b11-206e-ffb3-8ccb51593502.png)

### ③ルートキーを設定内容
API作成後、ルートキーを作成する画面に遷移します。
今回は各ルートキーを下記で設定していきます。

|キー名|用途|
|---|---|
|$connect|クライアントがWebSocket通信を開始する時に利用|
|$disconnect|クライアントがWebSocket通信を終了する時に利用|
|$default|該当するルートキーが無い場合に利用|
|sendmessage|メッセージ送信時に利用|
|getmessages|メッセージ取得時に利用|

ただ、Lambda関数を作成していないとルートキーの作成ができないので、
先にdynamoDBのテーブル作成とLambda関数の作成を行っていきます。


## dynamoDBでテーブル作成

dynamoDBは下記の内容で作成していきます。
構成は、自分宛てに届いたメッセージを見れるチャットの想定です。


- <b>connections

|項目|内容|キー設定|
|---|---|---|
|connection_id|コネクションID|パーテーションキー |
|user_id|ユーザーID||

- <b>messages

|項目|内容|キー設定|
|---|---|---|
|user_id|コネクションID|パーテーションキー|
|created_at|作成日付|ソートキー|
|message|メッセージ||

※ 実際にチャット機能として作成する場合は
　ルームID、誰から誰に送ったのか、既読情報などの情報も必要かと思いますが、
　今回は簡易的なチャット機能なので、こちらの情報だけで作っていきます。


## Lambda関数を作成

ランタイムは `Python3.8` で下記のLambda関数を作成していきます。

|関数|内容|アクセス権限設定|
|---|---|---|
|OnConnect|コネクションテーブルに<br>コネクションID・ユーザーIDを保存|dynamoDB|
|OnDisconnect|コネクションテーブルのレコード削除|dynamoDB|
|SendMessage|メッセージを保存|dynamoDB,ExecuteAPI|
|GetMessages|メッセージを取得|dynamoDB,ExecuteAPI|

※ アクセス権限は一旦フルアクセス許可で設定

- <b>OnConnect

```python
import json
import boto3

# dynamoDBのテーブル取得
dynamodb = boto3.resource('dynamodb')
connections = dynamodb.Table('connections')

def lambda_handler(event, context):

    # 「connectionId、userId」を取得
    connection_id = event.get('requestContext',{}).get('connectionId')
    user_id = event.get('queryStringParameters',{}).get('userId')

    # connectionsテーブルに追加
    result = connections.put_item(
        Item={
            'connection_id': connection_id,
            'user_id': str(user_id)
        }
    )
    return { 'statusCode': 200,'body': 'ok' }
```

- <b> OnDisconnect

```python
import json
import boto3

dynamodb = boto3.resource('dynamodb')
connections = dynamodb.Table('connections')

def lambda_handler(event, context):
    connection_id = event.get('requestContext',{}).get('connectionId')
    result = connections.delete_item(Key={ 'connection_id': connection_id })
    return { 'statusCode': 200, 'body': 'ok' }
```

- <b> SendMessage

```python
import json
import boto3
import json
from boto3.dynamodb.conditions import Key, Attr
from datetime import datetime, timedelta, timezone

# dynamoDBのテーブル取得
dynamodb = boto3.resource('dynamodb')
connections = dynamodb.Table('connections')
messages = dynamodb.Table('messages')

# TimeZoneの設定
JST = timezone(timedelta(hours=+9), 'JST')

def lambda_handler(event, context):

	# パラメータ取得
    body = json.loads(event["body"])
    user_id = body['user_id']
    message = body['message']
    
    # 作成日時
    now = "{0:%Y-%m-%d %H:%M:%S}".format(datetime.now(JST))

    # messageテーブルへ保存
    messages.put_item(
        Item={
            'user_id': user_id,
            'message': message,
            'created_at': now
        }
    )

    # ApiGatewayManagementApiの設定
    domain_name   = event.get('requestContext',{}).get('domainName')
    stage         = event.get('requestContext',{}).get('stage')
    apigw_management = boto3.client('apigatewaymanagementapi',
                                    endpoint_url=F"https://{domain_name}/{stage}")
                                   
    # 送信先userのconnection_idを取得
    connection_items = connections.scan(
		ProjectionExpression="connection_id",
        FilterExpression=Attr("user_id").eq(str(user_id))
	).get('Items')    
 
    if len(connection_items) == 0:
	    return { 'statusCode': 200,'body': 'ok' }

    # メッセージ情報を取得
    get_message_items = messages.query(
        KeyConditionExpression=Key("user_id").eq(str(user_id)) & Key("created_at").lte(now),
        ScanIndexForward=True
    ).get('Items')

    # メッセージがあればメッセージ、無ければ空配列を設定
    content_all = [] if len(get_message_items) == 0 else get_message_items

	# 送信先userにメッセージを返却
    for item in connection_items:
        _ = apigw_management.post_to_connection(ConnectionId=item['connection_id'],
                                                    Data=json.dumps(content_all))

    return { 'statusCode': 200,'body': 'ok' }
```

- <b> GetMessages

```python
import boto3
import json
from boto3.dynamodb.conditions import Key, Attr
from datetime import datetime, timedelta, timezone


# テーブル定義
dynamodb_client = boto3.client('dynamodb')
dynamodb = boto3.resource('dynamodb')
connections = dynamodb.Table('connections')
messages = dynamodb.Table('messages')

# TimeZoneの設定
JST = timezone(timedelta(hours=+9), 'JST')

def lambda_handler(event, context):

    # パラメータ情報を変数に設定
    body = json.loads(event["body"])
    user_id = body['user_id']
    connection_id = event.get('requestContext',{}).get('connectionId')
    domain_name = event.get('requestContext',{}).get('domainName')
    stage       = event.get('requestContext',{}).get('stage')

    # コネクションIDが合っているか確認
    connection_items = connections.query(
        ProjectionExpression="connection_id",
        KeyConditionExpression=Key("connection_id").eq(connection_id),
        FilterExpression=Key('user_id').eq(str(user_id)),
    ).get('Items')

    if len(connection_items) == 0:
        return { 'statusCode': 500,'body': 'something went wrong' }
    
    # ApiGatewayManagementApiの設定
    apigw_management = boto3.client('apigatewaymanagementapi',
                                    endpoint_url=F"https://{domain_name}/{stage}")
    
    # 取得日時
    now = "{0:%Y-%m-%d %H:%M:%S}".format(datetime.now(JST))

    # メッセージ情報を取得
    get_message_items = messages.query(
        KeyConditionExpression=Key("user_id").eq(str(user_id)) & Key("created_at").lte(now),
        ScanIndexForward=True
    ).get('Items')

    # メッセージがあればメッセージ、無ければ空配列を設定
    content_all = [] if len(get_message_items) == 0 else get_message_items

    # コネクションにメッセージをPOST
    _ = apigw_management.post_to_connection(
        ConnectionId=connection_id,
        Data=json.dumps(content_all)
    )
    
    return {
        'statusCode': 200,
        'body': 'ok'
    }
```

## ルートキーとLambdaの紐付け

作成したLambdaとAPI Gatewayのルートキーとを紐付けていきます。

|ルートキー|Lambda関数|
|---|---|
|$connect|OnConnect|
|$disconnect|OnDisconnect|
|$default|OnConnect|
|sendmessage|SendMessage|
|getmessages|GetMessages|

![4_ルートキーとLambdaの紐付け.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/305412/8c92acdd-8945-06de-51a4-79782700acfb.png)


## 作成したWebSocket APIをデプロイ

ルートキーの紐付けが完了したら、WebSocket APIをデプロイしていきます。
今回はステージ名は`Test`で作成します。

![5_デプロイ.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/305412/d93cd5e3-58c8-aaed-3bbf-d99ebce30870.png)

デプロイが完了したら、 `WebSocket URL`と`接続 URL`が発行されます。
フロントとの通信に必要なURLになります。

AWS側での設定は以上で、最後に動作確認だけ実施していきます。

## wscatでのWebSocket APIを動作確認

wscatをインストール

```
$ npm install -g wscat
```

wscatを使って、動作を確認してdynamoDBにデータが保存されていることを確認する。

```bash
$ wscat -c [WebSocket URL]?userId=A

Connected (press CTRL+C to quit)
> {"action":"sendmessage","user_id": "A", "message": "Test"}
[{"message": "Test", "user_id": "A", "created_at": "2020-08-29 17:22:43"}]

> {"action":"getmessages", "user_id": "A"}
[{"message": "Test", "user_id": "A", "created_at": "2020-08-29 17:22:43"}]
```

dynamoDBの状態はこちら！

![スクリーンショット 2020-08-29 17.29.17.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/305412/b877c5ad-cdce-0b6c-0cf1-305068a9c758.png)
![スクリーンショット 2020-08-29 17.29.21.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/305412/36491fa9-a869-65e6-9969-1f6c451bf1d4.png)

## まとめ

`Vue.js + AWS Lambda + dynamo DB`でチャット機能のAWSの設定を作成してみました。
今回は動作することをメインに作成したので、エラー時の処理はあまり入れれていないのと、実際に実装するには他にも考えないといけないことも多いかと思います。

次はVue.jsを使ってフロント画面を作成して、APIとの通信を実施していきます。
※ 投稿日時は未定

ご指摘などがあれば是非コメントお願いいたします！