コストレスアーキテクチャでLINE Botを作ろうとして苦戦した話


この記事は「ボット (Bot) Advent Calendar 2017」 の22日目の記事です。

今月、とあるイベントのサブ企画として、
**「イベント用LINEアカウントに写真を投稿すると記念フレームが付いて返ってくる！」**
というLINE Botを作って公開しました。

主に設計面で困ったことを中心に書こうと思います。

# コストレスアーキテクチャとは！！！！
- さっき考えた造語です
- サーバレスアーキテクチャとほぼ同義です
- 料金がかかりそうなものは極力使わないという一種の縛りプレイみたいなものです

# 経緯とか方針とか考えとか技術選定とか
- 何か面白い企画できない？とイベント主催者に相談されたのがきっかけ
- 2~300人の集客が想定された
- 若者ばかりだったので、LINEならみんな使ってくれそうだと思った
- ほぼ身内だし、最悪コケてもしゃあないくらいの軽い気持ち
- なんか写真とか使って記念になりそうなことがしたい
- 実装よりも企画やフレームのデザイン作りに時間をかけたい
- 単発だしとりあえず動かせればおk
- 単発だしデプロイとかも泥臭くていい
- ケチだからお金は極力かけたくない(重要)

**→AWS　Lambda + node.jsで画像加工のLINEBotを作ろう。
動くものを安くサクッと仕上げよう。**

# 構成について

当初、以下のような単純なものを想定していました。
やることは投稿画像をトリミングしてからフレームを合成するだけなので、
APIGateWayから実行したLambda内で処理を完結させ、
Messaging APIのReplyAPIで返信するつもりでした。

※Messaging APIはLINEが提供するAPIで、無料のDeveloperプランがあります
https://developers.line.me/ja/services/messaging-api/

![Untitled Diagram (2).png](https://qiita-image-store.s3.amazonaws.com/0/79236/11c0526e-4d14-1e80-2709-7f241f42aad1.png)


さぁ作るかと思ったのも束の間、いくつか課題が浮き彫りになりました。

- Replyに必要なReplyTokenは数秒(？)で切れてしまう
- Pushで画像を返す場合はURL指定が必須
- Messaging APIのDeveloper Trialプランは友達50人まで

## Replyに必要なReplyTokenは数秒(？)で切れてしまう

MessagingAPIには、
ユーザの投稿に返信するReplyAPIと
Bot側から投稿できるPushAPIがあります。

1つ目の問題は、Replyに必要なReplyTokenの切れる時間です。
何秒で切れるのかいまいち情報が無かったのですが、
LINEへの投稿で発行されたReplyTokenを確認して急いでコピってcurlを投げてもエラーになってしまうレベルでした。
文字の返信くらいしかできないのかなーという印象だったので、画像加工をする時間があるか不安でした。

## Pushで返す画像はURL指定が必須

ならば上記の構成のままPushAPIで返せばいいじゃん！と思ったのですが、
PushAPIで画像を返す場合は、画像のパスをURLで指定しなければいけませんでした。

ユーザがLINEに投稿した画像は、画像取得APIを通じて取得できますが、
そのままフレーム合成してそのままReplyAPIで返すことができなかったため、
一度どこかのストレージに保存する必要がありました。
このため、functionをいくつかのstepに分けることになり、

①画像を取得してS3に保存
②S3の画像をトリミング、フレーム合成をしてもう一度保存
③S3の画像をPushAPIで返信
と、３つに分けました。

※①②は分けなくても良いのですが、1機能で1つとしました。

![Untitled Diagram (1).png](https://qiita-image-store.s3.amazonaws.com/0/79236/6aeaa9cb-fcff-ed9c-cbb9-f4557c688a5b.png)

PushAPIにはUserIDが必要ですが、投稿と非同期になるため返信先をどう特定するかが問題です。
しっかりやるなら、画像URLとUserIdをDBにでも持たせて、キューなどで順番にPushするのが綺麗だと思うのですが、
極力金をかけないという謎の縛りプレイを始めた私は、
画像の保存パスを

バケット名/UserId/YYYYMMDDhhmmss.png

とすることでDBは使わない方向でいきました。
後続のLambdaはS3へのPUTをトリガーに動くので、
画像のパスからUserIdを抜き取ってReplyします。

## Messaging APIのDeveloperプランは友達50人まで

これが一番問題でした。
Developerプランの範疇を超えて使おうとしている自分がいけないのですが、
今回想定されるお客さんは2~300人なので、全然足りません(泣)

有料プランもありますが、目指すはコストレスです。
しかもPushAPIはDeveloperプランかプロプラン(¥32,400/月)でしか使えないのです。世の中世知辛い。

というわけで、苦肉の策でアカウントを6つ用意しました。

![Untitled Diagram.png](https://qiita-image-store.s3.amazonaws.com/0/79236/8f20e2f2-4994-17fd-6e0f-fcbf73db3891.png)

展開用のURL(QRコード)はAPIGateWayのエンドポイントになっており、
ランダムに1つのLINEアカウントへのリンクを返します。

素晴らしいです。ロードバランサー的な役割によって、
友達リクエストを負荷分散をさせることができました(謎)

かなりごり押しですが、ここも妥協点です。

同じ人が複数回URLにアクセスしてくることも想定できましたが、
身内イベントだしまぁいっか、という感じです。

## 金がかからないのはいいが、デプロイがつらい
Botアカウント6つをポチポチ作りながら、ふと思いました。

　　　　**これはデプロイで死ねる** 、と。

「単発なのでデプロイは泥臭くてもいい」とは言ったものの6つはあまりに面倒です。

MessagingAPIを使うためには、
LINEアカウント毎のCHANNEL_ACCESS_TOKENが必要になります。
これをLambdaの環境変数に設定する必要があるので、
裏側は処理は全て同じLambda、というわけにはいきませんでした。

もちろん、UserIdと共にCredential情報もDBに、と行きたいところですが、
目指すはコストレスです。

少しでもデプロイが楽になればとCloudFormation(AWS SAM)を使うことにしました。


![Untitled Diagram (3).png](https://qiita-image-store.s3.amazonaws.com/0/79236/e886596a-537c-61a6-10f8-4fe12f6d2143.png)

yaml形式のテンプレートによって、AWSの構成を立ち上げることができます。
多少面倒さは残りますが、立ち上げ時に環境変数にCredential情報を設定すればいいような形でつくりました。

あまり詳しくないのですが、Serverless Frameworkとかだと環境変数をファイルで外出しして、
コマンドの引数で与えるだけで各環境が作れたりするっぽいですね。すごい。

とりあえず、これで幾分かはスマートにいったと思います。

## Lambdaの実装
ここは設計ではないので、割愛します。
Node.jsとjimpという画像加工ライブラリを使いました。
Node.jsを書くのは初めてだったので非同期処理やPromiseに翻弄されしんどかったです。
jimpはシンプルで分かりやすかったのでオススメです。
https://github.com/oliver-moran/jimp

## イベント当日に公開。しかし…

無事、当日を迎えたのですが、
android端末の方から画像を保存できないという報告が。
OSのバージョンや、エラーメッセージのスクショをもらいそびれたのですが、
「LINEのサーバーから送られた画像じゃない！怪しいからダウンロードしないよ！」というようなエラーでした。

タイムライン上にはフレーム付きのサムネイルが返ってきているのですが、
保存しようとするとエラーが出るようでした。。くやしい。

動作確認は数人にお願いしていたのですが、全員iPhoneだったみたいです。。
LINEBotと言えども、アプリ上で使うものだということを全く考えてなかったです。。

#　まとめ
- なんとか無理やりコストレスで作れました。Lambdaは無料枠範囲内です。
- 厳密にはS3とAPIGateWayが若干かかってますが、11,12月の請求見ても分からないレベルです。
- コストレスアーキテクチャって言葉は流行らなくていいです。
- お金はケチらないほうがいいな、と思いました。