WebSocket APIをAmazon API Gatewayで実装してみる

## 概要
こんにちは。ジャパン・グローバルゲートウェイの中谷です。
現在API Gatewayの勉強をしているのですが、やはり机上の知識だけでは挙動を把握しづらいところがあるので、WebSocket APIを実装してみます。

この記事はAWSの以下のチュートリアルの実施内容をまとめたものです。

>Tutorial: Building a serverless chat app with a WebSocket API, Lambda and DynamoDB - Amazon API Gateway
https://docs.aws.amazon.com/apigateway/latest/developerguide/websocket-api-chat-app.html

## システム構成

このチュートリアルでは以下のような典型的なサーバーレスアーキテクチャでWebSocketを実装します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3081962/e0a42184-f389-e16a-85fa-08585806b038.png)

LambdaとDynamoDBの作成に関しては、チュートリアル内で``starter.yml``というCfnのテンプレートが用意されているので、こちらをマネジメントコンソールからデプロイします。スタック名は``websocket-api-chat-app-tutorial``とします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3081962/e89b71ce-45bc-03bf-214b-7dac6440de54.png)

## WebSocketAPIの作成
次に、今回のメインであるWebSocketAPIを作成します。
まず、ルート選択式で``request.body.action``を選択します。ルート選択式では、クライアントがメッセージを送信する際にAPI Gatewayが呼び出すルートを決定します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3081962/f8e57b67-60a1-56ca-c9f5-e6641b616092.png)

次にルートを追加します。
ルートって何やねんという話ですが、こちらはWebSocket APIで定義されるエンドポイントのことです。これらのエンドポイントは、クライアントから送信されたメッセージを処理する時に使用します。
各ルートでは、クライアントから送信されたメッセージの内容に基づいて、特定のアクション（Lambda関数など）を実行します。これにより、APIは動的にメッセージを処理し、適切なレスポンスを返すことができます。

API GatewayのWebSocket APIでは、``$connect``、``$disconnect``、``$default``の3つのルートが事前に定義されています。

``$connect``ルートでは、クライアントがAPIに接続したときに実行されるアクションを処理します。このルートで、接続の初期化や認証などの処理を行います。

``$disconnect``ルートでは、クライアントがAPIから切断されたときに実行されるアクションを処理します。このルートで、リソースの解放や接続情報の削除などの処理を行います。

``$default``ルートは、他のルートにマッチしないメッセージを処理する際のルートです。このルートで、エラー処理や、未定義のメッセージタイプの処理を行うことができます。

また、これらのルートの他にも、自身でカスタムルートを定義して、特定のメッセージタイプやアクションを処理することもできます。今回のチュートリアルでは、``sendmessage``ルートを定義して、リアルタイムにメッセージを送受信できるようにします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3081962/0d872b73-be02-875b-d754-9ce6e1bf6105.png)

ルートを追加したら、各ルートに対応するLambda関数を紐づけます。これにより、ルート選択式で評価したルートごとにハンドラー内に記述した処理が実施されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3081962/6074dfdf-8373-7122-ece0-6acbd1f9ffb3.png)


最後に、ステージを追加してデプロイします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3081962/2fdd0995-88a0-ed08-1a85-39ecc1d7b5ef.png)

## sendmessageハンドラーの処理内容

Cfnのテンプレートを見ると、yaml内にハードコーディングしたソースから動的にLambda関数のハンドラーを生成しているようなので、今回追加した``sendmessage``ルートで使用するハンドラーのコード部分を抜粋してみます。

```javascript:index.js
const AWS = require('aws-sdk');
const ddb = new AWS.DynamoDB.DocumentClient();

exports.handler = async function (event, context) {
  let connections;
  try {
    connections = await ddb.scan({ TableName: process.env.table }).promise();
  } catch (err) {
    return {
      statusCode: 500,
    };
  }
  const callbackAPI = new AWS.ApiGatewayManagementApi({
    apiVersion: '2018-11-29',
    endpoint:
      event.requestContext.domainName + '/' + event.requestContext.stage,
  });

  const message = JSON.parse(event.body).message;

  const sendMessages = connections.Items.map(async ({ connectionId }) => {
    if (connectionId !== event.requestContext.connectionId) {
      try {
        await callbackAPI
          .postToConnection({ ConnectionId: connectionId, Data: message })
          .promise();
      } catch (e) {
        console.log(e);
      }
    }
  });

  try {
    await Promise.all(sendMessages);
  } catch (e) {
    console.log(e);
    return {
      statusCode: 500,
    };
  }

  return { statusCode: 200 };
};
```

色々書いてありますが、メインの処理としては

- DynamoDBから接続情報を格納しているテーブルをスキャンし、すべての接続を取得
- ハンドラの引数``event``にクライアントから送信されたペイロードを格納し、その後ペイロードの``body``部から``message``要素を抽出してJSONでパース
- 接続元の全てのクライアントに対して受信したメッセージを送信し、エラーが発生した場合にはステータスコード500を返し、正常に処理が完了した場合は200を返却

といった内容になっています。

## wscatのインストール
wscatを使用すると、WebSocketの動作確認にあたって自身でサーバー/クライアント側のプログラムを記述しなくても動作確認をすることが出来ます。

wscatは以下のコマンドでインストールできます。

```sh
npm install -g wscat
```

## wscatでの動作確認

wscatで動作確認するにあたり、WebSocket URLをマネジメントコンソールからコピーします。URLは以下のような形式になっています。
``wss://abcdef123.execute-api.us-east-2.amazonaws.com/production``

取得したら、以下のコマンドで接続します。

```sh
wscat -c wss://abcdef123.execute-api.us-west-2.amazonaws.com/production
```

今回は二つのターミナル上から実行し、メッセージを送受信できるか確認します。
各ターミナル上で以下のような形式でメッセージを送信します。``action``に``sendmessage``と入れることで、ルート選択式での評価後``sendmessage``ルートが呼び出されます。
```json
{"action": "sendmessage", "message": "hogehoge"}
```

ターミナルの出力を確認すると、お互いに送信したメッセージが表示されていて、ちゃんとメッセージがやり取りされていることが確認出来ます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3081962/eaa2b283-54e9-6d29-9289-3d0516934057.png)

## まとめ
WebSocket APIはなかなか普段関わる機会がないのでイメージがしづらかったですが、自分で手を動かすと理解度が高まった気がします。今度はGraphQLにも挑戦してみたいです。