Posted at

APIGatewayのwebsocketを使ったチャットをServerless Frameworkで作成する


はじめに

AWSによるチャットのサンプルのServerlessFrameworkバージョンを作りました。

APIGatewayのwebsocketを使ったチャットのサンプルです。サンプルではAWS SAMが使われていますが、サンプルをベースにチャットをこれから開発して行くのに、ServerlessFrameworkを使う方が楽そうだなー、と言うモチベーションで作りました。


目標物

下記リンク先のサンプルと同じものをServerlessFrameworkで作ります。

[発表]Amazon API GatewayでWebsocketが利用可能

simple-websockets-chat-app


実装


ディレクトリ構造

.

├── onconnect
│ └── app.js
├── ondisconnect
│ └── app.js
├── sendmessage
│ └── app.js
└── serverless.yml


serverless.yml

service:

name: SimpleChatWebSocket

provider:
name: aws
stage: ${opt:stage, 'dev'}
region: ${opt:region, 'ap-northeast-1'}
runtime: nodejs10.x
iamRoleStatements:
- Effect: Allow
Action:
- dynamodb:*
Resource:
- Fn::GetAtt: [ ConnectionsTable, Arn ]
environment:
TABLE_NAME:
Ref: ConnectionsTable
websocketsApiName: ${self:service.name}
websocketsApiRouteSelectionExpression: $request.body.message

functions:
connect:
handler: onconnect/app.handler
events:
- websocket:
route: $connect
disconnect:
handler: ondisconnect/app.handler
events:
- websocket:
route: $disconnect
sendmessage:
handler: sendmessage/app.handler
events:
- websocket:
route: sendmessage

resources:
Resources:
ConnectionsTable:
Type: AWS::DynamoDB::Table
Properties:
TableName: simplechat_connections
AttributeDefinitions:
- AttributeName: connectionId
AttributeType: S
KeySchema:
- AttributeName: connectionId
KeyType: HASH
ProvisionedThroughput:
ReadCapacityUnits: 5
WriteCapacityUnits: 5
SSESpecification:
SSEEnabled: True


各Lambda

それぞれAWSが用意してくれているサンプルコードそのままです。

リンクのみで済まそうかと思いましたが、

リンク先に変更があると動かなくなる可能性もあると思い現時点で動いたものを貼ります。


onconnect/app.js

simple-websockets-chat-app/onconnect/app.js

var AWS = require("aws-sdk");

AWS.config.update({ region: process.env.AWS_REGION });
var DDB = new AWS.DynamoDB({ apiVersion: "2012-10-08" });

exports.handler = function (event, context, callback) {
var putParams = {
TableName: process.env.TABLE_NAME,
Item: {
connectionId: { S: event.requestContext.connectionId }
}
};

DDB.putItem(putParams, function (err) {
callback(null, {
statusCode: err ? 500 : 200,
body: err ? "Failed to connect: " + JSON.stringify(err) : "Connected."
});
});
};


ondisconnect/app.js

simple-websockets-chat-app/ondisconnect/app.js

var AWS = require("aws-sdk");

AWS.config.update({ region: process.env.AWS_REGION });
var DDB = new AWS.DynamoDB({ apiVersion: "2012-10-08" });

exports.handler = function (event, context, callback) {
var deleteParams = {
TableName: process.env.TABLE_NAME,
Key: {
connectionId: { S: event.requestContext.connectionId }
}
};

DDB.deleteItem(deleteParams, function (err) {
callback(null, {
statusCode: err ? 500 : 200,
body: err ? "Failed to disconnect: " + JSON.stringify(err) : "Disconnected."
});
});
};


sendmessage/app.js

simple-websockets-chat-app/sendmessage/app.js

const AWS = require('aws-sdk');

const ddb = new AWS.DynamoDB.DocumentClient({ apiVersion: '2012-08-10' });

const { TABLE_NAME } = process.env;

exports.handler = async (event, context) => {
let connectionData;

try {
connectionData = await ddb.scan({ TableName: TABLE_NAME, ProjectionExpression: 'connectionId' }).promise();
} catch (e) {
return { statusCode: 500, body: e.stack };
}

const apigwManagementApi = new AWS.ApiGatewayManagementApi({
apiVersion: '2018-11-29',
endpoint: event.requestContext.domainName + '/' + event.requestContext.stage
});

const postData = JSON.parse(event.body).data;

const postCalls = connectionData.Items.map(async ({ connectionId }) => {
try {
await apigwManagementApi.postToConnection({ ConnectionId: connectionId, Data: postData }).promise();
} catch (e) {
if (e.statusCode === 410) {
console.log(`Found stale connection, deleting ${connectionId}`);
await ddb.delete({ TableName: TABLE_NAME, Key: { connectionId } }).promise();
} else {
throw e;
}
}
});

try {
await Promise.all(postCalls);
} catch (e) {
return { statusCode: 500, body: e.stack };
}

return { statusCode: 200, body: 'Data sent.' };
};


デプロイ

$ sls deploy -v

Serverless: Packaging service...
Serverless: Excluding development dependencies...
Serverless: Creating Stack...
Serverless: Checking Stack create progress...
CloudFormation - CREATE_IN_PROGRESS - AWS::CloudFormation::Stack - SimpleChatWebSocket-dev
CloudFormation - CREATE_IN_PROGRESS - AWS::S3::Bucket - ServerlessDeploymentBucket
CloudFormation - CREATE_IN_PROGRESS - AWS::S3::Bucket - ServerlessDeploymentBucket
CloudFormation - CREATE_COMPLETE - AWS::S3::Bucket - ServerlessDeploymentBucket
CloudFormation - CREATE_COMPLETE - AWS::CloudFormation::Stack - SimpleChatWebSocket-dev

~ 略 ~

CloudFormation - UPDATE_COMPLETE - AWS::CloudFormation::Stack - SimpleChatWebSocket-dev
Serverless: Stack update finished...
Service Information
service: SimpleChatWebSocket
stage: dev
region: ap-northeast-1
stack: SimpleChatWebSocket-dev
resources: 24
api keys:
None
endpoints:
wss://2yy181eel2.execute-api.ap-northeast-1.amazonaws.com/dev
functions:
connect: SimpleChatWebSocket-dev-connect
disconnect: SimpleChatWebSocket-dev-disconnect
sendmessage: SimpleChatWebSocket-dev-sendmessage
layers:
None

Stack Outputs
SendmessageLambdaFunctionQualifiedArn: arn:aws:lambda:ap-northeast-1:xxxxxxxxxxxx:function:SimpleChatWebSocket-dev-sendmessage:6
DisconnectLambdaFunctionQualifiedArn: arn:aws:lambda:ap-northeast-1:xxxxxxxxxxxx:function:SimpleChatWebSocket-dev-disconnect:6
ConnectLambdaFunctionQualifiedArn: arn:aws:lambda:ap-northeast-1:xxxxxxxxxxxx:function:SimpleChatWebSocket-dev-connect:6
ServiceEndpointWebsocket: wss://2yy181eel2.execute-api.ap-northeast-1.amazonaws.com/dev
ServerlessDeploymentBucketName: simplechatwebsocket-dev-serverlessdeploymentbucke-rbjshnd8b2xf

Serverless: Run the "serverless" command to setup monitoring, troubleshooting and testing.


テスト

wscatコマンドを使い(インストールはこちらを参照)、出力されたendpointsにアクセスします。

$ wscat -c wss://2yy181eel2.execute-api.ap-northeast-1.amazonaws.com/dev

connected (press CTRL+C to quit)
>

入力待ちになるのでJSON形式でメッセージを送ると…

$ wscat -c wss://2yy181eel2.execute-api.ap-northeast-1.amazonaws.com/dev

connected (press CTRL+C to quit)
> {"message":"sendmessage", "data":"hello world"}
< hello world
>

ちゃんとhello worldが返って来ました!

CloudFormationへの出力などディテールは違いますが実装としてはAWSが用意してくれているサンプルと同じものが作れました。


おわりに

よくあるお客様サポートみたいなチャットに改造して行きたいです。

ServerlessFrameworkの初学者ゆえにそもそものHowToから始めました。

参考にしたサイト