【Heroku】Python Webサーバをおっ立てよう！BIN！

:::note warn
2022年11月28日より「Heroku」の無償プランが廃止になりました。
この記事は「Heroku」に無償プランがあったときの記事になります。現在は、有料プランになりますが、手順などはほぼ同じでできると思います。
Heroku の価格：https://jp.heroku.com/pricing
:::

# ➊ はじめに
**Heroku**は、無料でwebサーバをホスティングできるPaaS型サービスです。クレカの登録もなく無料で使わせてくれるので、安心してありがたく使わせて頂いています。「webアプリを作ってローカルで遊ぶだけでなく、全世界に公開したら何となくワクワクさも増えるよね」ということで、**Herokuにwebサーバをおっ立てましょう。BIN！**

# ➋ やること
**Python**コードのwebアプリを**Heroku**にデプロイします。

>🤖：はじめまして。ロボットの「ボロット」です。
>👧：挨拶はいいから、**Heroku**へのデプロイ方法を早く教えなさい！
>🤖：では早速………とは言え、いきなり**Heroku**へwebサーバをおっ立てようと焦ってはいけません。急がば回れ、ものには手順があります。少しずつ手順をこなしていきましょう。

# ➌ Herokuへのデプロイ方法
**Heroku**へのデプロイ方法は、「**Gitからデプロイ**」と「**Dockerでデプロイ**」の2種類があります。

>🤖：まずは、**Heroku**へのデプロイ方法について確認をしておきましょう。**Heroku**にデプロイする方法は、以下2つの方法があります。
>👧：要点のみにしてね。私は忙しいんだから。

## (1) Gitからデプロイ
Gitからデプロイする方法です。Gitとしては、`Heroku Gitリポジトリ`や`GitHubリポジトリ`を使用することができます。下図では、GitHubからデプロイする方法としています。
![github.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/405376/b2aa0eec-a1fc-d093-bfe5-7b8bc6c1555c.png)
Gitからデプロイをする場合は、足りないBuildPackを選んだり、そのための設定ファイルを作ったり、ビルド後のSlugサイズが**500MiBまで**と制約が多いです。

例えば、pipでInstallしたライブラリは、Defaultの「`heroku/python`」というBuildPackでそのままInstall可能ですが、condaを使用してInstallしたライブラリを入れた場合は、[Heroku Buildpacks](https://elements.heroku.com/buildpacks)から、`conda`等で検索を実行し、別途必要なビルドパックを探し、そのための設定ファイルも用意する必要があります。

>👧：制限がいっぱいあって、面倒くさそうね…

## (2) Dockerでデプロイ
Dockerでデプロイする方法です。コンテナレジストリ​を使用する方法とheroku.ymlを使用する方法があります。ここではコンテナレジストリを使用する方法を説明します。
![docker.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/405376/23bd3e74-20a2-870e-29b8-23b67fadc5ff.png)

>👧：こっちの方が、見た感じスマートね！

## (3) 結局どっちがいいの？

>👧：で？2つあるのは分かったから、結局どっちがいいの？
>🤖：結論から言うと、**Dockerでデプロイする方法の方が良いです**。

ボロット目線で恐縮ですが、以下にメリット・デメリットを簡単にまとめました。Dockerでデプロイする方法の方がメリットが大きいですし、Docker自体、大変便利で他でも色々お役に立つので、知らない方はこの際に覚えちゃっても良いかもしれません。

|デプロイ方法|メリット|デメリット|
|:-:|:-|:-|
|Gitからデプロイ|・Dockerの知識がなくてもデプロイ可能|・Slugサイズの制限がある<BR>・ビルド構築が大変になりがち<BR>・開発と本番環境が完全一致しない|
|Dockerでデプロイ|・Slugサイズの制限なし<BR>・Dockerイメージのビルドは柔軟性あり<BR>・開発と本番環境が同じ動作|・Dockerの知識が必要|

# ➍ python webアプリ
>👧：んで。気にはなっていたんだけど、webアプリどーすんの？私作るのヤダよ🙁。
>🤖：はい。ボロット自作webアプリである(`mnister`)を利用して、**Heroku Deploy Kit**をご用意しました。webアプリについては、簡単なコードなのでソースコードを参照するか、[過去の記事](https://qiita.com/PoodleMaster/items/33dae7a3b10dc117fbed)を参考にしてください😌。
>👧：ボロいwebアプリだかなんだか知れないけど、とりあえず、webアプリの内容には興味がないからいいわ😒。
>🤖：ハイ、リョウカイシマスタ😑………

## (1) mnisterとは
`mnister`とは、フロントエンドにJavaScript、バックエンドにPythonを用い、ブラウザ側で手書きした文字画像をサーバ側へ送信し推論して返却する自作webアプリケーションです。なおバックエンドのwebアプリケーションは、`Flask`というフレームワークを使用して作成されています。
![mnister.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/405376/04224b23-f661-f9b1-298c-fea239ee8036.png)

## (2) 要求事項
### ■要求事項
- python3.8+ ([python](https://www.python.org/downloads/) or [annaconda](https://www.anaconda.com/products/individual))
- [git](https://git-scm.com/downloads)
- [Docker / Docker desktop (about version 20.10.7)](https://www.docker.com/get-started)
- [Heroku Account](https://jp.heroku.com/home)
- [Heroku CLI](https://devcenter.heroku.com/ja/articles/heroku-cli)

### ■コマンド確認
以下、コマンドが正しく使えるか試しにバージョンでも確認しておきましょう。

>🤖：コマンドのパラメータは、大文字❛`V`❜と小文字❛`v`❜があるので注意してくださいね。
>👧：あら、似てるんだからいいじゃない！
>🤖：ダメです…

```bash
python -V
Python 3.8.11
```
```bash
git --version
git version 2.25.1
```
```bash
docker -v
Docker version 20.10.7, build f0df350
```
```bash
heroku -v
heroku/7.56.1 linux-x64 node-v12.21.0
```

## (3) webアプリInstall
mniserをgit cloneでローカルにDLします。

```bash
git clone https://github.com/PoodleMaster/mnister_HerokuDeployKit
```
ワークディレクトリを変更します。

```bash
cd mnister_HerokuDeployKit
```
ライブラリをインストールします。

```bash
pip install -r requirements.txt
```

# ➎ Herokuデプロイへの道
それでは、(1)～(4)まで順番に確認しながらやっていきましょう。
![heroku.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/405376/481a58f3-f5b4-e287-c725-6451590655a3.png)
## (1) Flaskで起動確認
まずは、pythonでwebアプリを簡単に作れるフレームワーク(`Flask`)を使用して、webアプリ(`mnister`)をLocalhostで実行します。

```bash
python server.py
```
```bash:実行ログ
 * Serving Flask app 'server' (lazy loading)
 * Environment: production
   WARNING: This is a development server. Do not use it in a production deployment.
   Use a production WSGI server instead.
 * Debug mode: off
 * Running on http://127.0.0.1:9000/ (Press CTRL+C to quit)
```

　👉 http://127.0.0.1:9000

>🤖：Flaskから起動したローカルサーバへは、アクセス出来ましたか？
>👧：ほーい、できたよ🙆！mnisterアプリも動作したよ😊。
>🤖：では、次はGunicornで起動しましょう！

## (2) Gunicornで起動確認
HTTPサーバー(`Gunicorn`)を使用して、webアプリフレームワーク(`Flask`)＋webアプリ(`mnister`)をLocalhostで実行します。

```bash:起動方法
gunicorn --bind=localhost:8000 server:app
```
```text:実行ログ
[2021-07-28 20:04:18 +0900] [18210] [INFO] Starting gunicorn 20.0.4
[2021-07-28 20:04:18 +0900] [18210] [INFO] Listening at: http://127.0.0.1:8000 (18210)
[2021-07-28 20:04:18 +0900] [18210] [INFO] Using worker: sync
[2021-07-28 20:04:18 +0900] [18212] [INFO] Booting worker with pid: 18212
```

　👉 http://127.0.0.1:8000

>🤖：Gunicornで起動したローカルサーバへは、アクセス出来ましたか？
>👧：同じことを何度もさせないで😤！でけたけど、何が違うの😡⁉
>🤖：FlaskのWebフレームワークは、便利なビルトインWebサーバーを備えているけど、一度に1つのリクエストしか処理できないんだ。Gunicornを使うと任意のPython Webアプリを並列して実行できるようになるんだ。とりあえず、次はdockerでコンテナ化してみよう！

## (3) Dockerコンテナで起動確認
`Docker`を使用してHTTPサーバ(`Gunicorn`)＋webアプリフレームワーク(`Flask`)＋webアプリ(`mnister`)をコンテナ化しLocalhostで実行します。

>👧：Dockerって、最近良く聞くけど、なんだか良く分からないんだけど😅………
>🤖：とりあえず、[こちらから](https://qiita.com/PoodleMaster/items/75edc1744b0a4986c1c8#3-%E3%81%8A%E5%8B%89%E5%BC%B7) Dockerの超基礎のお勉強をお願い致します😌。
>👧：ふぇーい😗

### (3-1) Dockerfile作成
`docker build`コマンドで、イメージを作るため「`Dockerfile`」という設定ファイルを作成します。ここに環境構築するためのコマンド類を記載します。

今回サンプルのwebアプリは、pipでInstallするライブラリばかりだったので、condaコマンドは不要でしたが、とりあえずcondaコマンドも実行できるイメージを利用することとします。[Docker Hub](https://hub.docker.com/)から「[`continuumio/anaconda3`](https://hub.docker.com/r/continuumio/anaconda3)」をPullするため、FROM指定します。

最後の行の「`--bind=0.0.0.0`」というのは、全てのIPアドレスからの受信を許可するという意味です。

```dockerfile:Dockerfile
# base
FROM continuumio/miniconda3:4.9.2

# proxy配下で実行している場合は、以下を有効化しProxyサーバを指定してください。
## apt-get
# ENV http_proxy  "http://proxy.server.com:8080"
# ENV https_proxy "http://proxy.server.com:8080"

## pip
# ENV HTTP_PROXY  "http://proxy.server.com:8080"
# ENV HTTPS_PROXY "http://proxy.server.com:8080"

# init
RUN apt-get update --allow-releaseinfo-change
RUN apt-get install -y build-essential
RUN apt-get install -y libjpeg-dev
RUN pip install --upgrade pip

# install conda package
# condaコマンドも使えます。
# RUN conda install gcc_linux-64 gxx_linux-64

# install pip package
RUN pip install Pillow==7.2.0
RUN pip install tensorflow-cpu==2.5.0
RUN pip install Flask==2.0.1
RUN pip install gunicorn==20.1.0

# app
WORKDIR /bg
COPY server.py .
COPY colab_mnist.hdf5 .
COPY static static/
COPY templates templates/

EXPOSE 5000
CMD ["gunicorn", "--bind=0.0.0.0:5000", "server:app"]
```

### (3-2) dockerイメージの作成
Dockerfileが作成できたら、dockerイメージを作成しましょう。

```bash
sudo docker build -t img-mnister .
```
```text:実行結果
[+] Building 254.8s (20/20) FINISHED
 => [internal] load build definition from Dockerfile                                                               0.0s
 => => transferring dockerfile: 915B                                                                               0.0s
 => [internal] load .dockerignore                                                                                  0.0s
 => => transferring context: 2B                                                                                    0.0s
 => [internal] load metadata for docker.io/continuumio/miniconda3:latest                                           3.4s
 => [auth] continuumio/miniconda3:pull token for registry-1.docker.io                                              0.0s
 => [internal] load build context                                                                                  0.1s
 => => transferring context: 7.80MB                                                                                0.1s
 => [ 1/14] FROM docker.io/continuumio/miniconda3@sha256:592a60b95b547f31c11dc6593832e962952e3178f1fa11db37f43a2a  0.0s
 => CACHED [ 2/14] RUN apt-get update                                                                              0.0s
 => CACHED [ 3/14] RUN apt-get install -y build-essential                                                          0.0s
 => CACHED [ 4/14] RUN apt-get install -y libjpeg-dev                                                              0.0s
 => CACHED [ 5/14] RUN pip install --upgrade pip                                                                   0.0s
 => CACHED [ 6/14] RUN pip install Pillow==7.2.0                                                                   0.0s
 => [ 7/14] RUN pip install tensorflow-cpu==2.5.0                                                                239.6s
 => [ 8/14] RUN pip install Flask==2.0.1                                                                           2.8s
 => [ 9/14] RUN pip install gunicorn==20.1.0                                                                       2.5s
 => [10/14] WORKDIR /bg                                                                                            0.0s
 => [11/14] COPY server.py .                                                                                       0.0s
 => [12/14] COPY colab_mnist.hdf5 .                                                                                0.1s
 => [13/14] COPY static static/                                                                                    0.0s
 => [14/14] COPY templates templates/                                                                              0.0s
 => exporting to image                                                                                             6.1s
 => => exporting layers                                                                                            6.1s
 => => writing image sha256:52fe0852439e2126eab7663362b6ceba0925d8f5b05c01e3497c770b9216f189                       0.0s
 => => naming to docker.io/library/img-mnister             
```

>👧：あれ？さっき作った`Dockerfile`は使用しないの？
>🤖：`docker build`コマンドの最後の`.`が、`Dockerfile`のある場所を表しています。つまり、カレントディレクトリにある`Dockerfile`は使用しています。

### (3-3) dockerコンテナの作成・開始
`docker run`コマンドは、「コンテナの作成を行う`docker create`コマンド」と「コンテナの開始を行う`docker start`コマンド」を一括で実施するコマンドです。dockerイメージが作成できたら、`docker run`コマンドで、dockerコンテナを作成・開始させましょう。

```bash
sudo docker run -it --publish=5000:5000 --name="con-mnister" img-mnister
```
```text:実行結果
[2021-07-30 13:41:41 +0000] [1] [INFO] Starting gunicorn 20.1.0
[2021-07-30 13:41:41 +0000] [1] [INFO] Listening at: http://0.0.0.0:5000 (1)
[2021-07-30 13:41:41 +0000] [1] [INFO] Using worker: sync
[2021-07-30 13:41:41 +0000] [8] [INFO] Booting worker with pid: 8
```
　👉 http://127.0.0.1:5000

>🤖：dockerコンテナのサーバへは、アクセス出来たかな？
>👧：またまた、できました🙆！ただ、dockerコマンドがパラメータが多くて、少々メンドイわね🙁ソースコード変更したりすると、何度も同じパラメータを打つことにならない？そーゆーの私、指が痛くてイヤなんですけど😠💨
>🤖：コマンド打ちが、少し楽になる方法を後で教えます。とりあえず、次いきましょう。いよいよ**Heroku**へデプロイですよ！

## (4) Herokuへデプロイ
localhostでアクセスすることができたら、次は**Heroku**へデプロイしましょう！

### (4-1) Dockerfile更新
**Heroku**の注意点ですが、『Web プロセスは、**Heroku**により設定された「​`$PORT`」上でHTTPトラフィックをListenする必要があります。​Dockerfile内の​EXPOSEは考慮されません』とのことなので、Listen Portを「`$PORT`」へ変更する必要があります。

```dockerfile:Dockerfile
# Dockerfileの最終行を次に書き換える
CMD gunicorn --bind 0.0.0.0:$PORT server:app
```
### (4-2) Heroku CLI
Container Registr にログインします。

```bash
sudo heroku container:login
```
```text:実行結果
Login Succeeded
```

`mnister-web`という名称で、**Herokuアプリ**を作成します。
ユニークである必要があるので、**Herokuアプリ名称**が他の方に取られていたら、別名にしてください。

```bash
sudo heroku create mnister-web
```
```text:実行結果
Creating ⬢ mnister-web... done
https://mnister-web.herokuapp.com/ | https://git.heroku.com/mnister-web.git
```

イメージをビルドし、Container Registryにプッシュします。

```bash
sudo heroku container:push web -a mnister-web
```
```text:実行結果
=== Building web (/home/popcorn/mnister_HerokuDeployKit/Dockerfile)
[+] Building 2.1s (19/19) FINISHED
 => [internal] load build definition from Dockerfile                                                               0.0s
 => => transferring dockerfile: 1.02kB                                                                             0.0s
 => [internal] load .dockerignore                                                                                  0.0s
 => => transferring context: 2B                                                                                    0.0s
 => [internal] load metadata for docker.io/continuumio/miniconda3:latest                                           1.8s
 => [internal] load build context                                                                                  0.1s
 => => transferring context: 7.80MB                                                                                0.1s
 => [ 1/14] FROM docker.io/continuumio/miniconda3@sha256:592a60b95b547f31c11dc6593832e962952e3178f1fa11db37f43a2a  0.0s
 => CACHED [ 2/14] RUN apt-get update                                                                              0.0s
 => CACHED [ 3/14] RUN apt-get install -y build-essential                                                          0.0s
 => CACHED [ 4/14] RUN apt-get install -y libjpeg-dev                                                              0.0s
 => CACHED [ 5/14] RUN pip install --upgrade pip                                                                   0.0s
 => CACHED [ 6/14] RUN pip install Pillow==7.2.0                                                                   0.0s
 => CACHED [ 7/14] RUN pip install tensorflow-cpu==2.5.0                                                           0.0s
 => CACHED [ 8/14] RUN pip install Flask==2.0.1                                                                    0.0s
 => CACHED [ 9/14] RUN pip install gunicorn==20.1.0                                                                0.0s
 => CACHED [10/14] WORKDIR /bg                                                                                     0.0s
 => CACHED [11/14] COPY server.py .                                                                                0.0s
 => CACHED [12/14] COPY colab_mnist.hdf5 .                                                                         0.0s
 => CACHED [13/14] COPY static static/                                                                             0.0s
 => CACHED [14/14] COPY templates templates/                                                                       0.0s
 => exporting to image                                                                                             0.0s
 => => exporting layers                                                                                            0.0s
 => => writing image sha256:c3adae5e069a951b99b0fa50a708c0d92045e36b49801ac164882b62819264a8                       0.0s
 => => naming to registry.heroku.com/mnister-web/web                                                           0.0s

Use 'docker scan' to run Snyk tests against images to find vulnerabilities and learn how to fix them
=== Pushing web (/home/popcorn/mnister_HerokuDeployKit/Dockerfile)
Using default tag: latest
The push refers to repository [registry.heroku.com/mnister-web/web]
fc5ee76742ef: Pushed
45ee37fa32e6: Pushed
95dd8dd0f97d: Pushed
d5c9db00cffd: Pushed
cf6f7fd36bf7: Pushed
32c94721f3b8: Pushed
d2f49e7a3555: Pushed
4a20a88105aa: Pushed
eae386b112f7: Pushed
e9ca61d9199c: Pushed
83ea5b6fba93: Pushed
fc58f36c22d8: Mounted from mnister-web/web
4067850c57b2: Mounted from mnister-web/web
e7a951258ef9: Mounted from mnister-web/web
692e8918cbe0: Mounted from mnister-web/web
814bff734324: Mounted from mnister-web/web
latest: digest: sha256:d7a25246b9372c6bd7b9da9b69da75f43dd4af4e1bdd4a329a5dc77fa54314a3 size: 3687
Your image has been successfully pushed. You can now release it with the 'container:release' command.
```

イメージからコンテナをwebサービスにリリースします。

```bash
sudo heroku container:release web -a mnister-web
```
```text:実行結果
Releasing images web to app-mnister-web... done
```

　👉 https://mnister-web.herokuapp.com/

>🤖：各々自分で作成した**Herokuアプリ名**のURLへアクセスしてくださいね。ところで、**Heroku**上のwebサーバへアクセス出来たかな？
>🙆：うんはい。できました！簡単ですな😉ドヤッ
>🤖：おめでとうございます👏パチパチ

# ➏ おまけ　　　(`Docker Compose CLI`の利用)
ソースコードを修正した場合などは、➎-(3)のイメージ作成とコンテナ開始を何回かやり直すこととなると思いますが、その度に`docker build`コマンド、`docker run`コマンドをシコシコ叩くのは面倒ですよね。いいのあります。`docker compose`コマンドを使うと、これらをまとめて実行することができます！

>👧：あれ⁉`docker-compose`コマンドなら使ったことあるような🤔？
>🤖：ハイフンなしの`docker compose`コマンドの方が新しいです。`docker compose`コマンドと`docker-compose`コマンドは、ほぼ同じもなので、気にしないでください。

## (1) 設定ファイル作成
イメージ名「`img-mnister`」、コンテナ名「`con-mnister`」、アクセスポート「`5000番`」を使うように、設定ファイルを作成します。

```dockerfile:docker-compose.yaml
version: '3'
services:
  web:
    build: .
    ports:
    - "5000:5000"
    image: img-mnister
    container_name: con-mnister
```

`docker-compose.yaml`をもっと勉強したい方は、「[こちら](https://docs.docker.jp/compose/compose-file.html)」へどうぞ。

## (2) 既存コンテナ停止と削除
➎-(3-3)で同名のコンテナを既に作っているので、ここでいきなり次のSTEPである`docker compose up`コマンドを実行するとエラーとなります。

```text
sudo docker compose up
[+] Running 1/2
 ⠿ Network mnister_HerokuDeployKit_default  Created                                                                     0.6s
 ⠏ Container con-mnister               Creating
Error response from daemon: Conflict. The container name "/con-mnister" is already in use by container "4e8a871238c839525e8a61b29687451c01cfd6a34f0f9a1944acddd66f7884f7". You have to remove (or rename) that container to be able to reuse that name.
```
そのため、先に`docker rm`コマンドで「`con-mnister`」コンテナを削除しておきます。ちなみに、コンテナを削除するためには停止をしておく必要があります。コンテナを停止していない場合は、`docker stop`コマンドで停止させます。

- 「`con-mnister`」コンテナ停止

```bash
sudo docker stop con-mnister
```

- 「`con-mnister`」コンテナ削除

```bash
sudo docker rm con-mnister
```

## (3) イメージ作成＆コンテナ作成・開始
`sudo docker compose up`コマンドで、イメージ作成とコンテナを作成・開始を実施しますが、既にイメージがある場合はビルドは行いません。また、既にコンテナがある場合も作成は行わず、コンテナの開始のみ実施します。

初回や、ソースコードを変更していない状態での再起動などを行う場合に、以下のコマンドを使用します。

```bash:
sudo docker compose up
```

ソースコード等を修正してビルドが必要な場合は、強制ビルドのオプションをつければ、イメージ作成、コンテナ作成・開始を実施します。

```bash:
sudo docker compose up --build
```

>🤖：`docker compose`コマンドは、他にももっと色々なことができますので、色々調べて見ると約に立つものがあると思います。
>👧：時間があったら調べておきます🤔。
>🤖：やならないな😒。

# ➐ 以上
webアプリを作ってもlocalhostのみで運用していては、なんか夢がないですよね。やっぱり自分が考えたwebアプリが全世界の人々が使えるというワクワクを**Heroku**は叶えてくれます。ただ、どうやって全世界の人々へ使って貰えるように発信していくのかは別問題ですが…自信を持って本当に良いサービスであれば、SNSなども利用して世界へ発信していくと良いかもしれませんね。

お疲れ様でした！