- なんか古いやり方の記事しかなかったので、自分で調べたことを書き残しておくよ
前提・読者の想定
- 普段 Serverless x Lambda x Node.js x TypeScript が扱えていること
- そこの詳しい説明は無い
やりたいこと
- WebSocket サーバーを TypeScript で書くよ
- AWS API Gateway で serve するよ
- Serverless でデプロイするよ
- ローカル環境でも簡単にデバッグしたいよ
- 以上のことをシンプルに実現したいよ
- 公式サンプルだとDynamo使ったりしてるけど、Dynamo触りたくないし。。。
メリット
- AWS でオートスケールする WebSocket サーバーが書けるよ
- とってもお手軽だし、そのうえランニングコストも低い
- (比較対象: GCP CloudRun しか見てないけど。)
やったこと
準備
# in your lambda project
npm i aws-lambda-ws-server
# for testing websocket
npm i -g wscat
他のドキュメントでよく見る serverless-websockets-plugin
は 本日時点で不要
aws-lambda-ws-server
- メリット
- ローカル起動と、Serverless でのデプロイがサポートされているよ
- ちゃんと動いたし、中身がシンプルでバグる余地も少なそうだから採用したよ
- デメリット
- Starが少ないよ
- README がいろいろ説明不足だよ
- 型定義もないよ 😇
-
注意点
- MAPPING_KEY 環境変数で route を分岐するために使うプロパティ名の指定が必要
- ローカル起動のデフォルトでは
(message json).message
が参照される (なんでやねん) - デプロイ後のデフォルトは
(message json).action
- このサンプルでは環境変数を使ってローカルの挙動を
action
に合わせている
src/handler.ts
const ws = require("aws-lambda-ws-server");
export const websocketApp = ws(
ws.handler({
// 接続時に通る
async connect(event: WebSocketEvent) {
console.log("connection %s", event.id);
return { statusCode: 200 };
},
// 切断時に通る
async disconnect(event: WebSocketEvent) {
console.log("disconnect %s", event.id);
return { statusCode: 200 };
},
// 未定義の action を指定すると通る
async default(event: WebSocketEvent) {
const {
id: connectionId,
message: { body },
context: { postToConnection },
} = event;
console.log("default message", connectionId, body);
await postToConnection({ action: "default", echo: body }, connectionId);
return { statusCode: 200 };
},
// "myAction1" アクションのハンドラ
async myAction1(event: WebSocketEvent) {
const {
id: connectionId,
message: { body },
context: { postToConnection },
} = event;
console.log("myAction1", connectionId, body);
await postToConnection({ action: "myAction1", echo: body }, connectionId);
return { statusCode: 200 };
},
// "myAction2" アクションのハンドラ
async myAction2(event: WebSocketEvent) {
// 省略: myAction1 と同等の実装
},
// 動かないサンプル (ローカルでは動くが、デプロイ後は動かない)
// なぜなら serverless.yml の route 定義が漏れているから
async myAction3(event: WebSocketEvent) {
// 省略: myAction1 と同等の実装
},
})
);
/**
* aws-lambda-ws-server に型定義がないので書いておく
*/
export type WebSocketEvent = {
id: string;
event: {
requestContext: {
routeKey: string;
eventType: "CONNECT" | "DISCONNECT" | "MESSAGE";
connectionId: string;
};
multiValueHeaders: { [key: string]: string[] };
body: string;
};
context: {
postToConnection: (body: any, connectionId: string) => Promise<void>;
};
// message は any json だが、このプロジェクトでは常にこの形として定義する
message: {
action: string;
body: any;
};
};
serverless.yml
※追記したところだけ抜粋
provider:
iam:
role:
statements:
- Effect: Allow
Action:
- "execute-api:ManageConnections"
Resource:
- "arn:aws:execute-api:*:*:**/@connections/*"
functions:
websocketApp:
handler: src/handler.websocketApp
events:
- websocket:
route: $connect
- websocket:
route: $disconnect
- websocket:
route: $default
- websocket:
route: myAction1
- websocket:
route: myAction2
ローカルデバッグ
console 1 (server)
# 先述の注意点。route分岐のために使うプロパティ名を環境変数で指定
$ export MAPPING_KEY=action
$ npx ts-node-dev --inspect=9229 --clear --respawn src/handler.ts
# 以下 client からのリクエストに対するログ出力
connection NB+jzmXmqOtPpnCa8eNVDw==
myAction1 NB+jzmXmqOtPpnCa8eNVDw== { hello: 'websocket1' }
myAction2 NB+jzmXmqOtPpnCa8eNVDw== { hello: 'websocket2' }
myAction3 NB+jzmXmqOtPpnCa8eNVDw== { hello: 'websocket3' }
default message NB+jzmXmqOtPpnCa8eNVDw== { hello: 'websocket4' }
disconnect NB+jzmXmqOtPpnCa8eNVDw==
console 2 (client)
$ wscat -c ws://localhost:5000
Connected (press CTRL+C to quit)
> {"action":"myAction1","body":{"hello":"websocket1"}}
< {"action":"myAction1","echo":{"hello":"websocket1"}}
> {"action":"myAction2","body":{"hello":"websocket2"}}
< {"action":"myAction2","echo":{"hello":"websocket2"}}
> {"action":"myAction3","body":{"hello":"websocket3"}}
< {"action":"myAction3","echo":{"hello":"websocket3"}}
> {"action":"myAction4","body":{"hello":"websocket4"}}
< {"action":"default","echo":{"hello":"websocket4"}}
デプロイ後の挙動
& wscat -c wss://{****}.execute-api.ap-northeast-1.amazonaws.com/{****}
Connected (press CTRL+C to quit)
> {"action":"myAction1","body":{"hello":"websocket1"}}
< {"action":"myAction1","echo":{"hello":"websocket1"}}
> {"action":"myAction2","body":{"hello":"websocket2"}}
< {"action":"myAction2","echo":{"hello":"websocket2"}}
# ⭐ serverless.yml に "myAction3" route 定義がないため action が認識されていない
> {"action":"myAction3","body":{"hello":"websocket3"}}
< {"action":"default","echo":{"hello":"websocket3"}}
> {"action":"myAction4","body":{"hello":"websocket4"}}
< {"action":"default","echo":{"hello":"websocket4"}}