Help us understand the problem. What is going on with this article?

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から始めました。

参考にしたサイト

gossan
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした