TwilioのIPメッセージングをAWS lambdaとs3で実装

:::note alert
2023年5月1日を持ちまして、株式会社KDDIウェブコミュニケーションズのTwilioリセール事業が終了したため、本記事に記載されている内容は正確ではないことを予めご了承ください。
:::

昨年５月のSIGNALイベントで発表されたTwilioのIPメッセージングサービスが、オープンβ状態となっているので、公開されているデモプログラムをAWS Lambda(API Gateway)とS3を使ってAWS用に移植してみました。
IPメッセージングとは、チャットを実現するサービスで、すでにJavaScript、iOS、AndroidのSDKもリリースされています（Androidだけはドキュメントがまだ整備されていません）。
[IPメッセージング](https://jp.twilio.com/ip-messaging)
サンプルプログラムも準備されていますが、今回はそのサンプルプログラムをAWS LambdaとS3に移植することで、サーバーレスアーキテクチャでIPメッセージングを実現したいと思います。
# システム構成図
今回のシステム構成は次のようになります。
![構成図 (2).png](https://qiita-image-store.s3.amazonaws.com/0/86046/20b7ac83-3edd-921e-257a-ccb8d8c6f5cc.png)

① S3から静的WebコンテンツとJavaScriptをダウンロード。
② JavaScriptからIPメッセージングのアクセストークンを取得。
③ 別のクライアントとメッセージの交換。
この図からもわかるように、Lambdaを利用するのはIPメッセージングのアクセストークンを取得するときのみで、実際のメッセージのやり取りはクライアントとTwilioサーバー間で行われます。
# 準備
IPメッセージングを利用するには、以下の情報が必要です。それぞれ手順に従って事前に取得しておいてください。
### twilioアカウント
Twilioのアカウントを持っていない方は、まずはこちらの記事を参考にアカウントを取得してください。
[Twilioアカウントの新規作成](http://blog.twilio.kddi-web.com/2014/06/25/%E6%96%B0%E8%A6%8F%E7%99%BB%E9%8C%B2%E3%81%A7%E3%81%99%E3%81%90%E4%BD%BF%E3%81%88%E3%82%8B-%E3%82%AF%E3%83%A9%E3%82%A6%E3%83%89%E9%9B%BB%E8%A9%B1-sms-api-twilio%E3%81%AE%E6%96%B0%E8%A6%8F%E7%99%BB%E9%8C%B2/)
### IPメッセージング Service SID
Twilioのアカウントが取得できたら、以下の手順に従いIPメッセージングのサービスを生成してください。
1. 以下のURLにアクセスします（要ログイン）。
　 [https://jp.twilio.com/user/account/ip-messaging/services](https://jp.twilio.com/user/account/ip-messaging/services)
2. 「IPメッセージングサービスを作成する」のボタンを押します。
3. フレンドリーネームに「ipmlambda」と入力して「作成」ボタンを押します。
4. IPメッセージングサービスが生成されますので、「Service SID」に表示されている文字列を記録しておきます。
### Twilio API Key
以下の手順でAPIキーを生成します。
1. 以下のURLにアクセスします（要ログイン）。
　 [https://jp.twilio.com/user/account/ip-messaging/dev-tools/api-keys](https://jp.twilio.com/user/account/ip-messaging/dev-tools/api-keys)
2. 「APIキーを作成する」のボタンを押します。
3. フレンドリーネームに「IP Messaging」と入力して「Create API Key」ボタンを押します。
4. API KeyのSidとSecretが表示されるので、どちらも記録しておきます。***特に、API Secretについては、後から表示させることができないので、忘れずに必ず記録しておいてください。***
### AWS Command Line Interfaceのインストール
この記事では、AWS CLIを使って作業をしていきますので、以下の記事を参考に予めインストールしておきます。
[AWS Command Line Interfaceのセットアップ](http://docs.aws.amazon.com/cli/latest/userguide/cli-chap-getting-set-up.html)
[AWS Command Line Interfaceの設定](http://docs.aws.amazon.com/cli/latest/userguide/cli-chap-getting-started.html)

# コードの取得
一連のコードをGitHubにあげてありますので、こちらをダウンロードしてください。

```lang:sh
$ git clone https://github.com/mobilebiz/ipmlambda.git
```
次に、必要なライブラリをnpmコマンドを使ってダウンロードします。

```lang:sh
$ cd ipmlambda
$ npm install
```
*※もしaws-sdkのインストールに管理者権限が必要というエラーが出た場合は、sudoを付けて実行してください。*
# AWS Lambdaのロールの作成
Lambda functionのロールをCLIを使って作成しておきます。
必要なポリシーはダウンロードしたフォルダ内のrole-policy.jsonとして準備してあります。

```lang:role-policy.json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "logs:CreateLogGroup",
        "logs:CreateLogStream",
        "logs:PutLogEvents"
      ],
      "Resource": "arn:aws:logs:*:*:*"
    }
  ]
}
```
このファイルを使ってロールを作成するには、ダウンロードしたファルダ内で以下のコマンドを実行します。

```lang:sh
$ aws iam create-role --role-name ipmlambda_role --assume-role-policy-document file://role-policy.json
```

続けて、作成したロールにCloudWatchLogsへの書き込み権限をアタッチします。

```lang:sh
$ aws iam attach-role-policy --role-name ipmlambda_role --policy-arn "arn:aws:iam::aws:policy/CloudWatchLogsFullAccess"
```

作成されたロールを確認します。

```lang:sh
$ aws iam get-role --role-name ipmlambda_role
```

jsonファイルが表示されます。
後ほど使うので、作成されたロールの中にあるArn情報（arn:aws:iam::xxxxxxxxxx:role/ipmlambda_rolr）を記録しておいてください。

# コーディング
IPメッセージングでは、チャットを実行する前にアクセストークンを取得する必要があります。アクセストークンの取得はサーバー経由で行うことが推奨されているので、今回はこの部分をAWS LambdaとAPI Gatewayを使って実装します。
今回、Lambda上のfunctionとして登録するのは、srcフォルダ内のindex.jsとrandos.js、さらにこれらの内部で利用するライブラリ群です。この内、メインとなるのは「index.js」です。
### src/index.js
準備のところで取得しておいた内容をindex.jsに反映させます。
srcフォルダ内にあるindex.jsをエディタで開き、11行目からの変数に、先ほど取得した内容にそれぞれ書き直してください。

```lang:node.js
var twilio = require('twilio')
  , _accountSid = 'Your Twilio Account SID'　← TwilioのアカウントSID
  , _apiKey = 'Twilio API Key'　← TwilioのAPIのSID
  , _apiSecret = 'Twilio API Secret'　← TwilioのAPIのSecret
  , _serviceSid = 'IP Messaging Service SID'　← IPメッセージングのService SID
  , AccessToken = twilio.AccessToken
  , IpMessagingGrant = AccessToken.IpMessagingGrant
;
```

# ローカルで単体テスト
今回は、ローカルでテストができるように、localtest.jsを作成してあります。
以下のコマンドを実行してみます。

```lang:sh
$ node localtest.js
```

```lang:結果
Process start.
Received event:
 {
  "param": {
    "device": "browser"
  }
}
Process completed.
Test completed with succeed. DaftWendyZimmerman, eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiIsImN0eSI6InR3aWxpby1mcGE7dj0xIn0.eyJqdGkiOiJTSzY1MzQ2YTQ1MzUyOWU3Y2I5ZTkxYTZhNDJlOTJkNzZhLTE0NTMwMTcxMDkiLCJncmFudHMiOnsiaWRlbnRpdHkiOiJEYWZ0V2VuZHlaaW1tZXJtYW4iLCJpcF9tZXNzYWdpbmciOnsic2VydmljZV9zaWQiOiJJUzk0ZGE5ZjYwNGQ3ZjQ2YTZhOGZiOTY4Yjk1MzUyNzhmIiwiZW5kcG9pbnRfaWQiOiJUd2lsaW9DaGF0RGVtbzpEYWZ0V2VuZHlaaW1tZXJtYW46YnJvd3NlciJ9fSwiaWF0IjoxNDUzMDE3MTA5LCJleHAiOjE0NTMwMjA3MDksImlzcyI6IlNLNjUzNDZhNDUzNTI5ZTdjYjllOTFhNmE0MmU5MmQ3NmEiLCJzdWIiOiJBQ2RhMGIxZmNkMDQyMGQzNGRiOGJmMjJkMTA4YWExYjU1In0.rEXZEGgd45oDxYYmM-5-qWOQjvJlfdl_MW4UZBBScyU
```

Test completed with succeed.が表示されていればテストはOKです。もしエラーが出るようであれば、先ほど修正したコードの値が間違っている可能性があります。

# Lambda functionの新規登録
Lambdaに登録するためには、ソースファイルの他に、ライブラリなどをまとめてzipファイルにする必要があります。今回はgulpを使って、一連のビルドを自動化するようにしてあります。

```lang:sh
$ gulp build
```

```lang:結果
[16:57:57] Using gulpfile ~/Documents/workspace/test/ipmlambda/gulpfile.js
[16:57:57] Starting 'build'...
[16:57:57] Starting 'clean'...
[16:57:57] Finished 'clean' after 15 ms
[16:57:57] Starting 'copySrc'...
[16:57:57] Starting 'copyModule'...
[16:57:58] Finished 'copySrc' after 112 ms
[16:57:58] Finished 'copyModule' after 761 ms
[16:57:58] Starting 'zip'...
[16:57:59] Finished 'zip' after 643 ms
[16:57:59] Finished 'build' after 1.43 s
```

エラーが発生せずに終了したことを確認しておきます。このコマンドで、distフォルダ内にipmlambda.zipというファイルが生成されるので、そちらも併せて確認しておきます。

次に、create.shをエディタで開きます。

```lang:create.sh
#!/bin/bash
aws lambda create-function \
--function-name ipmlambda \
--zip-file fileb://./dist/ipmlambda.zip \
--role YOUR_ROLE \
--handler index.handler \
--runtime nodejs \
--region ap-northeast-1
```

5行目のYOUR_ROLEの部分を、先ほど作成したロールのArn情報に書き換えます。

最後に、以下のシェルスクリプトを使ってLambdaに登録します。

```lang:sh
$ ./create.sh
```

```lang:結果
{
    "CodeSha256": "VCrAnUm6QUxYG0ROsIjsuifQjgQcrVvDPzArKQxEdDs=",
    "FunctionName": "ipmlambda",
    "CodeSize": 1544967,
    "MemorySize": 128,
    "FunctionArn": "arn:aws:lambda:ap-northeast-1:XXXXXXXXXX:function:ipmlambda",
    "Version": "$LATEST",
    "Role": "arn:aws:iam::XXXXXXXXXX:role/ipmlambda_role",
    "Timeout": 3,
    "LastModified": "2016-01-17T08:03:47.962+0000",
    "Handler": "index.handler",
    "Runtime": "nodejs",
    "Description": ""
}
```

正常に作成が完了すると、jsonが戻ります。
この中のFunctioArnは後ほど利用するので、メモしておきます。

なお、今回は利用しませんが、もしLambda functionを更新したい場合は、update.shというシェルスクリプトも用意してありますので、そちらを利用してください。

# API Gatewayの設定
Lambda functionの登録が終了したら、次のそのLambda functionを呼び出すAPI Gatewayを準備します。
ここからはAWSのマネージメントコンソールを使います。
マネージメントコンソールにログインしたら、API Gatewayの画面に移動します。
<img width="451" alt="スクリーンショット 2016-01-17 18.44.59.png" src="https://qiita-image-store.s3.amazonaws.com/0/86046/4d21549c-ed6f-f329-d51c-d2aff43b4d81.png">
「Create API」のボタンを押します。

![スクリーンショット 2016-01-17 18.47.27.png](https://qiita-image-store.s3.amazonaws.com/0/86046/570ee83a-ce2e-2d33-912c-75d6dde6ea46.png)
API name欄に「IPMLambda」と入力し、「Create API」ボタンを押します。

![スクリーンショット 2016-01-17 18.50.34.png](https://qiita-image-store.s3.amazonaws.com/0/86046/c1433eb5-9f09-791c-91b9-95017b1e5811.png)
Resources画面が表示されたら、「Create Resource」のボタンを押します。

![スクリーンショット 2016-01-17 18.52.14.png](https://qiita-image-store.s3.amazonaws.com/0/86046/579dd829-a8ed-272f-90fa-e633ca4ebc9f.png)
Resource Name欄に「token」と入力し（Resource Path欄は自動的に入ります）、「Create Resource」ボタンを押します。

![スクリーンショット 2016-01-17 18.55.27.png](https://qiita-image-store.s3.amazonaws.com/0/86046/cbf9d4f0-f951-f6f1-78fe-ab39203b6d37.png)
次に、いま作成した/tokenというリソースが選ばれている状態で、「Create Method」ボタンを押します。

![スクリーンショット 2016-01-17 18.56.18.png](https://qiita-image-store.s3.amazonaws.com/0/86046/4e61b3ed-ae07-0ce0-728f-294b6165b06e.png)
/tokenの下にリストが表示されるので、中から「GET」を選択し、その右側に表示されているチェックマークのアイコンを押して確定します。

![スクリーンショット 2016-01-17 18.58.28.png](https://qiita-image-store.s3.amazonaws.com/0/86046/58765021-e723-803b-e0a0-bdd021686b2d.png)
GETメソッドが選択されている状態で、Integration typeから「Lambda Function」を選択し、Lambda Resionは「ap-northeast-1」、Lambda Function欄に「ipmlambda」を選択したら、「Save]ボタンを押します。

![スクリーンショット 2016-01-17 19.00.51.png](https://qiita-image-store.s3.amazonaws.com/0/86046/d870d03c-d8c7-925a-d01c-40bcb5d196d1.png)
Lambda functionにAPI Gatewayからのアクセス権限を設定してよいかと聞かれるので、「OK」ボタンを押します。

![スクリーンショット 2016-01-17 19.04.57.png](https://qiita-image-store.s3.amazonaws.com/0/86046/9bcf9632-c0bc-2ca9-2dc6-b47afce85bac.png)

### Integration Requestの設定
画面上の「Integration Request」をクリックし、さらに「Mapping Templates」の矢印を展開します。
![スクリーンショット 2016-01-17 23.18.19.png](https://qiita-image-store.s3.amazonaws.com/0/86046/a048139c-4f1c-4818-e308-2762e962c927.png)
画面下にある「Add mapping template」をクリックします。

![スクリーンショット 2016-01-17 23.20.01.png](https://qiita-image-store.s3.amazonaws.com/0/86046/84e043c8-5823-adca-c611-4b466d0fd656.png)
Content-Typeの欄に「application/json」と入力して、右側のチェックアイコンをクリックします。

![スクリーンショット 2016-01-17 23.22.21.png](https://qiita-image-store.s3.amazonaws.com/0/86046/63d09783-e6a3-8516-6fd3-a4d5b24cc8ed.png)
画面右側のInput passthroughの右側にある鉛筆アイコンをクリックします。

![スクリーンショット 2016-01-17 23.26.25.png](https://qiita-image-store.s3.amazonaws.com/0/86046/7866ada3-6067-7a8e-3201-303b391e1dde.png)
リストから「Mapping template」を選択、さらにTemplate欄に以下のコードを記述して、最後に「Mapping template」の右側のチェックアイコンをクリックします。

```lang:Template
{
  "param": {
#foreach( $key in $input.params().querystring.keySet() )
    "$key": "$input.params().querystring.get($key)"#if( $foreach.hasNext ),#end
#end
  }
}
```
これでIntegration Requestの設定は終了です。
### CORSを有効にする
今回、S3に配置した静的コンテンツからAPI Gatewayを呼び出すため、API GatewayにCORS（Cross-Origin Resource Sharing）の設定をしておく必要があります。
画面左のResourcesから、/tokenを選択します。

![スクリーンショット 2016-01-17 23.35.50.png](https://qiita-image-store.s3.amazonaws.com/0/86046/4183a83a-2d8e-26c6-b6d4-ec07acb25c39.png)
「Enable CORS」ボタンを押します。

![スクリーンショット 2016-01-17 23.36.49.png](https://qiita-image-store.s3.amazonaws.com/0/86046/388ebebf-101e-27a1-4d46-ca1fb8553d22.png)
OPTIONメソッドを追加する画面が表示されるので、「Enable CORS and replace CORS headers」ボタンを押します。

![スクリーンショット 2016-01-17 23.38.41.png](https://qiita-image-store.s3.amazonaws.com/0/86046/3fb39f16-3f73-eb30-f49f-da10e6b2d303.png)
確認のダイアログで「Yes, replace existing values」ボタンを押します。

![スクリーンショット 2016-01-17 23.40.03.png](https://qiita-image-store.s3.amazonaws.com/0/86046/1481f541-e65a-577a-0d86-24faa363a72d.png)

以上でCORSの設定は終了です。
### API Gatewayのデプロイ
「Deploy API」ボタンを押します。

![スクリーンショット 2016-01-17 23.41.57.png](https://qiita-image-store.s3.amazonaws.com/0/86046/48232544-3aa2-e094-7da7-bc2c7ff2be41.png)
Deploy APIのダイアログが表示されるので、Deployment stage欄から「New Stage」を選択し、Stage name欄に「prod」と入力、最後に「Deploy」ボタンを押します。

![スクリーンショット 2016-01-17 23.47.04.png](https://qiita-image-store.s3.amazonaws.com/0/86046/f80babdd-988a-fd7e-3638-1de2a536e96c.png)

デプロイが終了したら、Invoke URL欄に表示されているURLをメモしておきます。
### API Gatewayのテスト
ブラウザを開き、作成したAPI Gatewayのurlに***/tokenを付加して***表示してみます。
![スクリーンショット 2016-01-17 23.52.24.png](https://qiita-image-store.s3.amazonaws.com/0/86046/403e58e5-8d8a-6864-7d6c-214793ed9b66.png)
画面のように、identityとtokenがjsonで戻ればAPI Gatewayの設定はすべて完了です。
※/tokenを忘れずに。

# 静的コンテンツの設定
ローカルフォルダのpublic/index.jsをエディタで開きます。
3行目のurlを、先ほどテストしたurl（/tokenを付けたもの）に書き換えます。

```lang:public/index.js
$(function() {
    // AWS API Gateway url
    var url = "https://xxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/prod/token"; ← ココ

    // Get handle to the chat div
    var $chatWindow = $('#messages');
```

# S3の設定
最後に、IPメッセージングのコンテンツをS3にアップロードしていきます。
### バケットの作成
まずは、ユニークになるようなバケット名を決めて、作成をします。

```lang:sh
$ aws s3 mb s3://バケット名 --region ap-northeast-1
```

```lang:結果
make_bucket: s3://バケット名
```

### コンテンツのアップロード
public配下のコンテンツをアップロードします。

```lang:sh
$ cd public
$ aws s3 sync ./ s3://バケット名/ --region ap-northeast-1
```

```lang:結果
upload: ./index.html to s3://バケット名/index.html
upload: ./index.css to s3://バケット名/index.css
upload: ./index.js to s3://バケット名/index.js
```

### 静的ウェブサイトホスティングの設定

```lang:sh
$ aws s3 website s3://バケット名 --index-document index.html --region ap-northeast-1
```

### ポリシーの設定
最後にアップロードしたコンテンツのポリシーを設定します。
ダウンロードしたフォルダ内のpolicy.jsonをエディタで開き、Resourceを自身のバケット名に修正します。

```lang:policy.json
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "PublicReadForGetBucketObjects",
	    "Effect": "Allow",
	    "Principal": "*",
	    "Action": "s3:GetObject",
            "Resource": "arn:aws:s3:::バケット名/*"　← ココ
	}
    ]
}
```

ポリシーを適用します。

```lang:sh
$ cd ..
$ aws s3api put-bucket-policy --bucket バケット名 --policy file://policy.json --region ap-northeast-1
```

# テスト
以上ですべての設定が終了です。
最後にブラウザからS3のコンテンツにアクセスし、チャットが始まることを確認します。

```lang:sh
$ open http://バケット名.s3-website-ap-northeast-1.amazonaws.com
```

![スクリーンショット 2016-01-18 01.24.09.png](https://qiita-image-store.s3.amazonaws.com/0/86046/2c4681dd-3598-c7f5-1a53-608db446fda1.png)

 
# まとめ
TwilioのIPメッセージングを使うと、従来のようにSoketサーバーを構築しなくてもチャットシステムを構築することができます。また、LambdaやS3を活用することで、ランニングコストも抑えることが可能になります。