Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
11
Help us understand the problem. What is going on with this article?
@gossan

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

More than 1 year has passed since last update.

はじめに

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

参考にしたサイト

11
Help us understand the problem. What is going on with this article?
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
gossan
個人的見解であり、所属する組織とは関係ありません。

Comments

No comments
Sign up for free and join this conversation.
Sign Up
If you already have a Qiita account Login
11
Help us understand the problem. What is going on with this article?