はじめに
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から始めました。
参考にしたサイト