🚀

LINE BOTを動かしてみる[heroku編]

2020/09/26に公開

概要

Zenn初投稿ということで、巷にありふれまくっていますが、
Linebotのバックエンドをherokuで構築する手順をまとめた記事を投稿します。

そのほかのサービスを利用した構築については、以下を参照ください。

バックエンドの実装

メジャー言語であればほとんどの言語を使用することが可能です。今回は執筆時点で人気な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'))

LINE DevelopersからのVerifyを成功させる

本対応は必須ではありません。
LINE DevelopersのWebhook URLを設定する画面にVerifyボタンが存在し、ここから設定したバックエンドへの疎通を取るような形になっています。
しかし、サンプルをそのまま利用すると、このVerifyのレスポンスコードが200になることはありません。
これはVerifyリクエストのreply_token00000000000000000000000000000000またはffffffffffffffffffffffffffffffffとなっており、どうもWebAPI側でこの2つのトークンは不正であると判別していることによるようです。
そこで、この2種類のトークンは特別扱いで200のステータスコードを返すようにhandlerへ分岐を追加します。

+ if (event.reply_token == '00000000000000000000000000000000' or
+             event.reply_token == 'ffffffffffffffffffffffffffffffff'):
+         return

本対応は必須ではありませんが、推奨設定です。
ローカルでの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 URLhttps://${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