【初めての個人開発】FlaskアプリとTwitterの自動返信BotをHerokuにデプロイした話

# はじめに

初投稿になります。プログラミング歴１年半のPHPエンジニアです。

普段の業務ではAWSを使っているのですが、まだ本番環境に関わったことがないので、今回のHerokuへのデプロイを通していろいろと勉強になったことを初心者の視点でまとめていきたいと思います。

今回作ったアプリについて簡単にご紹介します。

- Twitterで特定のワードのリプライに反応して自動で返信

- flaskで自動返信用テキストの追加・編集を行う管理画面を作成

- Twitter Streaming API (Filter realtime Tweets) を利用して、特定のワードのリプライを24時間監視

- BotはPythonの`twitter`ライブラリを用いて実装

# ディレクトリ構成

```bash
app/
├── web.py           # flaskアプリの実行ファイル
├── web/             # flaskアプリ
│   ├── models/
│   ├── static/
│   ├── templates/
│   ├── views/
│   ├── __init__.py
│   ├── config.py
│   └── database.py
├── bot.py           # 自動返信Botの実行ファイル
├── bot/             # 自動返信Bot（一部省略）
│   ├── __init__.py
│   ├── config.py
│   └── database.py
├── Procfile
├── requirements.txt
├── runtime.txt
├── .env
├── .gitignore
├── migrations/      # Flask-Migrateで作成
└── venv/            # ローカルの仮想環境
```

flaskについては、以下の記事を参考にさせていただきました。

- [Flask + SQLAlchemyプロジェクトを始める手順](https://qiita.com/shirakiya/items/0114d51e9c189658002e)

- [Flaskのディレクトリ構成について考えてみたらBlueprintsを試していた話](https://qiita.com/rarewin/items/8f252313db8ee7fa2de0)

Twitter APIを利用している自動返信Botについては、こちらで詳細の説明をしています。

- [Twitter Streaming API を使ってリアルタイム自動返信Botを作る](https://qiita.com/YiwaiY/items/7148389e3d76e61b798d)

# Herokuにデプロイする上で必要なファイル

Herokuに環境、必要なライブラリや実行ファイルなどを伝えるためのファイルを準備する必要があるみたいです。

## requirements.txt

必要なライブラリをバージョンも含めて記載します。
venvで開発を行ったので、仮想環境にインストールしたパッケージを以下のコマンドでファイルに出力しました。

```bash
(venv)$ pip freeze > requirements.txt
```

インストールしたライブラリ一覧

```txt:requirements.txt
alembic==1.4.2
autopep8==1.5.3
cffi==1.14.0
click==7.1.2
cryptography==2.9.2
Flask==1.1.2
Flask-Login==0.5.0
Flask-Migrate==2.5.3
Flask-SQLAlchemy==2.4.3
gunicorn==20.0.4
itsdangerous==1.1.0
Jinja2==2.11.2
Mako==1.1.3
MarkupSafe==1.1.1
pycodestyle==2.6.0
pycparser==2.20
PyMySQL==0.9.3
python-dateutil==2.8.1
python-dotenv==0.14.0
python-editor==1.0.4
six==1.15.0
SQLAlchemy==1.3.18
toml==0.10.1
twitter==1.18.0
Werkzeug==1.0.1
```

## runtime.txt

これは別になくても良いみたいですが、pythonのバージョンを指定するために準備します。
また、[Herokuがサポートしているバージョン](https://devcenter.heroku.com/articles/python-support)を指定するよう注意が必要です。

```txt:runtime.txt
python-3.7.9
```

## Procfile

アプリの起動方法を指定します。
flaskアプリは本番環境では`gunicorn`などのWSGIサーバーを使う必要があります。

```txt:Procfile
web: gunicorn web:app --log-file=-
worker: python bot.py
```

`web`では、web.py中のappというFlaskインスタンスを起動しています。
`worker`では、自動返信Botの実行ファイルbot.pyを実行しています。

# 環境変数の設定

ローカル環境と本番環境で異なる環境変数（ex. DB情報）をコードを書き換えずにどう設定するかにつまづいたので、まとめます。

## Herokuの環境変数

以下のコマンドで環境変数を設定できます。
（コマンドを使用するためには、[Heroku CLI のインストール](https://devcenter.heroku.com/articles/heroku-cli)が必要です）

```bash
$ heroku config:set DB_HOST=xxxxxxxxxxx
```

環境変数の一覧を表示して、設定できているか確認できます。

```bash
$ heroku config
```

MySQL DB の環境変数の設定は、こちらを参考にさせていただきました。
・[HerokuでMySQLを利用する方法](https://blog.nakamu.life/posts/heroku-mysql)

## ローカル環境の環境変数

.envファイルを用意して、そこに環境変数を記載していきます。
機密情報なので、gitignoreするのを忘れないようにしましょう。

```txt:.env
ENV = 'LOCAL'

# DB
DB_HOST = 'xxxxx'
DB_NAME = 'xxxxx'
DB_USER = 'xxxxx'
DB_PASSWORD = 'xxxxx'

# Session
SESSION_SECRET_KEY = 'xxxxx'
```

## アプリ側で環境変数を読み込む

app/web/config.py で以下のように環境変数を読み込んで、アプリ側の設定を行います。

```python:config.py
"""FlaskのConfigを提供する"""
import os
from os.path import join, dirname
from dotenv import load_dotenv

dotenv_path = join(dirname(__file__), '../.env')
load_dotenv(dotenv_path)


class Config:

    # Flask
    if (os.environ.get('ENV') == 'LOCAL'):
        DEBUG = True
    else:
        DEBUG = False

    # Session
    SECRET_KEY = os.environ.get('SESSION_SECRET_KEY')

    # SQLAlchemy
    SQLALCHEMY_DATABASE_URI = 'mysql+pymysql://{user}:{password}@{host}/{db_name}?charset=utf8'.format(**{
        'user': os.environ.get('DB_USER'),
        'password': os.environ.get('DB_PASSWORD'),
        'host': os.environ.get('DB_HOST'),
        'db_name': os.environ.get('DB_NAME')
    })
    SQLALCHEMY_TRACK_MODIFICATIONS = False
    SQLALCHEMY_ECHO = False


Config = Config
```

このように設定すると、Heroku環境でもローカル環境でも環境変数を読み込むことができます。
Pythonにおける環境変数の扱いについては、こちらを参考にさせていただきました。
・[【GitHub】に載せたくない環境変数の書き方 Python](https://qiita.com/hedgehoCrow/items/2fd56ebea463e7fc0f5b)

# 1アカウントにつき1日1リプライをどう実装するか

短時間に何度も自動返信するのは、Twitter API的によろしくないので、1アカウントにつき1日1リプライまでという仕様にしました。

一度リプライしたユーザをどうやって管理するか迷ったんですけど、

>また、Dynosは、Herokuで実行されているアプリケーションの正常性を維持するために、少なくとも1日1回再起動（循環）されます。ローカルファイルシステムへの変更はすべて削除されます。サイクリングは24時間ごとに発生します（さらに、アプリケーションのすべてのdynoが同時に再起動しないように、最大​​216のランダムな分）。

[Herokuの自動再起動](https://devcenter.heroku.com/articles/dynos#restarting)が24時間周期で行われるみたいなので、今回はworkerに指定したPythonファイル上（常時ループし続けている）のリストでリプライしたユーザを管理することにしました。
こうすることで、自動再起動されるたびにリストは初期化されるので、24時間ごとに再び自動返信を行うことができるようになります。

```python:twitter.py
replied_user_list = [] # 返信したユーザをリストで管理

# 24時間監視し続ける（プログラムが走り続ける）
for tweet in twitter_stream.statuses.filter(language='ja', track=tracking_text):
    # 以下、特定のワード（tracking_text）が含まれるツイートを検知したときの処理
    # ...
    # 自動返信を行うと、リストに追加する
    replied_user_list.append(user_id)
    # ...
```

# webがsleepするとworkerもsleepする問題

Heroku の無料プラン (Free Dyno) では、30分間Webアプリにアクセスがないと自動的にsleepします。
これに関しては、全く問題がないのですが、どうやら`web`がsleepすると`worker`もsleepしてしまうみたいです。[https://devcenter.heroku.com/articles/free-dyno-hours#dyno-sleeping]

>Apps that only utilise a free worker dyno do not sleep, because they do not respond to web requests. Be mindful of this as they may run 24/7 and consume from your pool of hours.

`worker`がsleepしてしまうと、自動返信Botが反応しなくなってしまうので、これはかなり問題です。

そこで、`web`をsleepさせないために以下のようにリクエストを定期的に送るなどの対処法があるみたいですが、
・[Herokuの無料dynoをスリープさせないで24時間稼働させる4つの方法](https://casualdevelopers.com/tech-tips/how-to-prevent-idling-of-free-dyno-on-heroku/)

これだと、`web`と`worker`のdynoがともに24時間稼働し続けることになってしまい、無料のdyno時間である1000時間/月をオーバーしてしまいます。

結果、`web`は必要なとき以外は停止しておくことにしました。（Heroku ダッシュボードで簡単に起動できる）
`worker`単体ではsleepすることがないので、これで自動返信Botが24時間稼働し続けられるようになりました。

# まとめ

pythonでのWebアプリ開発は初めてで、そういうことも書こうと思ったんですが、今回はHerokuへデプロイする上でつまづいたことを中心に書きました。

コーディング以外の知識をもっと身に付ける必要があるなと痛感しました。
今後はAWSの勉強をして、AWSソリューションアーキテクトのアソシエイトを取りたいと思います。

初めての技術ブログですので、読みにくい等の技術以外のアドバイスもどんどんよろしくお願いします。もちろん、技術的な指摘もよろしくお願いします。