LINE Bot を AWSを使ってシステム構築してみた。

技術系の記事投稿先を自前のBlogに移しました。初心者向け,AWSなど幅広いTechジャンルで記事投稿がんばってますので、よろしかったらこちらも参照ください！

https://hiyuzawa.jp/


<img src="https://i.gyazo.com/1cb323bd58181a5ec13419d0d707ea32.png">


LINE Bot API が公開され一般のデベロッパにも公開されたので使ってみたメモ。
申し込みはこちらから. ~~先着10,000名ですがまだ申し込みを受付けている模様(2016/4/14現在)~~ 
~~上限に達したようです(2016/4/16)~~
[追加受付が開始されたようです。](http://linecorp.com/ja/pr/news/ja/2016/1340)(2016/4/26)
https://business.line.me/services/products/4/introduction

## LINE Bot API
BotはユーザがBotアカウントとお友達になり、Botに話しかければ(メッセージを送信すれば)、メッセージに応じてレスを返しくれるものです。LINE Bot API は LINE が Botを開発したいデベロッパ向けに提供するプラットフォームであり、これを利用すれば LINE 上でBotを作ることができます。

<img src="https://i.gyazo.com/ab452da26fe80e4fa12541b3f608ba02.png">


LINE がインフラとして提供している部分は

* ユーザがLINE Botへ送信したメッセージをプログラマが用意したサーバに転送してくれる(Callback)部分①
* デベロッパが生成したユーザへのレスを発信者に返信するためのAPI②

となり、①で受信したメッセージに応じてレスポンスを生成し②でLINEサーバに送信すればユーザへ応答メッセージを返すことができます。

[Bot API Overview - LINE developers](https://developers.line.me/bot-api/overview)

## Bot API Trial Account

今回、一般に募集したBot API Trial Account で利用できる仕様(制限)は以下のとおりです

* Bot アカウントはアカウント1つにつき1つまで
* Bot アカウントに登録できるフレンド登録は50人まで (申請により上限を5000人まで拡張可能になりました) 
* Callback API (①に相当する部分)について
	* HTTPS (SSL証明書に制限あり)
	* POSTで呼び出される
* Message Response API (②に相当する部分)について
	* HTTPS にてPOST
	* 送信側のサーバのIPアドレスを事前にLINE側のホワイトリストに登録する必要があります(IP指定 or ネットワークアドレス(サブネット24-30))

[Bot API Trial Account - LINE developers](https://developers.line.me/type-of-accounts/bot-api-trial)

## Bot システムの設計

### 単純なシステム設計

Bot システムは単純には以下の機能に分割されます

1. (ユーザからメッセージが含まれる)リクエストを受信する
2. メッセージに応じたレスポンスを生成する
3. レスポンスを送信する
 
LINE Botでは 1. のリクエストはLINEサーバより HTTPSのPOSTにて送信されてきます。よって1.を処理するためのHTTPSサーバが必要です。一般のHTTPサーバであればリクエストに対してレスポンス(HTMLなど)を応答するのでレスポンス(3.)をそこで返すような気になりますが違います。1.の受信の応答はLINEサーバに対して「受信が成功したよ」という応答のみです。

3 のレスポンスの送信は LINE Bot API に対して POST リクエストを送信します。
ユーザからのメッセージから簡単な処理(単純な"おうむ返し",あらかじめ用意した定型文から選択して応答)などであれば、1台のサーバでリクエストを受信するHTTPSサーバの受信処理の中で、レスポンス生成 & レスポンス送信まで行うことも可能です。

<img src="https://i.gyazo.com/2647da24ee2f79d76c98c638115eaaf4.png"> 

上のような形です。2.の黄色の部分がリクエストを受信したHTTPSのプロセス内の処理です。この中でレスポンスメッセージを生成し送信のHTTPSリクエスト処理も行う形です。

### 単純なシステムの問題点

個人で楽しむ程度、またトライアルアカウントのBotフレンド登録上限50人程度であれば、上の構成でも全く問題無いでしょう。大規模なサービスを想定しなければならないケースでは

* 大量のメッセージが同時にくる場合
* レスポンスの生成に複雑な処理が必要な場合

に上記の単純なシステムではシステム負荷の問題、ユーザに対して思った以上にやりとりに時間よ要するBotになる可能性があります。この辺について非常にわかりやすく書かれている記事がありましたので紹介します。

[大量メッセージが来ても安心なLINE BOTサーバのアーキテクチャ - Qiita](http://qiita.com/yoichiro@github/items/6d4c7309210af20a5c8f)

### 問題点を解消したシステム設計の一例

問題点を解消するにはまず、リクエストとレスポンスの処理を非同期にすることが必要です。非同期とはメッセージの受信と送信を別のプログラムで行います。

* 受信プログラムの役割

	メッセージの受信処理は受信内容を最低限の処理に留め保存(当然レスポンスに必要な情報は保存する）、それが終わったら即時に「受信を正しく受け付けました」と応答します (1)。送信が非同期で別途行われるためレスポンスメッセージの生成等の負荷がかかる処理をここでやるメリットはありません。「受け付けました」と処理をできるだけ早く終わらすことが重要です。
	

* 送信プログラムの役割

	保存されたリクエストが無いかを常に監視します。リクエストを発見した場合はそれを取り出し、レスポンスメッセージを生成し、送信を行います(2.)。
	
<img src="https://i.gyazo.com/e8faecd147638de855832be5456028e0.png">


ここでさらっと書きましたが悩むところは２点あります。

1. メッセージの保存ってどういう管理(ストレージがよいか?)
	一般的にFIFO(First In First Out)の処理ができるシステムを選びます（Queue キューイング)の機能を持つともいえるます.
	処理が終わったらそのデータは必要ありませんのでその点も考慮が必要です
	
2. 保存されたリクエストの監視ってどうするのが適切なのか?
	こちらは様々なプログラムが考えられます、常駐型(デーモン)のプログラムを用意し常に処理をすべきデータの有無を監視します。または定期実行(cron)で起動されそこでデータを監視します。さらには、保存側で保存された時にイベントを発生させそれに応じてプログラムが起動され処理を実行する形式もあります。いずれも選定したストレージの機能によって実現可能なことが異なります。また、ユーザへのレスポンスの反応時間の許容によっても設計の考慮が必要です（Botなので反応が遅いのは使い物にならないですからね...)

## AWS による LINE Bot システムの設計 

さて、前置きが長きなりましたが AWS を使って LINE Bot システムを作っていきます。せっかくなので前述の問題点を考慮に入れた下記のシステム構成を目指します。

<img src="https://i.gyazo.com/1cb323bd58181a5ec13419d0d707ea32.png">

このシステム構成のポイントとなる部分は

* AWS ap-northeast(東京リージョン)に VPC(Public/Private Subnet)を構成し多少真面目にネットワーク構成も考える
* サーバレス(EC2は使用しない)で Private subnet 内で駆動する Lambda を積極的に利用する
* LINEサーバからのリクエストを受信する部分は API Gateway を利用する (HTTPS で受信可能)
* メッセージの保存は Dynamo DB を利用する
* メッセージの取り出しは Dynamo DB のデータ更新時に発動される Stream & Trigger を利用する
	* Dynamo DBにレコードが追加されたら Lambda が呼び出される
* LINEサーバへのレスポンスメッセージの送信はPublic subnet の NAT Gateway を通過させる
	* Nat Gateway に Public IP (EIP) を与えることで LINE サーバへの From IP が固定されこれをホワイトリストに申請できる

ストレージとして別途考えられるのは SQS でありこれがFIFOの理想的なものであるが、キューの取り出しに対して Lambda のイベント起動が対応していないので別途その点(常駐プログラムの作成等)を考慮する必要があるので今回は DynamoDB を利用する

### 1.ネットワークの構築

<img src="https://i.gyazo.com/1822be3e213dbcf8042a2699653a874b.png"> 
AWSのVPCを使います。完成系はこんな感じです。
<img src="https://i.gyazo.com/298ab75f22cf4a098f4ce638b55d2ea3.png">

* VPCを作成します。
	* VPC CIDR (10.0.0.0/16)

	<img src="https://i.gyazo.com/2e13748470ccea95a3bb4dfb1400c0fb.png">

* VPC内にSubnetを2つ作成します
	* 10.0.0.0/24 (Public)
	* 10.0.1.0/24 (Private)

	<img src="https://i.gyazo.com/667b7db0bbbbe584f82cd32bdb3ec82a.png">

* Internet Gatewayを作成します
	* 作成しVPCにアタッチします (状態がAttachedになっていることを確認)

	<img src="https://i.gyazo.com/d9ac2933465fa3d36a8e9de568591a62.png">

* VPCのルートテーブルを2つ作成し Subnetそれぞれに適応させます
	* Public ルートテーブル 
		* デフォルトルート(0.0.0.0/0) を 先ほど作った Internet Gateway に設定します ①
	* Private ルートテーブル
		*  デフォルトルート(0.0.0.0/0) を NAT Gateway に設定します② (あとから作るので GW作成後に追加セットしてください!)
	* どちらもルートテーブルをサブネットに関連付けを忘れずにすること。

	<img src="https://i.gyazo.com/bd1d9ce2d18cbd0d418cb8dca76d73e7.png">
	
* NAT Gateway を作成
	* __Publicセグメント__に NAT Gateway を作成します
		* Internget Gateway が付与された Publicルートテーブル作成(前述)後にしないと作成が失敗します
		* 新しいEIPの作成でPublicIPを取得し Nat Gatewayに割り当てます
		* <font color="red">起動した時点より$0.062/hの料金(2016/4/18現在)が発生し続けます</font>
		* <font color="red">起動に失敗した。もしくはNAT Gatewayを削除する際は、忘れずにEIPも別途削除すること(一緒には削除されない&未利用のEIPにはリザーブドの料金が発生し始めます)</font>
		* NAT Gatewayが正しく起動したらPrivate Subnet のルートテーブル②をセットして下さい。

	<img src="https://i.gyazo.com/480f36cabe09ef946aeb00865d43c51f.png">

以上でネットワークの準備は終了です。

	* ネットワークが正しく構築されているかは以下のように確認することも可能です
		1. Public Subnet に EC2サーバ (PublicIP付き) を起動します (いわゆる踏み台サーバ)
		2. Private Subnet に EC2サーバ を起動します
		3. 1.で起動したEC2に SSH ログインします.
		4. 1.のサーバから外部に通信してみます (ex. $ curl http://www.yahoo.co.jp )
			* 正しく通信できるはずです (Internet Gateway を経由して外部インターネットと通信できています)
		5. 1.のサーバから2.のサーバにログインします
			* SSH-Keyを1.のサーバにSCPしそれを利用する必要があります(もしくはSSH転送で一気に2.へログイン)
		6. 2.のサーバから外部に通信してみます (ex. $ curl http://www.yahoo.co.jp )
			* 正しく通信できるはずです (Nat Gateway から Internet Gateway を経由して外部インターネットと通信できています)

### 2-1. メッセージの受信 - 準備

LINEサーバからメッセージが受信できるところまでを作成します. 下記のような感じです。赤いところを作ります.

<img src="https://i.gyazo.com/2ec29bc53f930ad62fe2a8140910e56b.png">

* Botの準備
	* Line developerにログインし Channels を選択すると Bot の設定ができますので事前にやっときます. Edit できる項目は
		* Name : Bot の名前です。フレンドからはこれが見えます
		* App Icon : いわゆるLineのアイコンです。(設定必須ではありません)
		* Callback URL : Bot にメッセージが届いた時のデベロッパー側の呼び出しURLです。ここにHTTPS/POSTされます。
	* 非編集の項目として以下の３つのパラメータが与えられます。
		* Channel ID
		* Channel Secret
		* MID
* Bot とのフレンド登録
	* 設定画面にあるQRコードを用いて一般LINEアカウトでこのBotとフレンド関係を結び、メッセージを送信できる状態にしときます。

この状態でBotへメッセージを送信すると既読にはなるものの応答がありません。Callbackが定義されていませんのでLINEサーバはメッセージを受信するものの処理をすることなくそこで終了します。

### 2-2. メッセージの受信 - Lambda 関数の準備

<img src="https://i.gyazo.com/1faa8e61f8f4d91787450c0382964494.png">

関数は以下のとおりnodejsで書きます。とりあえず、LINEサーバからメッセージが受信できることを確認したいだけなのでシンプルに定義します。

```node
exports.handler = function(event, context) {
    console.log('Received event:', JSON.stringify(event, null, 2));
};
```

* Configuration - Role には IAM にて以下のポリシーがアタッチされたRoleを作成して割り当てます
	<img src="https://i.gyazo.com/35a21fae268fb32cbce14f13a3b8c837.png">
	
	<font color="red">今回はRoleの詳細は話は本質と離れるので緩すぎるrole設定で進めますが、本番を想定した場合は最低限の権限を付与したroleを作成すべきです。必要とする権限は以下です。</font>
	* <font color="red">VPC 内でLambda を稼働させる権限</font>
	* <font color="red">Dynamo DBにLambda からアクセスできる権限</font>

* Advanced settings(Configuration) 
	* VPC を作成したVPC (10.0.0.0/16)を選択
	* SubnetsをPrivate Subnet (10.0.1.0/24)を選択
	
	<img src="https://i.gyazo.com/d0d2b51b2d72d47a1c9efe11a67670aa.png"> 
	2つ以上のSubnetを選択し可用性を高めるべきだとの警告がでますが今回はそのままでok.

* Testを実施
	* Hello world のテストデータでテストをしてみる。
	* Lambda Monitoring画面の Log Output に Lambdaに与えた入力値がそのまま出力されています。
	* CloudWatchログへも同じLogデータが送信され保存されていることを右上の「View log in CloudWatch」から確認します。

	<img src="https://i.gyazo.com/dc98892d9d9607e5ce127c32f3705d94.png">
	

### 2-3. メッセージの受信 - API Gateway

<img src="https://i.gyazo.com/76a3c505066ccb3752eaad8675021490.png">

API Gateway はAPIフロントエンドを構成するAWSのサービス。リバースプロキシとしてバックエンドAPIにリクエストを転送する使い方が一般的ですが、バックエンドにLambdaファンクションも用いることができるのでこれを使います。

　　<img src="https://i.gyazo.com/e365c5bd2109b0457ae7cbb0e03a373c.png">

* Resouce / Method を追加し POSTのエントリーポイントを作成する
	* 下の例は /receiver に POST をセットした形
	* Integration Request の詳細にて
		* Integration Type : Lambda Function
		* Lambda function : 2-2 で作成した Lambda 関数を指定
	* セットが終わったらDeployを実行する (prod)

以上により、以下のようなHTTPSのエントリーポイントが作成されます (xxxxxxxはAWSアカウント環境により異なる)

```
https://xxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/prod/receiver
``` 
これをLINE developerの Botの設定ページのCallbackに上のHTTPSをセットします。
 <font color="red">(現在なぜか:443をドメインの末尾につけてHTTPSアピールをせねばエラーになる)</font>
 
　　<img src="https://i.gyazo.com/3567b002dab7d03a46f5c7dab6127fee.png">
 
この状態でフレンド登録した一般のLINEアカウントよりBotへメッセージを送ってみます。
LINEサーバからCallbackへメッセージが送られ API Gateway経由で Lambda 関数にリクエストが届きます。
今回 Lambda関数ではリクエストをそのままログに落とすだけの処理なのでCloodWatch Logを確認するとLINEからの情報が届いていることが確認できます。( result.content.text にメッセージが入っている )
　　<img src="https://i.gyazo.com/3f7d2885eb6ce137470345695108ac66.png">

<font color="red">現在、LINE側のCallbackに設定した後にCallbackが実際に呼ばれるまで数時間要します.
(少なくとも私の環境では1時間ぐらい呼び出されないことで悩んだ後に6時間ぐらい放置して再度試してみると呼び出しが開始されました)</font>

### 3-1. メッセージの保存 - DynamoDB準備

<img src="https://i.gyazo.com/6103bbba1d23cd723e983748e8f8efbe.png">

受信したメッセージを保存する部分を作成する。本クションで下図の赤い部分を作成する。

<img src="https://i.gyazo.com/b536e2ef0e77daf1dd4677bf444e014a.png">

メッセージの保存として今回 Dynamo DBを利用します。Dynamo DBは AWSが提供する No SQL型のストレージです。 詳細の説明は割愛し今回の利用する部分だけを紹介します. このセクションで下図の赤い部分を行う。

* テーブルの作成
	* テーブル名: LineBot
	* プライマリーキー: id(文字列)
	* ストリームの設定が追加で必要だが後述する

### 3-2. メッセージの保存

2-2 で作成した受信用のLambda関数を以下のように変更する. 見ればわかるが受信したリクエストから

* id: メッセージを一意に定めるID
* text: メッセージ本文
* from: 送信者ID

を取得しDynamoDBにputItemしている。 (putItemでは型を指定したJSONをputする必要がある点が注意)

```nodejs
var aws = require('aws-sdk');
var dynamodb = new aws.DynamoDB({region: 'ap-northeast-1'});

exports.handler = function(event, context) {
    console.log('Received event:', JSON.stringify(event, null, 2));
    var msg = event.result[0];
    dynamodb.putItem(
        {
            "TableName": "LineBot",
            "Item": {
                "id": {"S": msg.content.id},
                "text": {"S": msg.content.text},
                "from": {"S": msg.content.from}
            }
        }, 
        function (err, data) {
            if (err) {
                console.log(err, err.stack);
            } else {
                console.log(data);
            }
        }
    );
};
```

これをsaveし、メッセージをBotに送信するとDynamoDBにデータが挿入されていることが確認できるる(AWSコンソールより確認出来る)

  <img src="https://i.gyazo.com/29841f232639080f4fb86f5e763b0f9e.png">
  
  
### 4-1. メッセージの送信 - ストリームの準備

このセクションではDynamoDBに挿入されたデータを取得し処理を行う部分を作成する。下図の赤い部分を作成する。

<img src="https://i.gyazo.com/96fb9ed1448f5a8e4217f25f357ae093.png">

DynamoDBにレコードが追加されたことをイベントとして通知してくれる仕組みがAWSには存在する。今回これを利用する。

* DynamoDBのストリームを作成する
	* 処理を行うLambda関数を新規に作成する. DynamoDBの管理画面で「ストリームの管理」を選択する。
	* 行の変更時にストリームが流れるが流れるデータの種別を選択する(新旧イメージを選択する)
		* 行は常に新規に入るだけなので「新しいイメージを」選択してもOK

	<img src="https://i.gyazo.com/d3471388cd7ccb8ee7e5e9421dd3cf8c.png">
	

* Lambda 関数を新規に作成する.
	* 設定内容は2-2で作成した受信用の関数と同様(VPC/Subnet/role)
	* 以下のようなコードとする

```nodejs
exports.handler = function(event, context) {
    event.Records.forEach(function(record) {
        console.log(record.eventName);
        console.log("DynamoDB Record: %j", record.dynamodb);
    });
};
```
 Dynamoのトリガーは複数の更新通知を一括で呼び出されることがあるので  Records のArrayを forEachで繰り返し処理する

* DynamoDBのトリガーとして上で作成したLambdaをセットする

 <img src="https://i.gyazo.com/29b09853cff0d29c0bd000f056a62f9a.png">

ここまで準備が完了し、再びLINEでBot向けにメッセージを送信するとDynamoDBに入ったデータがStreamingにて更新通知が流れ、それをLambda関数がトリガーとして拾って console.logにそのデータを出力します.

出力したlogをCloudWatchで確認するとたしかに3-2でDynamoDBに送信されたデータが作成したLambda関数で取得さあれている。

<img src="https://i.gyazo.com/2f239b326899cdc9a1b2457fb4d499fb.png">


### 4-2. メッツセージの送信

さて、ようやく最後の工程のメッセージの送信を行います。下図でいうところの赤い部分です。

<img src="https://i.gyazo.com/2cb7f34df480143b020a400af15e2757.png">

4-1で作成した lambda 関数の中でLINEサーバへ送信する処理を追記します. LINEのサーバへ送信する仕様は以下の通りです
	* Endpoint URL: https://trialbot-api.line.me/v1/events
	* HTTP method : POST
	* Required request header: ドキュメント参照

などいろいろな仕様があります。詳しくは [api-reference - LINE developer](https://developers.line.me/bot-api/api-reference) を参照してください。

また、以下の記事がよくまとまってましたので紹介します
* [LINE BOT API Trialでできる全ての事を試してみた - Qiita](http://qiita.com/betchi/items/8e5417dbf20a62f2239d)

```
var https = require('https');

exports.handler = function(event, context) {
    event.Records.forEach(function(record) {
        console.log(record.eventName);
        console.log("DynamoDB Record: %j", record.dynamodb);
        var resBody = JSON.stringify({
            to: [record.dynamodb.NewImage.from.S],
            toChannel: 1383378250,
            eventType: "138311608800106203",
            content: {
                contentType: 1,
                toType: 1,
                text: record.dynamodb.NewImage.text.S + "!"
            }
        });
        var url ='https://trialbot-api.line.me/v1/events';
        var opts = {
            host: 'trialbot-api.line.me',
            path: '/v1/events',
            headers: {
                "Content-type": "application/json; charset=UTF-8",
                "X-Line-ChannelID": "[YOUR CHANNEL ID]",
                "X-Line-ChannelSecret": "[YOUR CHANNEL SECRET]",
                "X-Line-Trusted-User-With-ACL": "[YOUR MID]"
            },
            method: 'POST'
        };
        var req = https.request(opts, function(res){
                res.on('data', function(chunk){
                    console.log(chunk.toString());
                }).on('error', function(e){
                    console.log('ERROR: '+ e.stack);
                });
            });
        req.write(resBody);
        req.end();
    });
};
```

headerの3つのパラメータは自分のBotアカウントで発行された定数をセットします。

さて、この状態でBotへリクエストします。一つ作業を忘れているので応答がされません。4-1のLambdaのログをCloudWatchで見てみると

 <img src="https://i.gyazo.com/8dd04ae180b64cea9e0db8c23025e2cf.png">

というのが記録されていると思います(IPは環境により異なる). LINE Bot APIの条件にアクセスするサーバのIPを事前にWhite List 登録が必要でした。それをせずにリクエストを投げると上記のようなBat Request (not allow access)のメッセージが応答されます。

また、このIPアドレスは 1. で構築したネットワーク内の Nat Gatwayに付与された PublicIP (EIP)と一致しているはずです。このことからもPrivateセグメントにあるLambda関数のインターネット向けの通信は Nat Gatway を中継して出ていることがわかります。

LINE developer のWhite List 設定を開き IPを登録します。数分たては反映されるはずです。

<img src="https://i.gyazo.com/7fd6c3ad736e03c7f09189e8b7e4686d.png">

Bot 向けにメッセージを送信してみると応答が返ってくるはずです。おつかれさまでした。

<img src="https://i.gyazo.com/d0ef13f0a102485325e49f3e7008706f.png"> <img src="https://i.gyazo.com/1cb323bd58181a5ec13419d0d707ea32.png">

### 5. まとめ

* Line Bot の説明か AWS の紹介かわからなくなった記事になってしまったがどちらもビギナーな方に有益な記事になれば良いと思います。
* 今回はシステム構築方法を主眼に置いた記事だったのでBotとしてはリクエストを"おうむ返し"するのみの面白くないものだったが、Botとしてウケるはそこが重要ですね。Traialアカウントでも location / スタンプ などメッセージのやり取りも可能なのでそちらも試してみたい
* FacebookもBotサービスを公開し、関連記事もでてきている。そちらも試してみたい。
* <font color="red">上でも書きましたが本チュートリアルで利用したNAT Gatewayは時間課金のAWSソリューションです. 作った後の後片付けはちゃんとしましょう</font> 参考までに2週間ぐらい放置するとこれぐらいの請求になります...

    <img src="https://i.gyazo.com/03d1a0d113213ec7de5d192602a98b20.png">

技術系の記事投稿先を自前のBlogに移しました。初心者向け,AWSなど幅広いTechジャンルで記事投稿がんばってますので、よろしかったらこちらも参照ください！

https://hiyuzawa.jp/