AWS 公式提供のaws-serverless-express を使ってLINE Bot 開発環境を作ってみた

# はじめに
昨年末に初めて[AWS Lambda 環境でLINE Bot を開発しました](http://qiita.com/sumihiro3/items/0bc6cddf4943ca2d18e3)。
簡単なBot を作るだけであれば、Lambda を素で使っても問題ありませんが、LINE Login など他のWEB サービスからの呼び出しを受け付けて動くような少し複雑なBot を作ろうと思うと、ちょっと辛くなってきます。

Lambda 環境でNode.js のexpress のようなフレームワークが使えないかと思って検索していると、[こちらの記事](http://qiita.com/exabugs/items/594d3cd417e99dc29387)を見つけました。

* [AWS 謹製 aws-serverless-express を使って APIGateway + Lambda + Node + Express で RESTful サービスの雛形を最速で作る](http://qiita.com/exabugs/items/594d3cd417e99dc29387)

記事によると、Lambda でexpress が使えるコードが[AWS 公式で（！）提供されている](https://aws.amazon.com/jp/blogs/news/running-express-applications-on-aws-lambda-and-amazon-api-gateway/)とドンピシャの内容でしたので、そうならば使ってみるしかありません。

以下、Mac OSX 環境で構築・開発したものとなります。

# AWS 環境の準備
## AWS 公式コードの取得
[GitHub で公開されているAWS の公式コード](https://github.com/awslabs/aws-serverless-express)を取得して、開発用コードにします。

```開発用コードの準備
$ git clone https://github.com/awslabs/aws-serverless-express.git
$ cp -r ./aws-serverless-express/example ~/aws/samplebot-express
$ cd ~/aws/samplebot-express
```

## アプリ設定
公式コードのREADME.md の手順に従って開発するアプリを設定します。
事前にS3のバケット（コードのデプロイ用）作成と、AWS CLI をインストールしておく必要があります。

```AWSへのアプリ設定
$ cd ~/aws/samplebot-express
$ npm run config -- --account-id="<accountID>" --bucket-name="<bucketName>"  --region="<region>" --function-name="<functionName>"
```

| パラメータ     | 意味        |
|:-------------|:------------|
| accountID    | AWS のアカウントIDです。[AWS コンソールのアカウント画面](https://console.aws.amazon.com/billing/home?#/account)で確認できます。 |
| bucketName   | S3 のバケット名です。このバケットはコードのデプロイだけに使われます。 |
| region       | 作成するLambda アプリ が稼働するリージョン。東京リージョンならap-northeast-1 ですね。 |
| functionName | 作成するLambda アプリ名です。 |

```AWSへのアプリ設定の実行例
$ cd ~/aws/samplebot-express
$ npm run config -- --account-id="xxxxxxxxxxx" --bucket-name="samplebot-express-bucket"  --region="ap-northeast-1" --function-name="samplebot-express"
```

## AWS へのアプリ登録
このコマンドだけでLambda やAPIGateway の登録ができてしまいます。
いや～楽チンですね。

```AWSへのアプリ登録
$ npm run setup
```

コマンド実行後、CloudFormation 画面で、該当のCloudStack のStatus が”CREATE_COMPLETE”になれば登録完了です。

## 動作確認
これでサンプルアプリが動く状態になったので、さっそく動作確認してみましょう。
該当のCloudStack のOutputs にある「ApiUrl」のURL にアクセスします。
![CloudStack のOutputs にある「ApiUrl」](https://qiita-image-store.s3.amazonaws.com/0/154381/29a46ad8-136f-b1a0-32a3-1a53276c5ae7.png "CloudStack のOutputs にある「ApiUrl」")

ちゃんと動いてますね〜！
RESTful API な感じで動いています。
![AWSServerlessExpress.png](https://qiita-image-store.s3.amazonaws.com/0/154381/f36d3968-752a-c6f9-4b63-e4bd97d6b4f2.png "AWSServerlessExpress.png")


# LINE アカウントの準備
AWS の方がひととおり動いたので、次はLINE Bot を動かすために必要なLINE アカウントを取得・設定します。

## アカウント準備
LINE Bot を動かすためにはLINE Business アカウント取得などをしておく必要があります。
アカウント準備のやり方もこの記事に書こうかと思ったのですが、[こちらの記事](http://qiita.com/yoshizaki_kkgk/items/bd4277d3943200beab26)が分かりやすかったので、こちらを参照して設定していただければよいです。

## "Channel Secret"と"Channel Access Token"の設定
LINE Bot 実行に必要な"Channel Secret"と"Channel Access Token"を取得して、Lambda の環境変数に設定しておきます。

### "Channel Secret"と"Channel Access Token"の取得
"Channel Secret"と"Channel Access Token"は、LINE developers の画面から取得できます。
![Channel SecretとChannel Access Tokenを取得](https://qiita-image-store.s3.amazonaws.com/0/154381/1d18727a-980d-1f70-2a05-920031e1d5e2.png "Channel SecretとChannel Access Tokenを取得")
"Channel Secret"は画面上部にある「SHOW」ボタンを押して表示されたものを、"Channel Access Token"は画面下部の「ISSUE」ボタンを押して発行されたものを控えておきます。

### Lambda アプリに環境変数の設定する
Lambda アプリの環境変数に先ほど取得した"Channel Secret"と"Channel Access Token"を設定します。
Lambda コンソールの”Code”欄下部に環境変数を設定する箇所があるのでここに追記して、画面上部の「Save」ボタンを押して設定を反映します。
（「Save」ボタンを押さずに後述の”メッセージ送信元の検証”で引っかかったのはナイショです ;-)）
![Lambda コンソール](https://qiita-image-store.s3.amazonaws.com/0/154381/a0df4e11-0dcf-221d-2475-499372dda01b.png "Lambda コンソール")
それぞれ、以下の変数名で設定しました。

* Channel Secret：LINE_CHANNEL_SECRET
* Channel Access Token：LINE_CHANNEL_ACCESS_TOKEN

### Webhook URL の設定
LINE でBot 宛にメッセージが来た場合や、「お友だち追加」時に実行されるWebhook URL を設定します。
AWS へのLambda アプリ登録時にCloudFormation で構築されたAPI Gateway のURL をLINE developers サイトに登録します。
今回はAPI Gateway へ構築済みURL 配下の"webhook/"パスがLINE から実行されるように設定します。

さきほど"Channel Secret"などを取得したLINE developers サイトの画面から「EDIT」ボタンを押してWebhook URL を入力します。
![Webhook URL を入力](https://qiita-image-store.s3.amazonaws.com/0/154381/d5f209ca-2d5a-8afd-7bc3-62192ea21e1d.png "Webhook URL を入力")

# Bot アプリの開発
LINE アカウントの準備ができたので、次はLambda 上で動かすBot アプリを開発します。
今回は送信されたテキストメッセージの語尾に”でんがな！”を付けて返すだけのBot を作ります。

## node.js のバージョン
node.js のバージョンはLambda と同じバージョン（v4.3.2）にしておきます。

```node.jsのバージョン
$ node -v
v4.3.2
```

## LINE Messaging API 用SDK のインストール
node.js 用のLINE Messaging API 公式SDK は残念ながら提供されていないため、今回は非公式SDK 「[node-line-bot-api](https://github.com/tejitak/node-line-bot-api)」を使っていきます。
「[node-line-bot-api](https://github.com/tejitak/node-line-bot-api)」はnpm でインストールできます。

```「node-line-bot-api」のインストール
$ npm install --save node-line-bot-api
node-line-bot-api@0.1.2 node_modules/node-line-bot-api
└── superagent@2.3.0 (methods@1.1.2, component-emitter@1.2.1, cookiejar@2.1.0, extend@3.0.0, mime@1.3.4, formidable@1.1.1, qs@6.3.0, debug@2.6.0, readable-stream@2.2.2, form-data@1.0.0-rc4)
```

## LINE Messaging API 用SDK の初期化
ここからコードを書いていきます。
app.js の冒頭でSDK「node-line-bot-api」をrequire して、SDK の初期化時にLINE アカウントを設定した際に取得した"Channel Secret"と"Channel Access Token"を渡します。

```js:app.js（一部）
'use strict'
const express = require('express');
const line = require('node-line-bot-api');
const bodyParser = require('body-parser');
const cors = require('cors');
const awsServerlessExpressMiddleware = require('aws-serverless-express/middleware');
const app = express();

// LINE BOT SDK 初期化
line.init({
  accessToken: process.env.LINE_CHANNEL_ACCESS_TOKEN,
  channelSecret: process.env.LINE_CHANNEL_SECRET
});
…（後略）
``` 

なお、Lambda に設定した環境変数は「process.env.${変数名}」で取得できます。
各種API のトークン値などを環境変数に設定しておけば開発用／本番用も簡単に切り分けられますね。

## LINE メッセージ送信元の検証
[LINE Messaging API の公式ページ](https://devdocs.line.me/ja/?shell#webhooks)にもあるとおり、Webhook 受信後に送信元が正しいかを検証する必要があります。
本来はWebhook リクエストヘッダにある署名「X-Line-Signature」値と、リクエストボディとHMAC-SHA256アルゴリズムなどを基に計算した値とを比較して、正しい送信元であるかを検証する必要がありますが、こちらは今回使うSDK「node-line-bot-api」に検証機能が実装されているので、こちらを使います。

```js:app.js（一部）
…（前略）
// 送信元の検証（X-Line-Signature 署名検証）にrawBodyが必要
// @see https://devdocs.line.me/ja/?shell#webhooks
app.use(bodyParser.json({
    verify(req, res, buf) {
        req.rawBody = buf;
    }
}));

app.post('/webhook/', 
         line.validator.validateSignature(),    // <-- 送信元の検証（X-Line-Signature 署名検証）
         function(req, res) {
…（後略）
``` 

## LINE メッセージ受信時の動作を作る
AWS 公式コードのapp.js にはサンプルコードが残っているので、今回使わない箇所は削除して、LINE webhook 用のPOST リクエスト受信部分を作ります。

```js:app.js（一部）
…（前略）
/**
* LINE からのWebhook 受信時に呼ばれる
* LINE developers の「Webhook URL」に設定したURL
* URL:https://xxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/prod/webhook/
*
* @see https://devdocs.line.me/ja/#webhook-event-object
*/
app.post('/webhook/', line.validator.validateSignature(), function(req, res) {
    console.log('LINE message Webhook called!!');
    
    // 受信したメッセージイベントを処理
    const promises = req.body.events.map(function(event) {
        let promise = null;
        if ('message' === event.type) {
            promise = handleMessageEvent(event);
        }
        return promise;
    });
    
    // メッセージごとの処理はPromiseで実行
    Promise
        .all(promises)
        .then(function(value) {
            // 処理が全て正常終了すれば、HTTP STATUS CODE 200を返す
            return res.json({success: true}); })
        .catch(function(error) {
            // 異常終了があれば、HTTP STATUS CODE 500を返す
            console.log('Error!: ', error);
            return res.status(500).json({});
        });
});
         
/**
* メッセージ受信時に呼ばれる
* @param event  {JSON}  Message Event
* @return   Promise（Reply message）
*
* @see https://devdocs.line.me/ja/#webhook-event-object
* @see https://devdocs.line.me/ja/#reply-message
*/
function handleMessageEvent(event)
{
    console.log('handleMessageEvent function called!!');
    const messageType = event.message.type;
    
    let promise = null;
    if ('text' === messageType) {
        // テキストメッセージであれば、メッセージの語尾に”でんがな！”だけ付けて返す
        promise = line.client.replyMessage({
            replyToken: event.replyToken,
            messages: [{
                type: 'text',
                text: event.message.text + 'でんがな！'
            }]
        });
    } else if ('location' === messageType || 'sticker' === messageType) {
        // 位置情報とスタンプ（公式スタンプのみ）は、そのまま返す
        promise = line.client.replyMessage({
            replyToken: event.replyToken,
            messages: [event.message]
        });
    } else {
        // それ以外は、適当なメッセージを返す
        promise = line.client.replyMessage({
            replyToken: event.replyToken,
            messages: [{
                type: 'text',
                text: 'それはあかんでぇ…'
            }]
        });
    }
    return promise;
}
…（後略）
``` 

## AWS へのアップロード
簡単ですがBot アプリをつくったのでAWS へアップロードします。
素のままアップロードする場合、アプリリソースをZip ファイルに固めてアップロードする必要がありますが、今回の場合はたったひとつのコマンドでアップロード出来るようにpackage.json で設定されています。
素晴らしいですねぇ。

```AWSへのアップロード
$ npm run package-upload-update-function
``` 

## いざ実行！
これでひととおりできたので実行してみましょう！
LINE アプリでBot を友だち追加して、Bot とのトーク画面でメッセージを送るだけです。

動きました！
![SampleBot01.png](https://qiita-image-store.s3.amazonaws.com/0/154381/ed6ef038-484f-8ccf-694e-193db1a7e6f0.png "SampleBot01.png")
テキストメッセージの場合は”でんがな！”が末尾について、位置情報とスタンプの場合はそのまま返信できていますね。

![SampleBot02.png](https://qiita-image-store.s3.amazonaws.com/0/154381/af115a5a-76ae-d819-7b4e-60df90b88a54.png "SampleBot02.png")
画像の場合は適当なテキストメッセージを返信できています :-)

こんな感じで簡単にサーバーレスなLINE Bot を開発できます。素晴らしく簡単ですねぇ。

# その他ハマったこと
この公式コードを使ってアプリを作っていると幾つかハマったことがあったので載せておきます。

## アプリにファイルを追加する場合
AWSへのアップロードするタスクがpackage.json で設定されていますが、サンプルで準備されているファイル／ディレクトリしか記載されていないので、新たにjs ファイルを追加した場合はpackage.json の「package-function」（Zip ファイルを作る）タスクに追記する必要があります。

```json:package.json（一部）
// 変更前
// "package-function": "zip -q -r lambda-function.zip lambda.js app.js index.html node_module",
// ↓
// 変更後 末尾に「lib」ディレクトリを追加
"package-function": "zip -q -r lambda-function.zip lambda.js app.js index.html node_modules lib",
```

## AWS 公式コードを使って複数のアプリを作成する場合
複数のexpress アプリを登録する場合、そのままでは複数登録できないので、次の設定をアプリごとに変えておきます。

#### package.json 
ファイルの８行目付近にある"cloudFormationStackName" の値を変えておく必要があります。
"cloudFormationStackName" がアカウント内で重複すると登録できません。

```json:package.json（一部）
 {
   "name": "aws-serverless-express-example",
   "version": "1.1.0",
   "description": "Example application for running a Node Express app on AWS Lambda using Amazon API Gateway.",
   "main": "lambda.js",
   "config": {
     "s3BucketName": "samplebot-express-bucket",
     "cloudFormationStackName": "AwsServerlessExpressStackSampleBot", // <-- ここ
     "region": "ap-northeast-1"
   }
```

#### simple-proxy-api.yaml 
ファイルの4行目付近にある "title" を変えておくとAPI Gateway のリソース名称が変わるので区別がつきやすいです。

``` simple-proxy-api.yaml（一部）
 ---
 swagger: 2.0
 info:
   title: SampleBotExpressApi // <-- ここ
 basePath: /prod
```

# おわりに
こんなにも簡単にWeb アプリを作ることが出来るなんて、ほんと良い世の中になりましたねぇ（遠い目

ちなみに、今回紹介した環境を使って「[ホテルBot](http://hotelbot.app-labo.net/)」というLINE Bot を開発しています。
[別の記事にも掲載しました](http://qiita.com/sumihiro3/items/ec310ea61d629c1daee9)が、Bot とのトークで”札幌で明日から２泊、一万円以下の禁煙ルームで朝食あり” のようにテキストメッセージで条件指定するだけで、条件にあったホテルの検索結果を返してくれ、そのまま予約までできるLINE Botです。
出張が多く頻繁にホテル予約をするので、これからも使い勝手がよくなるように改良していきたいと思っています。
私のように出張族の方や、旅行好きな方は是非お試しください。

LINE アプリで下のQRコードを読み取って、Bot を友だち追加すればすぐに使えます。
<img height="200" src="http://qr-official.line.me/L/cqnqVP5X5_.png">

このページをスマートフォンで見ている場合は、下のボタンをタップしてLINE アプリで「友だち追加」してください。
<a href="https://line.me/R/ti/p/%40ynv4372o"><img height="64" border="0" alt="友だち追加" src="https://scdn.line-apps.com/n/line_add_friends/btn/ja.png"></a>

それでは皆さんもよいLINE Bot 開発を！

# 参考記事など

* [AWS Lambda と Amazon API Gateway で Express アプリケーションを実行](https://aws.amazon.com/jp/blogs/news/running-express-applications-on-aws-lambda-and-amazon-api-gateway/)
* [aws-serverless-express](https://github.com/awslabs/aws-serverless-express)
* [AWS 謹製 aws-serverless-express を使って APIGateway + Lambda + Node + Express で RESTful サービスの雛形を最速で作る](http://qiita.com/exabugs/items/594d3cd417e99dc29387)
* [LINE BOTの作り方を世界一わかりやすく解説（１）【アカウント準備編】](http://qiita.com/yoshizaki_kkgk/items/bd4277d3943200beab26)
* [いらすとや](http://www.irasutoya.com/) （いつもお世話になっています！）
* [ホテルBot の使い方](http://qiita.com/sumihiro3/items/ec310ea61d629c1daee9)
* [ホテルBot](http://hotelbot.app-labo.net/)