4
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Python初心者がFlaskでLINEbotを作ることになった(Heroku+ClearDB編)

Last updated at Posted at 2020-05-28

##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サーバに入ってコンソール上で結果を表示させると日本語が?????となってしまいます。
ちょっと戸惑うのでコマンドで直すようにします。

docker-compose.yml
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でも設定しますが念のため入れています。

Dockerfile
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設定もあわせて変更してください。)

Procfile
web: gunicorn  run:app --log-file=-

##requirements.txt

前回記事で使用していたライブラリに加えて、
LINEの機能を利用するためのline-bot-sdk
HerokuでPythonを動かすためのWSGIサーバgunicornをインストールします。

WSGIサーバは勉強不足でうまく説明できない…すいません。
Flaskを動かしてくれるアプリケーションサーバ、という理解です。とんかつのツナギみたいな感覚で使ってます(適当)

requirements.txt
Flask
sqlalchemy
flask-sqlalchemy
flask-migrate
pymysql
line-bot-sdk
gunicorn

##run.py

run.py
# -*- coding: utf-8 -*-
from Register.app import app

if __name__ == '__main__':
    app.run()

##Register/app.py

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

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

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

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

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

コンフィグの情報を、以下に書き換えます。(mysqlmysql2

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ブラウザからデータを確認できるようにしてもよいと思います。

4
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?