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

##2021/1/19 追記
GitHubにサンプルコードをUPしました。→[こちら](https://github.com/AndanteSysDes/Line-Register-Sample)

#この記事の概要
仕事で初めてLINEbotを作ることになったので、本番稼働環境で試す前に
Herokuでチャットサーバを立てて動作確認を行うことにしました。
この記事は、Git, Heroku CLI（コマンドラインインターフェイス）を利用して
Flask + MySQLのLINEbotを作成する方法についてです。
今回は、Dockerfileを利用してHerokuアプリを作ります。

#前回までのあらすじ

■[Python初心者がFlaskでLINEbotを作ることになった（Flaskざっくり解説編）](https://qiita.com/suwa-andante/items/2e31e00f9b1ac7a4c375)
→Flaskってなによ、という方はこちらへどうぞ。

■[Docker-ComposeでFlask + SQLAlchemy+ MySQLを使えるようにする](https://qiita.com/suwa-andante/items/a25acc1523fa674e7eda)
→Herokuの前に、ローカル開発環境整えるにはどうするのよ、という方はこちらへ。
　データベースにひたすらPeterさんを増減させるだけのWebサンプルを置いています。

■本記事ではいよいよLINEを通して動作するアプリケーションを作るよ！
（初心者過ぎて前置きが長かった）


#本記事の参考

大変勉強になりました。ありがとうございます。

・[Herokuデプロイ方法【Heroku+Flask+MySQL】](https://qiita.com/nochifuchi/items/573e4630fd5722e41ae8)
・[Rails on DockerをHerokuでDeployするまで](https://qiita.com/at-946/items/6958b1d35330c87b8e50)
・[【Windows】docker-composeを使ってmy.cnfの共有は気を付けよう！（🍣＝🍺問題など）](https://qiita.com/koyo-miyamura/items/4d1430b9086c5d4a58a5)
・[Flask + Herokuでアプリを動かす](https://qiita.com/svfreerider/items/50c68252c2f119583a28)

#サンプルアプリ：オウム返し＋発言記録bot
『LINEでユーザーがbotに対して発言する→botが発言内容を記録する→botが発言内容をオウム返しする』といった仕様のサンプルアプリを例にとって、Herokuを使ったbot開発を進めます。

#準備
・[LINE Developers](https://developers.line.biz/ja/)にアカウント登録する。
　LINEアカウント（私はビジネスアカウント（個人）を作りました）でログインし、
　プロバイダー作成→チャンネル作成まで進めておきます。
　詳しいところは割愛します。

・[Heroku](https://jp.heroku.com/home)アカウントを作成する。

・Gitをインストールして、コマンドラインから使えるようにしておく。



#ソースコードを用意する
基本は参考記事を見つつ、前回作ったものと、オウム返しbotのソースコードをミックス＆アレンジしている形です。
ローカル環境にもHeroku同じ動作環境を作りたいので、docker-compose.ymlも用意しています。
（Herokuでは使用しませんが、個別に外すのがめんどくさいので他の諸々と一緒にGit pushしちゃいます）
Dockerでローカル環境を作っていたものを、Herokuに持っていくにあたり、ディレクトリ構造が前回より変わっています。

##ツリー構造

```console
$ 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`

下から２行の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`

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

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

##`Register/app.py`

```python: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`

```python: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`


```python: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`

```python: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`

```python:Register/models/__init__.py
# -*- coding: utf-8 -*-
from .models import Message


__all__ = [
    Message
]
```


#コマンドライン操作でHerokuアプリケーションと連携DBを作成する

ソースコードを用意したら、Herokuの環境準備を行います。

##Herokuログイン

コマンドラインから、Herokuにログインします。
Heroku CLIをダウンロードしていない場合は、[Heroku Dev Center](https://devcenter.heroku.com/articles/heroku-cli#download-and-install)からダウンロードします。

```
$ 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エラーが起こった時の対処法](https://qiita.com/yo-iida/items/20e86e913000f6d080ca)


##ローカル環境でマイグレート準備

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