LINE BOTを動かしてみる[heroku編]
概要
Zenn初投稿ということで、巷にありふれまくっていますが、
Linebotのバックエンドをherokuで構築する手順をまとめた記事を投稿します。
そのほかのサービスを利用した構築については、以下を参照ください。
- CloudFunctions ※近日更新
- Cloud Run ※近日更新
- GKE ※近日更新
バックエンドの実装
メジャー言語であればほとんどの言語を使用することが可能です。今回は執筆時点で人気なPython(3.8)
を使用します。
特に機能の追加は行わないので、オウム返しするのみです。
ハンドラ実装
ソースコード全容
main.py
import logging
import os
from flask import Flask, abort, request
from linebot import LineBotApi, WebhookHandler
from linebot.exceptions import InvalidSignatureError
from linebot.models import MessageEvent, TextMessage, TextSendMessage
app = Flask(__name__)
app.logger.setLevel(logging.INFO)
# 変更点1
handler = WebhookHandler(os.getenv('CHANNEL_SECRET'))
line_bot_api = LineBotApi(os.getenv('CHANNEL_ACCESS_TOKEN'))
@app.route('/callback', methods=['POST'])
def callback():
signature = request.headers['X-Line-Signature']
body = request.get_data(as_text=True)
app.logger.debug("Request body: " + body)
try:
handler.handle(body, signature)
except InvalidSignatureError:
# 変更点3
app.logger.warn('Invalid signature. Please check your channel access token/channel secret.')
abort(400)
return 'OK'
@handler.add(MessageEvent, message=TextMessage)
def handle_message(event):
# 変更点2
if (event.reply_token == '00000000000000000000000000000000' or
event.reply_token == 'ffffffffffffffffffffffffffffffff'):
app.logger.info('Verify Event Received')
return
# オウム返し
line_bot_api.reply_message(
event.reply_token,
TextSendMessage(text=event.message.text))
if __name__ == "__main__":
app.logger.setLevel(logging.DEBUG)
app.run()
ソースコードはPython SDKのサンプルをベースに以下の修正を加えています。
シークレットをコードに埋め込まない
チャンネルシークレット
とアクセストークン
は秘匿情報の部類となります。仮に流出してしまった場合は、他人に自分のアカウントをのっとられてしまいます。
サンプルではチャンネルシークレット
とアクセストークン
を直接コードに記述するような書き方になっていますが、この状態でだれもが見られるリモートリポジトリにコードをpushすることはできませんね。(プライベートリポジトリであれば多少話は別ですが)
そこで、実行時のサーバ・コンテナの特定の環境変数に値を設定し、コード側はその環境変数から値を読み込むように記載します。(万全ではないですが、対応前に比べれば段違いにセキュアなつくりとなっています)
- handler = WebhookHandler('YOUR_CHANNEL_SECRET')
- line_bot_api = LineBotApi('YOUR_CHANNEL_ACCESS_TOKEN')
+ handler = WebhookHandler(os.getenv('CHANNEL_SECRET'))
+ line_bot_api = LineBotApi(os.getenv('CHANNEL_ACCESS_TOKEN'))
Verify
を成功させる
LINE Developersからの本対応は必須ではありません。
LINE DevelopersのWebhook URL
を設定する画面にVerify
ボタンが存在し、ここから設定したバックエンドへの疎通を取るような形になっています。
しかし、サンプルをそのまま利用すると、このVerify
のレスポンスコードが200になることはありません。
これはVerify
リクエストのreply_token
が00000000000000000000000000000000
またはffffffffffffffffffffffffffffffff
となっており、どうもWebAPI側でこの2つのトークンは不正であると判別していることによるようです。
そこで、この2種類のトークンは特別扱いで200のステータスコードを返すようにhandlerへ分岐を追加します。
+ if (event.reply_token == '00000000000000000000000000000000' or
+ event.reply_token == 'ffffffffffffffffffffffffffffffff'):
+ return
print → logger
本対応は必須ではありませんが、推奨設定です。
ローカルでのprintデバッグというのは有用な方法ですが、通常ソースコードにprint
は埋め込まない方がよいでしょう。情報を出力したい場合は通常ロギング(ライブラリ)を使用します。今回はFlask
のLoggerを利用するのが手っ取り早いと思います。
- print("Invalid signature. Please check your channel access token/channel secret.")
+ app.logger.warn('Invalid signature. Please check your channel access token/channel secret.')
Webサーバ設定ファイルの記述
上記実装で使用したFlaskフレームワークには、Webサーバとして起動することのできる機能も備わっていますが、デバッグ向けの機能となります。サービスとして公開する場合には、安定性や速度を鑑みてWSGIサーバを使用するのがよいでしょう。(このあたりの詳細はこちらも参照いただけると🙏)
今回はgunicornを利用します。
設定ファイル
config.py
import os
host = '0.0.0.0'
port = os.getenv('PORT', 5000)
bind = str(host) + ':' + str(port)
# Debugging
reload = True
# Logging
accesslog = '-'
loglevel = 'info'
# Proc Name
proc_name = 'Line-Bot-Practice'
# Worker Processes
workers = 1
worker_class = 'sync'
ポイントは1点のみです。
ポートはheroku側で自動設定される
herokuの仕様で、デプロイ後にポートが自動設定されるため、ポートに固定値を使用することはできません。
環境変数PORT
にこの値は設定されているため、os.getenv('PORT')
で取得します。
ライブラリバージョンの指定
外部ライブラリを使用する際は、利用バージョンを明示・固定するのがよいでしょう。
ローカルにインストールしている場合は、pip freeze > requirements.txt
コマンドで作成することも可能です。
Flask==1.1.2
line-bot-sdk==1.17.0
gunicorn==20.0.4
herokuデプロイ設定
Deploy With Git / Deploy With Docker の2パターンを選ぶことが可能です。
共通手順
どちらを選択する場合でも共通する手順です。
# Git初期設定
git init
# ログイン / ログイン用ページへ遷移するので必要情報を入力
heroku login
# アプリケーション作成
heroku create ${APP_NAME}
# 環境変数のセット
heroku config:set CHANNEL_SECRET=${YOUR_CHANNEL_SECRET} --app ${APP_NAME}
heroku config:set CHANNEL_ACCESS_TOKEN=${YOUR_ACCESS_TOKEN} --app ${APP_NAME}
Gitデプロイ
ディレクトリ構成
ルートディレクトリを起点に処理がなされるため、すべてルートに配置します。
./
├── config.py
├── Procfile
├── requirements.txt
├── runtime.txt
└── main.py
また、2つのファイルを追加します。
ランタイムの指定ファイル
Python固有の設定です。
runtime.txt
を作成することで、実行するPythonバージョンを指定することができます。
サポートされているバージョンはこちらを参照ください。
runtime.txt
python-3.8.5
起動設定ファイル
ルートディレクトリにProcfile
が存在する場合は、記載されている内容でプロセスが起動されます。
ファイルの記述内容はこちらに詳細が書かれていますが、プロセス種別はほとんどのケースでweb
でしょうからweb: {コマンド}
と書くことが多いでしょう。
Procfile
web: gunicorn main:app -c config.py
デプロイ
herokuリポジトリのmaster(またはmain)ブランチにpush
することでアプリケーションがデプロイされます。
git add .
git commit -m "first commit"
git push heroku master
動作確認
LINE DevelopersでWebhook URL
にhttps://${YOUR_HEROKU_APP}.herokuapp.com/
を設定してVerify
、または実際にLINEからメッセージを送信してオウム返しされてくれば問題なく稼働しています。
コンテナデプロイ
自身で作成したDockerfileをビルド、コンテナデプロイする方法です。
ディレクトリ構成
herokuはdocker build
を実行する際のコンテキストが、自動的にDockerfileの存在するディレクトリに固定されてしまい、外部から指定することができないため、Dockerfileはルートディレクトリに配置します。
./
├── example/
│ └── python/
│ ├── config.py
│ ├── requirements.txt
│ └── main.py
├── heroku.yml
└── herokuDockerfile
こちらも2つのファイルを追加します。
Dockerfile
ライブラリのインストールと、起動コマンドの設定を行います。
herokuアプリに設定した環境変数をよしなにコンテナに設定してくれるようです。
PYTHONDONTWRITEBYTECODE
の意義については、こちらを参照ください。
なお、PYTHONUNBUFFERED
の設定を行っているDockerfileをみかけますが、Python3.7以降デフォルト設定となっているため、3.7以降であれば明示する必要性はありません。
herokuDockerfile
FROM python:3.8-alpine
ENV PYTHONDONTWRITEBYTECODE 1
COPY example/python /usr/local/application
RUN pip3 install -r /usr/local/application/requirements.txt && \
rm -f /usr/local/application/requirements.txt
WORKDIR /usr/local/application
CMD ["gunicorn", "main:app", "-c", "config.py"]
デプロイ設定ファイル
Git側でのProcfile
と同様です。
setup
build
release
run
の4ステップを設定することが可能ですが、build
ステップで対象のDockerfileを指定することができていれば起動するでしょう。
ちなみにrun
ステップを記述すると、CMDを上書きすることができます。
heroku.yml
build:
docker:
web: herokuDockerfile
デプロイ/動作確認
Gitデプロイと同様です。
Discussion