Github管理下のDjangoプロジェクト(mysql使用)をherokuに構築する(Part1)

:::note warn
2023/02/01現在、herokuは無料プランの提供を終了しており、有料プランのみの提供となっています。
:::

無料でサーバーを立てられる[heroku](https://www.heroku.com)で[Django](https://www.djangoproject.com)を使ったアプリケーションを構築しようとしたところ、たくさんのトラップに引っかかったので、備忘録としてまとめることにしました。

# 0. 前置き
## 目的

1. herokuにDjangoアプリケーションを立てたい
2. リポジトリはGitHubに置いておきたい
3. Djangoで使うDBはMySQLがいい

今回は以上の目的を達成するための流れを一通り説明します。

## 前提知識

- ターミナルの基本操作ができること
- pythonの環境を整えていること
- djangoの基本的な使い方を知っていること

## ローカル環境

| 環境 | 項目 |
|:-|:-|
| OS | macOS BigSur 11.5.2 |
| Python | 3.9.7, venv(環境管理), pip(パッケージ管理) |
| Django | 3.2.7 |
| heroku CLI | heroku/7.59.0 darwin-x64 node-v12.21.0 |


## その他
- プロジェクト名は`[heroku-pj]`または`[heroku_pj]`と記載しています。各自の設定に置き換えてください
- herokuコマンドに必須の`-a`オプションは省略しているので、各自で補完してください


# 1. Pythonの設定
まずはローカルの環境を整えます。

## 1-1. 仮想環境(venv)の用意
※Pythonの環境整備は宗派が分かれるので参考程度で。

```zsh:zsh
% python -m venv .venv
% source .venv/bin/activate
# 警告がしつこく出るのでupgradeする
% pip install -U pip
```

## 1-2. 必要なライブラリのインストール
必要なライブラリは以下の通りです。(バージョンは執筆当時の最新版です)

```text:requirements.txt
asgiref==3.4.1
dj-database-url==0.5.0
Django==3.2.7
django-heroku==0.3.1
gunicorn==20.1.0
mysql==0.0.3
mysqlclient==2.0.3
psycopg2==2.9.1
pytz==2021.1
sqlparse==0.4.1
whitenoise==5.3.0
```

- `psycopg2`のインストールがうまくいかない場合は以下のページを参考にしてください
   - https://dev.classmethod.jp/articles/mac-psycopg2-install/


## 1-3. djangoプロジェクトの作成
今回はプロジェクトのルートをgit管理したいので、作ります。
アプリケーションは今回は作らなくても良いです。

```zsh:zsh
% django-admin startproject [heroku_pj]
% cd [heroku_pj]
```

## 1-4. 各種ファイルの作成
### .gitignore
必要なものがあれば適宜追記してください。

```text:.gitignore
__pycache__
.DS_Store
local.py
db.sqlite3
```


### requirements.txt
以下のコマンドで生成します(上記の[requirements.txt](#1-2-必要なライブラリのインストール)をコピペしても良いですが、最新版にしておくことをお勧めします)

```zsh:zsh
% pip freeze > requirements.txt
```

### Procfile
herokuのビルドに必要です。

```text:Procfile
web: gunicorn heroku_pj.wsgi --log-file -
```

### runtime.txt
heroku上のPythonバージョン指定に必要です。
バージョンは[こちらのページ](https://devcenter.heroku.com/ja/articles/python-support#supported-runtimes)で最新の対応状況を確認してください。

```text:runtime.txt
python-3.9.7
```

## 1-5. settings.pyの分離
開発環境(ローカル)と本番環境(heroku上)ではデータベース等の設定が異なりますので、ファイルを分離しておいた方が良いです。
本記事では分離することを前提としてコードを書いているので、分離しない人は1-5, 1-6をスキップしてください。

### 1-5-1. フォルダ構造

- [heroku_pj]-ルート
   - [heroku_pj]
      - settings
         - \_\_init\_\_.py
         - base.py
         - local.py
         - production.py
      - \_\_init\_\_.py
      - asgi.py
      - urls.py
      - wsgi.py
   - manage.py
   - (以下省略)

### 1-5-2. base.py
デフォルトの`settings.py`から以下の項目を除きます。

- `DEBUG`, `ALLOWED_HOSTS`, `SECRET_KEY`, `DATABASES`
  - `SECRET_KEY`は中身をコピー&メモしておくこと(あとで使います)

また、以下の項目を変更しておきます(差分のある項目のみ表示)

```python:[heroku_pj]/settings/base.py
# settingsの階層が1つ深くなるのでさらに親ディレクトリを取る
BASE_DIR = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))

# whitenoiseのミドルウェアをこの位置に追加する
MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'whitenoise.middleware.WhiteNoiseMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    (省略)
]

LANGUAGE_CODE = 'ja-jp'

TIME_ZONE = 'Asia/Tokyo'

# 以下はwhitenoiseとcollectstaticに関連して必要な設定
STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles')
STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage'
```

- 参考: [whitenoiseとは](https://devcenter.heroku.com/ja/articles/django-assets)

### 1-5-3. local.py
`BASE_DIR`は `base.py`で定義しているので明示する必要はありませんが、気持ち悪い人は書き加えてください。

```python:[heroku_pj]/settings/local.py
import os

from .base import *


DEBUG = True

# ここは各自でよしなにしてください
ALLOWED_HOSTS = ['127.0.0.1',]

SECRET_KEY = '[元のSECRET_KEYをそのままコピペ]'

#settings.pyからそのままコピー
DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.sqlite3',
        'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
    }
}
```

### 1-5-4. production.py
`BASE_DIR`については同上。
また、多くの値を環境変数から取得していますが、その設定については後述します。

```python:[heroku_pj]/settings/production.py
import os
import django_heroku
import dj_database_url

from .base import *


DEBUG = False

# ['*']でもいい
ALLOWED_HOSTS = ['127.0.0.1', '.herokuapp.com']

# 秘密鍵はファイルに書かない
SECRET_KEY = os.environ['SECRET_KEY']

DATABASES = {
    'default': {
        # localと異なり、mysqlのエンジンを用いる
        'ENGINE': 'django.db.backends.mysql',
        'OPTIONS': {
            # これを設定しないとMySQLのWARNINGが出る
            'init_command': "SET sql_mode='STRICT_TRANS_TABLES'",
        },
        # 以下はあとで値を設定する
        'NAME': os.environ['DB_NAME'],
        'USER': os.environ['DB_USERNAME'],
        'PASSWORD': os.environ['DB_PASSWORD'],
        'HOST': os.environ['DB_HOSTNAME'],
        'PORT': os.environ['DB_PORT'],
    }
}

# 補足参照
db_from_env = dj_database_url.config(conn_max_age=600, ssl_require=False)
DATABASES['default'].update(db_from_env)
django_heroku.settings(locals(), databases=False)
```

- MySQLを使わず(heroku標準の)PostgreSQLを使用したい場合、`DATABASES['default']['ENGINE']`の値は以下に変更になります
  - `'django.db.backends.postgresql_psycopg2'`


- `dj_database_url.config()`において、`ssl_require=False`を指定しないと`django_heroku.settings()`でエラーが出ます
  - これはdjango_herokuがsslをサポートしていないのに、読み込み時に勝手にssl設定を読み込もうとすることが原因のようです
  - 参考: [Django Heroku, server does not support SSL, but SSL was required](https://stackoverflow.com/questions/64076840/django-heroku-server-does-not-support-ssl-but-ssl-was-required)


## 1-6. manage.py/wsgi.pyの書き換え
開発環境と本番環境で適切なsettingsを読み込むために必要です。

### 1-6-1. manage.py
書き換え前(抜粋、main関数の最初)

```python:manage.py
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "[heroku_pj].settings")
```

書き換え後

```python:manage.py
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "[heroku_pj].settings.local")
```

### 1-6-2. [heroku_pj]/wsgi.py
書き換え前(抜粋)

```python:manage.py
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "[heroku_pj].settings")
```

書き換え後

```python:manage.py
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "[heroku_pj].settings.production")
```


ここまで長くなったので続きは[別記事](https://qiita.com/tanipen/private/1349ec39f3fc25a0a630)へ。