AWSでRESTで送信したものをWebSocketでリアルタイムに受け取るサーバレスシステム

#はじめに
オンラインイベントで応援ボタンなど押した時に、現地で一方向の情報として受信し、それをトリガーにエフェクトなど表示したいことがあります。
方法としてはSocket通信や、MQTTなどを利用したり、ngrokなどのローカル環境をネット上で受けれるようにするサービスを使うなどあります。
ただ、クライアントが数百人など多数の環境になるとソケットを大量に張る必要がある、サービスの上限制限や毎回URLが変わるなど運用が面倒なことなどが考えられます。
今回はそれらを回避しつつ、サーバレス環境でコストも少なくしたシステムを作ってみます。

#全体構成
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/58795/13a3d737-ea38-411f-c338-5e2f3abfc591.png)

送信する方はRESTで送ることで接続数に依存せずコストを下げることが可能です。
特にいつ押されるかわからないため、無駄にSocketなどを張って時間課金のコストを増やすということをしないようにします。
受信側は送られたらPUSHで情報を受けたいのでここのみSocketでAWSと接続し通信を行います。
通常はSocketが切られた場合にConnectionIdの削除など他にも付け加えるべきものがありますが、Qiita用の簡易的なものにしていますのでご了承ください。

## DynamoDBの設定
今回は2つのLambdaでconnection_idという値を共有します。
設定は以下のようなものを作成します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/58795/4f4727e8-36c6-50ad-465b-5cebb9b5484b.png)
作成後に
type:"admin"
というデータを１ついれておいてください。
ここに入る値を利用します。

## Lambdaの作成
Lambdaを二つ作成します。

### Socket用Lambda
受信画面がSocket張ったときのAPI Gatewayのconnection_idを保存するためのものです。

```js

const AWS = require('aws-sdk');
const documentClient = new AWS.DynamoDB.DocumentClient({ apiVersion: '2012-08-10'});
exports.handler = async event => {
  try {
    await documentClient.update({
      TableName: 'table',
      Key: {
        'type': 'admin'
      },
      UpdateExpression: "set connectionId = :c",
      ExpressionAttributeValues: {
        ":c": event.requestContext.connectionId
      },
      ReturnValues:"UPDATED_NEW"
    }).promise();
  }
  catch (err) {
    console.log(err);
    return { statusCode: 500, body: 'Failed : ' + JSON.stringify(err) };
  }
  return { statusCode: 200, body: 'Connected.' };
};
```

### 中継用Lambda
RESTを受けAPI GatewayのSocketに中継するためのものです。
あとでAPI GatewayのSocketサーバを構築したあとにendpointのURLはそれに合わせて書き換えする必要があります。

```js

const AWS = require('aws-sdk');
const documentClient = new AWS.DynamoDB.DocumentClient({ apiVersion: '2012-08-10' });

exports.handler = async(event) => {
    const apigwManagementApi = new AWS.ApiGatewayManagementApi({
        apiVersion: '2018-11-29',
        endpoint: 'XXXXXXXX.execute-api.ap-northeast-1.amazonaws.com/default'
    });
    try {
        const dbRes = await documentClient.get({
    	    TableName: 'table',
    	    Key:{
        	'type': 'admin'
            }
	}).promise();
        await apigwManagementApi.postToConnection({
            ConnectionId: dbRes.Item.connectionId,
            Data: JSON.stringify({ "send":"ok" })
        }).promise();
    } catch (e) {
        console.log(e);
    }
    const response = {
        statusCode: 200,
        body: '{"error":false}',
    };
    return response;
};
```

## API Gatewayの設定
こちらもRESTとWebSocketで受けるものを二つ作る必要があります。

### Socket用API Gateway
作成を押しWebSockegt APIのものを構築します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/58795/5631ad4f-2023-4f90-d0e8-c60d78677659.png)
ルート選択式は今回は利用しませんので、デフォルトであるように[$request.body.message]とでもしておいてください。
作成が終わるとWebSocketが接続した場合にどうするかの設定があります。今回は接続時のみ使いたいので$connectの(+)を押して、先ほど作成したSocket用Lambdaを接続してください。
以下のような形になっていたら、アクション>APIのデプロイで公開してください。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/58795/6f1173c1-ca37-90a7-bffa-f6a093c07daa.png)

ここで作成されたアドレスを中継用Lambdaのendpointに記載してください。

### 中継用API Gateway
こちらはHTTP APIで構築します。
このあたりはよくあるものなので他のQiitaの記事や、Lambda側の入力に新規で紐付けするなどしてみてください。
テストでは必要ありませんが、CROSSの設定を忘れないように設定してください。

## HTMLの作成
よくあるWebSocketの接続のページを作成します。
WebSocket接続は他のQiitaなどを参照してください。

```js

// socket
function connect() {
	const socketServerUrl = "wss://XXXXXXX.execute-api.ap-northeast-1.amazonaws.com/YYYYYY";
	let socket = new WebSocket(socketServerUrl);

	socket.onopen = function (e) {
		console.log(e);
	};
	socket.onmessage = function (e) {
		const d = JSON.parse(e.data);
		console.log(d);
	};
	socket.onclose = function (e) {
		console.log(e);
	};
	socket.onerror = function (e) {
		console.log(e);
	};
}
```

# まとめ
作成したHTMLをブラウザで開き、中継用API GatewayのURLを叩くと{"send":"ok"}という値が飛んできてるのがわかると思います。
この中継用API GatewayのURLをよくあるAjaxの通信で叩くことでブラウザ側に随時イベントが飛んできますので、いろいろなものに利用することができます。
これだけの作業と行数のプログラムでsocket通信がネット上に公開してる格安システムができるので、ぜひいろいろと使ってみてください。

## 注意
API GatewayのWebSocketは接続して放置していると10分程度で切断されます。定期的に通信させるか、切断後にリトライ方法など検討しておく必要があります。
それと、受信側のHTMLにはボタンが押されてから1秒程度の遅延が発生します。ここをもっと短くする方法などはコストの兼ね合いなどがありますので、いろいろな他のサービスなども検討してみてください。

## 余談
途中から雑になってるのは、そう、あなたの思った通り！細かい記事を書いて疲れたからです。