nikkie-ftnextの日記

イベントレポートや読書メモを発信

この物語は設定ファイルを分割したDjangoアプリをPythonAnywhereにデプロイするのに挑んだ闘いの記録である

はじめに

うごきましておめでとう、nikkieです。

Python製Webアプリケーションフレームワークの1つ、Django(ジャンゴ)。
Djangoの入門チュートリアルの1つ、Django Girls Tutorialでは、Djangoで作ったアプリをPythonAnywhereにデプロイできます。
チュートリアルから離れ設定ファイルを分割したアプリをPythonAnywhereにデプロイするのにハマり、対処するという経験を年末にしました。

目次

そもそもことの始まりは…

かつて(2019年に)PythonAnywhereにデプロイしたアプリがありました1

https://ftnext.pythonanywhere.com/

"dangermouse"というシステムイメージを使っていたのですが、2022年後半に「dangermouseはend-of-lifeなのでシステムイメージを変えてほしい」というメールが来ました。

ヘルプページに沿ってシステムイメージを変える2と、ページは表示されなくなりました(何かしたら壊れた!)。
エラーメッセージ「Py_Initialize: Unable to get the locale encoding」で調べると、Forumの中に同じメッセージについての記載が見つかります。

Fatal Python error: Py_Initialize: Unable to get the locale encoding and ModuleNotFoundError : Forums : PythonAnywhere

You need to rebuild your web app's virtual env now.

(意訳) Webアプリの仮想環境を再ビルドする必要があります。

そこでPythonAnywhereでの仮想環境再構築に取り組みました。
そのときの私はまだ知らなかったのです、設定ファイルを分割したDjangoアプリをPythonAnywhereでデプロイするのが大変だということを…🙀

まとめ

設定ファイルを分割したDjangoアプリをPythonAnywhereでデプロイするノウハウは以下に素晴らしいまとめがあります。

django-tutorial-workshop-memo.md · GitHub

このGistは必見です。
このGistを参照しながら、自分のアプリがデプロイできない事象に対処しました。
この後に続くのは、その闘いの記録です。

闘いを終えての私の感覚としては、設定ファイルを分割したら、Django Girls Tutorial記載のpa_autoconfigure_django.pyでのPythonAnywhereへのデプロイはやらない方がよいというものです。
開発しやすくするために設定ファイルを分けているのに、ここまでデプロイしづらいというのは、デメリットがメリットを大きく上回ってしまっていて、ぶっちゃけ割に合わないと感じます。

設定ファイルを分割したDjangoアプリをデプロイするなら

  • PythonAnywhereは使わないのがオススメ
  • PythonAnywhereでもレガシーなpa_autoconfigure_django.pyの代わりに新しいpaコマンドを使えばカイゼンしているかもしれません(どなたかチャレンジしてみて😉)

アウトプットする行動と矛盾してますが、このアウトプットが活用されない状態があるべきと考えます。

Django Girls Tutorial 「デプロイ!」

かつてDjango Girls Tutorialに取り組んだときは「よく分からない(ブラックボックス)けど、できた!🙌」という感覚でしたが、今回ハマったことでブラックボックスの蓋を開けました。

「PythonAnywhere でサイトを設定する」では2つのことをやっています。

  1. ライブラリpythonanywhereのインストール(pip3.6 install --user pythonanywhere
  2. pythonanywhereが提供するpa_autoconfigure_django.pyの実行(pa_autoconfigure_django.py --python=3.6 https://github.com/<your-github-username>/my-first-blog.git
    • この中で仮想環境も構築されます

ライブラリpythonanywhere

PythonAnywhereのヘルパーツールがライブラリとしてPyPIで配布されているんです!

ソースコードはこちら(このあと中を覗きます):

「Usage」を見ると、2つの方法で使えるとあります。

  1. コマンドラインインターフェース(paコマンド)
  2. Legacy scripts

Django Girls TutorialはLegacy scripts(の1つのpa_autoconfigure_django.py)を使っています。

レガシースクリプトpa_autoconfigure_django.pyがやっていること

https://github.com/pythonanywhere/helper_scripts/blob/v0.10.3/scripts/pa_autoconfigure_django.py

モジュールレベルのdocstringで以下のように説明されています:

  • リポジトリのダウンロード
  • virtualenvを作り、Djangoをインストール
    • requirements.txtが見つかったらインストールに使われる
    • PythonAnywhereは標準ライブラリのvenvではなくてvirtualenvのようです
  • APIを介してwebappを作る
    • これはPythonAnywhereの話でしょうか?
  • DjangoWSGI設定ファイルを作る
  • 静的ファイルの設定を追加する

実はこの内容はDjango Girls Tutorialにも書いてあるんですよね(「実行しているところを見れば、何をしているのかわかるでしょう。」)。

ソースコードでは、main関数にてDjangoProjectのメソッドを呼び出しています。

# https://github.com/pythonanywhere/helper_scripts/blob/v0.10.3/scripts/pa_autoconfigure_django.py#L27-L44
def main(repo_url, branch, domain, python_version, nuke):
    domain = ensure_domain(domain)
    project = DjangoProject(domain, python_version)
    project.sanity_checks(nuke=nuke)
    project.download_repo(repo_url, nuke=nuke),
    project.ensure_branch(branch),
    project.create_virtualenv(nuke=nuke)
    project.create_webapp(nuke=nuke)
    project.add_static_file_mappings()
    project.find_django_files()
    project.update_wsgi_file()
    project.update_settings_file()
    project.run_collectstatic()
    project.run_migrate()
    project.webapp.reload()
    print(snakesay(f'All done!  Your site is now live at https://{domain}'))
    print()
    project.start_bash()

仮想環境を再構築して、設定ファイルを分割したDjangoアプリをPythonAnywhereで動かすまで

設定ファイルを分割したDjangoアプリ

以下のように分割しています3

settings/
├── __init__.py
├── base.py
├── local.py
└── production.py

先日のまとめに即して言うと

  • 共通設定ファイル + 環境ごとの設定
  • 環境変数の扱いはpython-dotenv4
    • あんまり環境変数に寄せきれていない(SECRET_KEYくらい)5

pa_autoconfigure_django.pyが通るまで(仮想環境再構築)

pythonanywhereソースコードを編集してから、以下のコマンドを実行しました:

$ DJANGO_SETTINGS_MODULE=mysite.settings.production pa_autoconfigure_django.py --python=3.9 https://github.com/ftnext/djangogirls-nextstep.git --nuke --branch=feature/deploy-pythonanywhere
pythonanywhereソースコード編集

pa_autoconfigure_django.pyから呼び出されるproject.find_django_files()なのですが、settings.py以外の名前の設定ファイルを見つけられません
設定ファイルを分割したDjangoアプリをPythonAnywhereでデプロイするには、インストールしたpythonanywhereソースコードの編集が必要になります(必見Gistの24-4-1)。
PythonAnywhereのシェル(ブラウザから使える)でvimを使って編集しました。

https://github.com/pythonanywhere/helper_scripts/blob/v0.10.3/pythonanywhere/django_project.py#L82

-    self.settings_path = next(self.project_path.glob('**/settings.py'))
+    self.settings_path = next(self.project_path.glob('**/settings/production.py'))

ここはハードコードなので、インストールされたライブラリに手を入れるしかなさそうです。
引数で指定できると非常に使いやすくなるのですが…6paコマンドだと改善されているのかな?)

本番設定の編集(settings/production.py
  • STATICFILES_DIRSは定義しない(必見Gistの17-1参照。理由もあります)
  • SECRET_KEY環境変数から読み取る実装としています
import os

from dotenv import load_dotenv

load_dotenv()
SECRET_KEY = os.getenv('SECRET_KEY')
pa_autoconfigure_django.pyの引数

pa_autoconfigure_django.py --helpで確認できます

  • --nuke:仮想環境を再構築するために必要です
    • 仮想環境の名前は<your-pythonanywhere-domain>.pythonanywhere.com(例:ftnext.pythonanywhere.com
    • --nukeを指定すると、すでにできている仮想環境があっても新しく作り直します
  • --branch:デプロイ時点では https://github.com/ftnext/djangogirls-nextstep に複数のブランチがあったので指定する必要がありました
環境変数DJANGO_SETTINGS_MODULEの指定

pa_autoconfigure_django.pyの処理が進むと、project.run_collectstatic()project.run_migrate()python manage.py collectstaticpython manage.py migrate相当のことが実施されます(subprocessを使った実装)。
collectstaticmigrateのときに、本番環境用の設定ファイルを使うように指定する必要があります。
そこでDJANGO_SETTINGS_MODULEを使いました。

  • manage.pyを編集してデフォルトで本番設定を参照するようにしているやり方もあります(必見Gistの19-2
  • --settings引数はpa_autoconfigure_django.pyがサポートしていなさそうでした(manage.pyに渡せたらもっと便利ですね)

pa_autoconfigure_django.pyが通った後

ページを表示するにはもう少し手順が必要です。

pa_autoconfigure_django.pyで作られたWSGIファイルのDJANGO_SETTINGS_MODULEの編集

必見Gistではテンプレートの元を直していますが(24-4-3)、ソースコードの修正は最小限にしたかったので、テンプレートからコピーされたファイルを修正して対処しました。

project.update_wsgi_file()/var/www/<your-pythonanywhere-domain>_pythonanywhere_com_wsgi.pyができています。
このファイルで設定モジュールのimportができていません。
編集して対処しました(編集した後はWebコンソールの「Web」タブからReloadが必要です)。

# Set environment variable to tell django where your settings.py is
- os.environ['DJANGO_SETTINGS_MODULE'] = 'settings.settings'
+ os.environ['DJANGO_SETTINGS_MODULE'] = 'mysite.settings.production'

WSGIファイルでsys.pathを編集

/var/www/<your-pythonanywhere-domain>_pythonanywhere_com_wsgi.pyはまだ修正が必要です。

# djangoはprojectやappが入ったディレクトリの名前
# ref: https://github.com/ftnext/djangogirls-nextstep/tree/master/django
settings_path = '/home/ftnext/ftnext.pythonanywhere.com/django'
sys.path.insert(0, settings_path)

djangoディレクトリ以下にあるプロジェクトmysiteやアプリmysiteaccountsがimportできるように変更しました。

環境変数を読み込むためのWSGIファイルの編集

これが/var/www/<your-pythonanywhere-domain>_pythonanywhere_com_wsgi.pyの最後の修正です。
SECRET_KEY環境変数から読み込みます。
python-dotenvを使っているのは、開発時に以下のドキュメントを参照したためと思われます。

Webアプリケーションを動かすため、<your-pythonanywhere-domain>.pythonanywhere.com7の直下に.envファイルを置き、そこにSECRET_KEYの値を書きます(環境変数として設定)。

この.envを読み込むために、WSGIファイルに追加します。

from dotenv import load_dotenv
project_folder = os.path.expanduser('~/<your-pythonanywhere-domain>.pythonanywhere.com')
load_dotenv(os.path.join(project_folder, '.env'))

ドキュメントではbashでも環境変数を読み込むために(virtualenvの)postactivateスクリプトの設定も案内されています(今回はスキップ)。

PythonAnywhereの「Web」タブで「Static files」を変更

デフォルト値から変える必要がありました。
これをやらないとCSSが当たらないですし、アップロードした画像ファイルも参照できません。

  • URL /static/ を /home/<your-pythonanywhere-domain>/<your-pythonanywhere-domain>.pythonanywhere.com/django/static に変更
  • URL /media/ を /home/<your-pythonanywhere-domain>/<your-pythonanywhere-domain>.pythonanywhere.com/django/media に変更

これが必要な理由はproject.update_settings_file()によってsettings/production.pyの末尾に以下が追記されているからです。

# BASE_DIR は /home/<your-pythonanywhere-domain>/<your-pythonanywhere-domain>.pythonanywhere.com/django
MEDIA_URL = '/media/'
STATIC_ROOT = os.path.join(BASE_DIR, 'static')
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')

update_settings_fileの実装は、言ってみればsedコマンドでテキストファイルを編集するような実装です8
テキストファイルの操作となっているので、共通設定からのimportは無視されます。
これによりPythonAnywhereのルールが強制適用された形になっていて、「Static files」の設定値を変えて対処したということです。

WSGIファイルの最終形

/var/www/<your-pythonanywhere-domain>_pythonanywhere_com_wsgi.pyは最終的には以下のようになっています。

終わりに

PythonAnywhereでシステムイメージを変えた後、仮想環境を再構築し、アプリが動くようになりました。
設定ファイルを分割したことで、PythonAnywhereでの仮想環境の再構築には

  • インストールしたpythonanywhereソースコードの編集
  • 作成されたWSGIファイルの編集
  • Webコンソールで「Static files」の編集

といった操作が必要となっていました。
必見Gistがなければ再構築はもっとずっと難航したと思われます。
このアウトプットには大変感謝しています。

django-tutorial-workshop-memo.md · GitHub

pa_autoconfigure_django.py、これはDjango Girls Tutorialの範囲では便利ですが、Tutorialから離れるとむしろデメリットに感じてしまいます。
一番つらかったのは、ライブラリのユーザがソースコードを編集しないといけないことです(ハードコードの代償)。
また、コケるたびに最初から再実行したのですが、途中のステップから再開できるような引数があると非常に助かるなあと思いました。
正しい使い方を簡単に、誤った使い方を困難に」で言うと、Tutorialから離れたとき、誤った使い方が容易にできてしまい、正しい使い方はこの1エントリ分くらい大変なので、このスクリプトには伸びしろしかありません!
私の中でこのスクリプトカイゼンすることはあまり優先度が高くないのですが、もしアイデアが浮かんだ方がいたらぜひプルリクエストを送ってみてください!

もし次にやる機会があるならcookiecutter-djangoで作ったアプリケーションを以下に沿ってデプロイしたいですね。
pa_autoconfigure_django.pyこわい…



  1. このアプリは2019年のDjangoCongress JPのトークの準備で作成したものです。登壇報告 | #djangocongress にて、Django Girls Tutorialの次に取り組みたいトピックについて共有してきました - nikkie-ftnextの日記
  2. しばらく変えなくていいように最新のhaggisイメージに変えました。システムイメージについては Batteries included: PythonAnywhere で知ることができます
  3. https://github.com/ftnext/djangogirls-nextstep/tree/72b05b415e782247d9d8ca1e5f8a68eb73c9e1ba/django/mysite/settings
  4. django-environではないのも辛かった要因の1つなのかもしれません
  5. PyCon JPスタッフ向けのレビューアプリ開発は2020年からなので、Djangoに関しては練習中(伸びしろ豊富)の時期です
  6. 同様の指摘がissueでもされていそうです。Handle custom django settings. · Issue #32 · pythonanywhere/helper_scripts · GitHub
  7. URLで指定したリポジトリ<your-pythonanywhere-domain>.pythonanywhere.comという名前でgit cloneされています
  8. https://github.com/pythonanywhere/helper_scripts/blob/v0.10.3/pythonanywhere/django_project.py#L91-L117