ボンジニアの備忘録

凡人エンジニア、通称ボンジニアによる備忘録です。勉強した内容を書いていきます。ジャンル問わずです!間違っていたら指摘をお願いします!

Python + FlaskによるLINE BOTの開発(AWS使用)

以前、herokuでのLINE BOT作成を記事に書きましたが、もうちょっと実運用を考えた時にどうすればいいか自分なりにググって調べてAWSを選択しました。
yujikawa11.hatenablog.com


なので今回はAWSを使ったLINE BOTの開発を行いました。
なるべくサーバーレスでやりたかったので、いろいろ見ていましたが下記のような流れにしたいなぁと思いました。が!Nat gatewayは高いのでNat用インスタンスを使いました。
【理想】
LINE-->API Gateway-->AWS Lambda-->SNS-->SQS-->Elastic Beanstalk Worker-->Nat gateway-->LINE

【今回】
LINE-->API Gateway-->AWS Lambda-->SNS-->SQS-->Elastic Beanstalk Worker-->Nat用インスタンス-->LINE

f:id:yujikawa11:20160626224606p:plain

1. NATインスタンスの設定

EC2のインスタンス作成より下記のように、すでに作られているNAT用インスタンスを検索して使用します。
f:id:yujikawa11:20160627220125p:plain

特に設定はしてません、そのまま作成完了にしました。

あとはElastic IPを取得し、関連づけを行ってください。
ここで設定したElastic IPはLINE側にも登録します。

2. ネットワーク関連の設定

今回は全て同一のアベイラビリティゾーンで作成しています。

VPCの作成

VPCの作成ボタンより作成を行います。
f:id:yujikawa11:20160627212717p:plain
名前:LineBotVPC
VPC CIDR:10.0.0.0/16

■インターネットゲートウェイの作成

f:id:yujikawa11:20160627213039p:plain
先ほど作成したVPC(LineBotVPC)にアタッチします。

■サブネットの作成

ここではPrivateとPublicの二つを作成します。
f:id:yujikawa11:20160627213138p:plain

名前:Line Bot Private subnet
VPC:LineBotVPC
CIDR:10.0.1.0/24
ルート:NATインスタンスを指定する(送信先:0.0.0.0/0)※作ってなければ後で設定する

名前:Line Bot Public subnet
VPC:LineBotVPC
CIDR:10.0.1.0/24
ルート:インターネットゲートウェイで設定したIDを設定(送信先:0.0.0.0/0)

3. SNSの設定

新規でトピックを作成します。
f:id:yujikawa11:20160627220730p:plain

適当な名前をつけて作成しました。
名前:linebotSNS

4. SQSの設定

新しくキューを作成します。
f:id:yujikawa11:20160627221103p:plain
名前:lineMsgQue
ここでlineMsgQueとlinebotSNSをつなげる設定を行います。
f:id:yujikawa11:20160627221252p:plain

上記のように操作から「SNSトピックへのキューのサブスクライブ」でlinebotSNSを選択します。

5. Lambdaの設定

Lambdaを新規作成します。
今回はPythonを使いました。新しくfunctionを追加して以下のようなプログラムを作成します。
受信した内容をそのままSNSに渡すというものです。

from __future__ import print_function

import json
import boto3
import sys

sns = boto3.client('sns')

print('Loading function')

def handler(event, context):
    jsonstr = json.dumps(event, ensure_ascii = False)
    data = json.dumps({'default' : 'default message' ,'sqs': jsonstr}, ensure_ascii = False)
    #print(data)
    topic = 'LinebotSNSのARN'
    try:
        response = sns.publish(
            TopicArn=topic,
            Message=data,
            MessageStructure='json'
        )
        print("Success Send data>>")
        print(data)
        print("<<")
        return 'Successfully processed'
    except:
        print("ERROR START>>")
        print(data)
        print(sys.exc_info())
        print("<<ERROR END")
        return 'ERROR processed'
        
    return 'END'

■その他設定ついて

f:id:yujikawa11:20160627222120p:plain

RoleはSNSにアクセスできる権限をつけます。
また詳細設定からVPCは作成したVPCを選択し、サブネットはPrivateの方を選択しました。

またAPI Endpointの設定を行います。ここでは「POST」で設定を行い、そこで作成されたURLをLINE側のコールバックURLとして登録してください。
f:id:yujikawa11:20160630095015p:plain

6. Elastic beanstalk Workerの設定

「Create New Application」から新規でアプリケーションを作成します。
名前はLineBotWorkerにしました。
f:id:yujikawa11:20160630084347p:plain

次に「Create New Environment」から新しく環境を作成します。
f:id:yujikawa11:20160630084516p:plain
作成時にWebかWorkerを選択すると思いますが、今回はSQSとの連携があるためWorkerを選択します。
f:id:yujikawa11:20160630084619p:plain

LINEにメッセージを送るプログラム一式をzip化して、zipファイルを選択します。「Configure more options」を選択して詳細設定をしていきます。
f:id:yujikawa11:20160630084711p:plain

詳細設定ではVPCの部分の設定を行います。
Elastic beanstalkのネットワークを作成したVPC(LineBotVPC)にします。
またsubnet vpcはprivateにします。

その後作成をすれば完成です。

7 LINEへの送信プログラムについて

zip化したファイルは以下です。
フレームワークはflaskを使用しています。
プログラム自体はおうむ返しのプログラムです。
application.pyは名前を変更してもいいですが、その際はElastic beanstalk側の
アプリケーション名も変える必要があります。(デフォルトがapplication.py)

application.py

from flask import Flask,request,Response
import logging
import requests
import json
import configparser
inifile = configparser.SafeConfigParser()
inifile.read("./config.ini")

LINEBOT_API_EVENT ='https://trialbot-api.line.me/v1/events'
LINE_HEADERS = {
    'Content-type': 'application/json; charset=UTF-8',
    'X-Line-ChannelID':inifile.get("settings","X-Line-ChannelID"),
    'X-Line-ChannelSecret':inifile.get("settings","X-Line-ChannelSecret"),
    'X-Line-Trusted-User-With-ACL':inifile.get("settings","X-Line-Trusted-User-With-ACL")
}

def post_event(to, content):
    msg = {
        'to': [to],
        'toChannel': 1383378250,
        'eventType': "138311608800106203",
        'content': content
    }
    r = requests.post(LINEBOT_API_EVENT, 
                      headers = LINE_HEADERS, 
                      data = json.dumps(msg)
                      )
    
def post_text(to, text):
    content = {
        'contentType':1,
        'toType':1,
        'text':text,
    }
    post_event(to, content)


application = Flask(__name__)
@application.route("/sender", methods=['POST'])

def callback():
    status = None
    print("Start SQS Message recieve")
    if request.json is None:
        status = Response("", status=415)
    else:
        try:
            print("massages json recieve start")
            message = dict()
            messages = json.loads(request.json['Message'])
            print("massages json recieve end")
            for message in messages['result']:
                text = message['content']['text']
                response = text
                post_text(message['content']['from'], response)
                status = Response("",status=200)
        except Exception as ex:
            logging.exception('Error processing message: %s' % request.json)
            response = Response(ex.message, status=500)
    return status 

if __name__ == "__main__":
   application.run(host = '0.0.0.0')

config.ini

[settings]
X-Line-ChannelID:XXXXXXXXX
X-Line-ChannelSecret:XXXXXXXX
X-Line-Trusted-User-With-ACL:MID
proxy:http://XXXXXXXXXXX

requirements.txt

Flask==0.11.1
Flask-Cors==2.1.2
jsonschema==2.4.0
requests==2.9.1
simplejson==3.8.2
configparser==3.5.0