##2021/1/19 追記
GitHubにサンプルコードをUPしました。→こちら
#この記事の概要
仕事で初めてLINEbotを作ることになったので、本番稼働環境で試す前に
Herokuでチャットサーバを立てて動作確認を行うことにしました。
この記事は、Git, Heroku CLI(コマンドラインインターフェイス)を利用して
Flask + MySQLのLINEbotを作成する方法についてです。
今回は、Dockerfileを利用してHerokuアプリを作ります。
#前回までのあらすじ
■Python初心者がFlaskでLINEbotを作ることになった(Flaskざっくり解説編)
→Flaskってなによ、という方はこちらへどうぞ。
■Docker-ComposeでFlask + SQLAlchemy+ MySQLを使えるようにする
→Herokuの前に、ローカル開発環境整えるにはどうするのよ、という方はこちらへ。
データベースにひたすらPeterさんを増減させるだけのWebサンプルを置いています。
■本記事ではいよいよLINEを通して動作するアプリケーションを作るよ!
(初心者過ぎて前置きが長かった)
#本記事の参考
大変勉強になりました。ありがとうございます。
・Herokuデプロイ方法【Heroku+Flask+MySQL】
・Rails on DockerをHerokuでDeployするまで
・【Windows】docker-composeを使ってmy.cnfの共有は気を付けよう!(🍣=🍺問題など)
・Flask + Herokuでアプリを動かす
#サンプルアプリ:オウム返し+発言記録bot
『LINEでユーザーがbotに対して発言する→botが発言内容を記録する→botが発言内容をオウム返しする』といった仕様のサンプルアプリを例にとって、Herokuを使ったbot開発を進めます。
#準備
・LINE Developersにアカウント登録する。
LINEアカウント(私はビジネスアカウント(個人)を作りました)でログインし、
プロバイダー作成→チャンネル作成まで進めておきます。
詳しいところは割愛します。
・Herokuアカウントを作成する。
・Gitをインストールして、コマンドラインから使えるようにしておく。
#ソースコードを用意する
基本は参考記事を見つつ、前回作ったものと、オウム返しbotのソースコードをミックス&アレンジしている形です。
ローカル環境にもHeroku同じ動作環境を作りたいので、docker-compose.ymlも用意しています。
(Herokuでは使用しませんが、個別に外すのがめんどくさいので他の諸々と一緒にGit pushしちゃいます)
Dockerでローカル環境を作っていたものを、Herokuに持っていくにあたり、ディレクトリ構造が前回より変わっています。
##ツリー構造
$ tree /f
│ docker-compose.yml
│ Dockerfile
│ Procfile
│ requirements.txt
│ run.py
│
├─docker
│ └─db
│
└─Register
│ app.py
│ config.py
│ database.py
│ __init__.py
│
└─models
│ models.py
└─ __init__.py
##docker-compose.yml
ローカル環境構築用に準備します。
Herokuでこのファイルは使いませんが、Herokuで使うデータベースをマイグレーション管理するため、ローカル環境の準備が必要になるので用意しています。
MySQLが日本語で文字化けしないよう、前回のdocker-compose.yml
からコマンド--skip-character-set-client-handshake
を追加しています。
これがなくても、動作は問題ないのですが、MySQLサーバに入ってコンソール上で結果を表示させると日本語が?????
となってしまいます。
ちょっと戸惑うのでコマンドで直すようにします。
version: '3'
services:
flask:
container_name: register-python
build: .
ports:
- 5050:5000 #ホスト側は使っていないポート:Docker側は5000番ポート
links:
- mysql
privileged: true
volumes:
- .:/project
tty: true
environment:
TZ: Asia/Tokyo
FLASK_ENV: 'development' #デバッグモードON
FLASK_APP: 'run.py' #起動用アプリの設定
command: flask run -h 0.0.0.0
mysql:
container_name: register-db
image: mysql:5.5
environment:
MYSQL_ROOT_PASSWORD: root
MYSQL_DATABASE: register
MYSQL_USER: hoge
MYSQL_PASSWORD: huga
TZ: 'Asia/Tokyo'
command: mysqld --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci --skip-character-set-client-handshake
volumes:
- ./docker/db/data:/var/lib/mysql
- ./docker/db/sql:/docker-entrypoint-initdb.d
ports:
- 3306:3306 #ホスト側は使っていないポート:Docker側は3306番ポート
##Dockerfile
下から2行のFlask環境設定は、Heroku CLIでも設定しますが念のため入れています。
FROM python:3.6
ARG project_dir=/project/
ADD ./requirements.txt $project_dir
WORKDIR $project_dir
RUN pip install -r requirements.txt
RUN export FLASK_ENV=development #Herokuではdocker-compose使えないので追記
RUN export FLASK_APP='run.py' #Herokuではdocker-compose使えないので追記
##Procfile
heroku.ymlを使って構築する方法もあるそうですが、勉強中のためProcfileを使います。
いずれheroku.ymlが理解できたらAppendixとして追記するかも…。
HerokuでFlaskを使うときのお決まりの書き方です。
今回アプリの起動にrun.py
を使うので、run:app
の記載になっています。
manage.py
など、起動ファイル名を変える場合はmanage:app
に変えるなどアレンジしてください。
(DockerfileなどのFLASK_APP設定もあわせて変更してください。)
web: gunicorn run:app --log-file=-
##requirements.txt
前回記事で使用していたライブラリに加えて、
LINEの機能を利用するためのline-bot-sdk
、
HerokuでPythonを動かすためのWSGIサーバgunicorn
をインストールします。
WSGIサーバは勉強不足でうまく説明できない…すいません。
Flaskを動かしてくれるアプリケーションサーバ、という理解です。とんかつのツナギみたいな感覚で使ってます(適当)
Flask
sqlalchemy
flask-sqlalchemy
flask-migrate
pymysql
line-bot-sdk
gunicorn
##run.py
# -*- coding: utf-8 -*-
from Register.app import app
if __name__ == '__main__':
app.run()
##Register/app.py
# -*- coding: utf-8 -*-
from flask import Flask, request, abort
from .database import init_db, db
from .models import Message
from .config import Channel_access_token, Channel_secret
#ログ出力用(printはWinコマンドプロンプトで日本語が文字化けする)
from pprint import pprint
#LINEbot
from linebot import LineBotApi, WebhookHandler
from linebot.exceptions import InvalidSignatureError, LineBotApiError
from linebot.models import MessageEvent, TextMessage, TextSendMessage
def create_app():
app = Flask(__name__)
app.config.from_object('Register.config.Config')
init_db(app)
#LINE環境設定
line_bot_api = LineBotApi(Channel_access_token)
handler = WebhookHandler(Channel_secret)
@app.route('/callback', methods=['POST'])
def callback():
signature = request.headers['X-Line-Signature']
body = request.get_data(as_text=True)
try:
handler.handle(body, signature)
except InvalidSignatureError:
abort(400)
return 'OK'
#テキストメッセージイベントのハンドラーを追加
@handler.add(MessageEvent, message=TextMessage)
def handle_message(event):
#LINEのユーザーIDを取得
user_id = event.source.user_id
#送られてきたメッセージをテキストとして取得
input_msg = event.message.text
#DBに保存
message = Message(
line_user_id = user_id,
message = input_msg
)
db.session.add(message)
db.session.commit()
print('メッセージ登録:{}'.format(message))
#返答はオウム返し
line_bot_api.reply_message(event.reply_token,TextSendMessage(text=input_msg))
return app
app = create_app()
@handler.add(MessageEvent, message=TextMessage)
で、LINEからテキストメッセージをもらった時の機能を追加しています。
テキストメッセージなので、画像や位置情報なんかを送られてもこのハンドラーは機能しません。
(それ用のハンドラーを追加しないといけない。)
def handle_message(event):
以下は、実際にどういう動作をするのかを定義しています。
Message
テーブルに、LINEで使われるユーザIDと送られてきたメッセージをそのまま保存し、メッセージをオウム返ししています。
##Register/config.py
# -*- coding: utf-8 -*-
import os
class DevelopmentConfig:
# Flask
DEBUG = True
#MySQL
SQLALCHEMY_DATABASE_URI = 'mysql+pymysql://{user}:{password}@{host}/{database}?charset=utf8mb4'.format(**{
'user': os.getenv('DB_USER', '[DBユーザ]'),
'password': os.getenv('DB_PASSWORD', '[DBパスワード]'),
'host': os.getenv('DB_HOST', '[DBホスト]'),
'database': os.getenv('DB_DATABASE', '[データベース名]')
})
SQLALCHEMY_TRACK_MODIFICATIONS = False
SQLALCHEMY_ECHO = False
Config = DevelopmentConfig
#LINEチャンネル設定
Channel_access_token = '[チャンネルアクセストークン]'
Channel_secret = '[チャンネルシークレット]'
DB設定は、最初はローカル環境の設定にします。(ローカル環境でマイグレーションの準備をする必要があるので)
##Register/database.py
# -*- coding: utf-8 -*-
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate
db = SQLAlchemy()
def init_db(app):
db.init_app(app)
Migrate(app, db)
##Register/__init__.py
ファイルだけ作成しておきます。
(中身は空です。)
##Register/models/models.py
# -*- coding: utf-8 -*-
from datetime import datetime
from Register.database import db
class Message(db.Model):
__tablename__ = 'message'
message_id = db.Column(db.Integer, primary_key=True, autoincrement=True)
line_user_id=db.Column(db.String(255), nullable=False)
message = db.Column(db.String(255), nullable=False)
created_at = db.Column(db.DateTime, nullable=False, default=datetime.now)
updated_at = db.Column(db.DateTime, nullable=False, default=datetime.now, onupdate=datetime.now)
def __repr__(self):
return '<message_id = {message_id}, line_user_id = {line_user_id}, message = {message}>'.format(
message_id=self.message_id,
line_user_id=self.line_user_id,
message=self.message
)
##Register/models/__init__.py
# -*- coding: utf-8 -*-
from .models import Message
__all__ = [
Message
]
#コマンドライン操作でHerokuアプリケーションと連携DBを作成する
ソースコードを用意したら、Herokuの環境準備を行います。
##Herokuログイン
コマンドラインから、Herokuにログインします。
Heroku CLIをダウンロードしていない場合は、Heroku Dev Centerからダウンロードします。
$ heroku login
ブラウザ上でログインを促されるので、画面に従いログインします。
完了したらブラウザは閉じて大丈夫です。
##Herokuコンテナログイン
Dockerのイメージを使ってデプロイできるように、Heroku Container Registryにログインします。
$ heroku container:login
Login Succeeded
と出たらOK。
##Herokuアプリ作成
Heroku CLIを使って新しいHerokuアプリを作成します。
今回は、line-register
という名前のアプリを作ります。
(記事作成後にアプリ自体は削除しています)
アプリ名が他の人のHerokuアプリと競合してしまうと作成できません。
アプリ名を省略すると、勝手に名前を付けてくれます。
$ heroku create line-register
Creating ⬢ line-register... done
https://line-register.herokuapp.com/ | https://git.heroku.com/line-register.git
##ローカルで作成したdocker imageをHerokuのContainer Registryにpushする
Dockerfileのあるディレクトリに移動し、以下のコマンドを打ちます。
$ heroku container:push web -a line-register
heroku container:push web だけだとエラーが出てしまったので、
-a [herokuアプリ名]で対象のアプリを指定しています。
これで、Dockerfileのイメージをもとにビルドしてくれるようになります。
Webサーバとして起動するので、web
を指定します。
##データベースを作成する
今回はposgreではなくMySQLを使いたいので、ClearDBのアドオンを使います。
ClearDBはMySQLの機能を提供しているクラウドサービスです。
Herokuのアプリにデータベースの機能を持たせることができないので、このアドオンにとてもお世話になります。
Herokuでアドオンを使う場合は、あらかじめクレジットカードの登録が必要なので、ブラウザ上で登録しておきます。
一番小さいプランのigniteを設定しておけば無料なので、登録カードから決済されることはありません。
$ heroku addons:create cleardb:ignite
##heroku環境変数設定
ClearDBの作成が完了した後、データベースの設定内容を確認します。
$ heroku config
# 以下が出力されます
CLEARDB_DATABASE_URL: mysql://[username]:[password]@[hostname]/[db_name]?reconnect=true
コンフィグの情報を、以下に書き換えます。(mysql
→mysql2
)
heroku config:set DATABASE_URL='mysql2://[username]:[password]@[hostname]/[db_name]?reconnect=true'
Herokuで、必要となる環境変数をコマンドラインから設定していきます。
#DBの設定
$ heroku config:add DB_USERNAME="[username]"
$ heroku config:add DB_PASSWORD="[password]"
$ heroku config:add DB_HOSTNAME="[hostname]"
$ heroku config:add DB_NAME="[db_name]"
#LINEチャンネルの設定
$ heroku config:set YOUR_CHANNEL_SECRET="[チャンネルシークレット]"
$ heroku config:set YOUR_CHANNEL_ACCESS_TOKEN="[チャンネルアクセストークンの欄の文字列]"
#Flaskの環境設定
$ heroku config:set FLASK_ENV='development'
$ heroku config:set FLASK_APP="run.py"
#worker起動数を変更(無料プランだとR14メモリエラーが起きがちのため)
$ heroku config:set WEB_CONCURRENCY=1
最後のworkerについては、以下の記事を参考にさせていただきました。
HerokuでR14/R15エラーが起こった時の対処法
##ローカル環境でマイグレート準備
Herokuにデプロイすると、flask db init
flask db migrate
が機能しなくなっていしまいます。
実際やってみると、エラーは出ないのですが、migrationsファイルがHeroku上に作成できないことがわかります。
そのため、ローカル環境でinit
migrate
を行い、作成されたmigrationディレクトリをHerokuにアップロードし、upgrade
を実行するという流れでDBの管理を行います。
ということで、ここまで来たら、ローカル環境でDockerコンテナを立ち上げ、pythonサーバに入ります。
そして、以下を操作します。
$ flask db init
$ flask db migrate
Docker-compose.yml
で/project
をボリュームに設定しているので、
migrationディレクトリはhost側のプロジェクトフォルダの直下に作成されるはずです。
##SQLAlchemyのConfigを編集
次はHerokuにソースコードを持って行って操作するので、
Configのデータベース設定をローカルからClearDBの設定に直します。
##Herokuデプロイ
いよいよ、Dockerイメージからコンテナをリリースし、migrationsディレクトリとソースコードを含むプロジェクトファイルをマルっとHerokuに持っていきます。
$ heroku container:release web
$ git push heroku master
これを行った際、私の環境では以下のようにheroku.ymlを作成するよう怒られました。
remote: =!= Your app does not include a heroku.yml build manifest. To deploy your app, either create a heroku.yml: https://devcenter.heroku.com/articles/build-docker-images-heroku-yml
remote: Or change your stack by running: 'heroku stack:set heroku-18'
remote: Verifying deploy...
remote:
remote: ! Push rejected to line-register.
今回はheroku.yml使わないので、素直にスタックを変更することにしました。
$ heroku stack:set heroku-18
この後、git pushは成功し、アプリの配置が完了しました。
##Herokuにテーブルを作成する
ローカルで作成しておいたmigrationsフォルダを引き継いで、テーブルの作成を行います。
heroku run flask db upgrade
これで、ClearDB上にmessage
テーブルが作成されます。
これで、Herokuにデプロイが完了しました!
#LINEコンソール設定
HerokuアプリにLINEチャンネルからアクセスするため、LINE側の設定を変更します。
まず、LINE Developersにログインし、作成したチャンネルを選択します。
##LINEのwebhookアドレスを入力、webhookを有効化する
Messaging API設定から、Webhook URLを設定します。
URLは、https://[Herokuアプリ名].herokuapp.com/callback
という形式になります。
Webhookの利用はONにします。
すぐ下にある「応答メッセージ」「挨拶メッセージ」は無効にしておきましょう。
##LINEでチャンネル登録して、テスト実施
チャンネルのQRコードやIDを利用して、友達登録します。
実際にメッセージを送って、動作を確かめてみます。
オウム返しされていれば、LINE上のテストはOKです。
##MySQLにデータが登録されていることを確認
送ったメッセージが登録されているかは、MySQL Workbenchなどを利用して確認します。
app.py
を改良して、Webブラウザからデータを確認できるようにしてもよいと思います。