実践的Djangoプロジェクトの設計―開発・運用が楽になる設定ファイルを書こう! アンチパターンとベストプラクティス

Pythonで広く利用されているWebアプリケーションのフレームワークにDjangoがあります。Djangoで開発を始める際に、プロジェクトの設定ファイルをどのように記述すれば運用が楽になるのか。『Python実践レシピ』の著書もある筒井隆次(ryu22e)さんによる寄稿です。

実践的Djangoプロジェクトの設計―開発・運用が楽になる設定ファイルを書こう! アンチパターンとベストプラクティス

Djangoは、Python製のWebアプリケーションフレームワークです。もともとニュースサイトを管理する目的で開発が始まり、2005年7月にOSSとしてリリースされました。 Python Software Foundation(PSF)による調査「Python Developers Survey 2021」を見ると、DjangoはPythonのWebアプリケーションフレームワークの中でシェア第2位となっており(1位はFlask)、高い人気を博しています。

本稿では、そのDjangoを使ってサービスを開発・運用する際にぶつかるであろう問題点と、その解決手段についてお伝えします。 Djangoではプロジェクトをdjango-admin startproject プロジェクト名で作成できますが、デフォルトのままでは開発や運用がやりにくい設定が一部にあるためです。

想定する読者は、Djangoのチュートリアルは読んだことがある、または簡単なアプリケーションを作ったことはあるが、本格的に開発で使ったことはない方です。

本稿の執筆にあたって、齋藤功さん、奥山崇さん、七花京さんにレビュアーとしてご協力いただきました。この場を借りて御礼を申し上げます。

本番運用を想定した設定ファイルの構成

最初に、実際に運用する際に有用なDjangoの設定ファイルの構成について紹介します。本稿の内容は、すべてこの構成で設計していることを前提とします。

デフォルトで設定ファイルはどこにあるのか?

django-admin startprojectコマンドでDjangoプロジェクトを作成すると、プロジェクト名と同じ名前のディレクトリの直下にsettings.pyというファイルができます。これがデフォルトの設定ファイルです。

例えば、django_exampleというDjangoプロジェクトを作った場合、以下のディレクトリにsettings.pyがあります。

$ django-admin startproject django_example
$ cd django_example
$ ls -1 django_example  # このディレクトリの下のsettings.pyが設定ファイル
__init__.py
asgi.py
settings.py
urls.py
wsgi.py

デフォルトの設定ファイルでは環境を切り替えることができない

settings.pyには、環境ごとに設定を切り替える機能がありません。 例えば、Django Debug Toolbarのようなデバッグツールを開発環境に導入する際、INSTALLED_APPS"debug_toolbar"を登録する必要がありますが、これは本番環境では不要なものです。

以下のように環境変数で登録する・しないを切り替えることはできますが、こういった条件分岐を多用するとsettings.pyの内容が複雑化し、メンテナンスが困難になる恐れがあります。

from os import environ

# (省略)

INSTALLED_APPS = [
    "django.contrib.admin",
    "django.contrib.auth",
    "django.contrib.contenttypes",
    "django.contrib.sessions",
    "django.contrib.messages",
    "django.contrib.staticfiles",
]

if environ.get("DJANGO_MODE") != "production":
    INSTALLED_APPS += ["debug_toolbar"]

環境ごとに設定ファイルを分割する方法がお勧め

環境ごとに設定を切り替えたい場合は、settings.pyを環境ごとに分割する方法がよく使われます。

例えば、以下のように設定ファイルを分けておきます。

django_example
└── settings             # settings.pyがあった場所にこれを作る
    ├── __init__.py      # Pythonがファイルを含むディレクトリをパッケージとして扱うために必要なファイル
    ├── base.py          # すべての環境で使う共通の設定
    ├── local.py         # 開発環境用の設定
    ├── production.py    # 本番環境用の設定
    └── staging.py       # ステージング環境用の設定

それでは、デフォルトのsettings.pyから上記の構成に変更する手順を説明します。 まず、settingsディレクトリとsettings/__init__.pyファイルを作成し、settings.pysettings/base.pyに移動させます(以下コマンド例を参照)。

$ mkdir django_example/settings
$ touch django_example/settings/__init__.py
$ mv django_example/settings.py django_example/settings/base.py

続いてdjango_example/settingsディレクトリに、local.pyproduction.pystaging.pyという3つのファイルを、以下の内容で作成します。

from .base import *

ここでは*(ワイルドカード)を使ったimport文で、base.pyの内容をすべてインポートしています。この書き方では何がどの名前空間に存在しているか分かりにくくなるため、PEP8(Pythonの公式コーディング規約)では推奨されません。 しかし、明示的にインポート対象を書くと、書き漏らしが原因でbase.pyの設定の一部が読み込まれず予期せぬ不具合につながる場合があるため、例外的にこの書き方を採用しています。

上記を作成したら、デフォルトのbase.pyの内容を上書きする設定を、local.pyproduction.pystaging.pyそれぞれの環境ごとに書いていきます。

例えば前述したDjango Debug Toolbarを導入する場合には、開発環境専用の設定ファイルlocal.pyを以下のように記述します。

from .base import *

INSTALLED_APPS += ["debug_toolbar"]

分割した設定ファイルをDjangoコマンドに渡す方法

上記で分割した設定ファイルを、Djangoコマンドに渡す方法は2つあります。

1つ目は、環境変数DJANGO_SETTINGS_MODULEを使う方法です。 DJANGO_SETTINGS_MODULEに設定ファイルへの場所を指定しておくと、Djangoコマンド実行時にそのファイルを読み込むようになります。

例えば、django_example/settings/local.pyを指定したい場合は次のようにします。

DJANGO_SETTINGS_MODULE=django_example.settings.local

2つ目は、Djangoコマンドの--settingsオプションを利用する方法です。 例えば、testコマンドの実行にdjango_example/settings/local.pyを指定したい場合は、次のようにします。

$ python manage.py test --settings=django_example.settings.local

manage.pyはDjangoプロジェクト作成時に作られるファイルで、Djangoコマンドを実行するために使います。 --settingsオプションは、DJANGO_SETTINGS_MODULEの内容より優先されるため、あるコマンドの実行だけ通常と違う設定ファイルを使いたい場合に便利です。

最後に、manage.pyに定義されているデフォルトの設定ファイルの場所がsettings.pyのままになっているので、書き換えておきましょう。

変更前のmanage.pyは以下のような内容です。os.environ.setdefaultの第2引数を変更します。

# (省略)

def main():
    """Run administrative tasks."""
    os.environ.setdefault("DJANGO_SETTINGS_MODULE", "django_example.settings")
# (省略)

変更後のmanage.pyの内容は以下の通りです。

# (省略)

def main():
    """Run administrative tasks."""
    os.environ.setdefault("DJANGO_SETTINGS_MODULE", "django_example.settings.local")
# (省略)

これで、環境変数もオプションも指定しない場合には、開発環境用のdjango_example/settings/local.pyが読み込まれます。

考察1. 秘密の値を設定ファイルに含めるには

APIのアクセストークン、データベースの接続パスワードなど、設定ファイルで扱う必要があるものの、第三者に渡してはいけない値は慎重に扱う必要があります。

ここでは、Djangoの設定ファイルで秘密の値を扱う際のお勧めの方法について説明します。

【やってはいけない】秘密の値を設定ファイルに書いてしまう

秘密の値を直接設定ファイルに書いてしまう人をときどき見かけますが、これはアンチパターンです。

GitHubのプライベートリポジトリを使えば一応漏洩を防げるように見えますが、以下のようなインシデントが起こる可能性はあります。

  • リポジトリを置いているサービスの脆弱性が原因でソースコードを盗まれる
  • 開発者のPCからソースコードを盗まれる
  • 人為的なミスでGitHubリポジトリをパブリックにしてしまう

秘密の値は、設定ファイルに直接書かないようにすることを強くお勧めします。

秘密の値は環境変数に保存する

秘密の値は必ず環境変数に保存して、設定ファイルからはos.environで参照するようにしましょう。

例えば、環境変数API_KEYを設定ファイルで扱う場合は、以下のように書きます。

from os import environ

API_KEY = os.environ["API_KEY"]

ただし上記の書き方では、環境変数を設定し忘れるとアプリケーション起動時にKeyErrorが送出されます。以下のようなget_env関数を書いてos.environの代わりに使うと、もっと分かりやすいエラーメッセージを出力できます。

import os
from django.core.exception import ImproperlyConfigured

# os.environに該当する値がなければ例外を送出する関数を作る
def get_env(var_name):
    try:
        os.environ[var_name]
    except KeyError:
        # os.environに該当するキーがない場合に何のキーが足りないかを例外メッセージに含む
        error_msg = f"環境変数{var_name}が存在しません"
        raise ImproperlyConfigured(error_msg)

サードパーティライブラリを利用する

django-environというサードパーティライブラリを利用するのも良い方法です。 django-environでは、上記のget_env関数に相当するEnvクラスを提供しています。 必須の環境変数API_KEYをdjango-environで扱う場合は、以下のように書きます。

import environ

env = environ.Env()

API_KEY = env('API_KEY')

環境変数の設定し忘れがあった場合は、アプリケーション起動時に以下の例外を送出します。

$ python manage.py runserver
Traceback (most recent call last):
(省略)
django.core.exceptions.ImproperlyConfigured: Set the API_KEY environment variable

環境変数の設定方法そのものについては、本稿の趣旨と少し外れるので詳しい説明を割愛しますが、以下に代表的な方法を紹介します。

  • .bashrcファイル(Bashの起動時に読み込まれるファイル)にexport API_KEY="..."のように定義
  • direnvを導入してDjangoプロジェクトの直下に置いた.envrcexport API_KEY="..."のように定義
  • Pipenvを導入してDjangoプロジェクトの直下に置いた.envAPI_KEY="..."のように定義
  • django-environのenv file機能を利用して定義
  • AWS Secrets Managerを利用して定義(本番環境のみ)
  • Google Cloud Secret Managerを利用して定義(本番環境のみ)

考察2. メール送信の設定

ここでは、Djangoでメール送信機能を持つWebアプリケーションを開発する際の注意点について説明します。

【やってはいけない】開発環境から本番のSMTPサーバーに接続

メール送信機能を持つWebアプリケーションを開発する際、気をつけなければならないのは、開発中のアプリケーションでのメールの誤配信です。

以下のように本物のSMTPサーバーに接続する設定を書いてしまうと、誤って他人のメールアドレスを入力して実際にメールが送信されてしまう事故が起こるかもしれません。

EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend"
EMAIL_HOST = "smtp.gmail.com"
EMAIL_PORT = 587
EMAIL_USE_TLS = True
EMAIL_HOST_USER = "test@example.com"
EMAIL_HOST_PASSWORD = "<secret>"

疑似SMTPサーバーに接続する

開発時は、MailHogのようなテスト用の疑似SMTPサーバーに接続しておく方法をお勧めします。 MailHogはWindows版、macOS版、Linux版が用意されているため、たいていの開発環境で利用できます。

MailHog経由でメールを送信するには、設定ファイルを以下のように書いておきます。

EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend"
EMAIL_HOST = "127.0.0.1"
EMAIL_PORT = 1025

EMAIL_USE_TLSEMAIL_HOST_USEREMAIL_HOST_PASSWORDは、上記の例では不要です。

MailHogコマンドを起動して疑似SMTPサーバーを立ち上げた状態で、Djangoアプリケーションからメールを送信すると、実際にメールが配送されることはなくMailHogに保存されます。

メールの内容はhttp://127.0.0.1:8025/で確認できます。 以下は、python manage.py sendtestemail test@example.comで送信したメールです。

メールの内容をブラウザで確認
メールの内容をブラウザで確認

ステージング環境のように複数人で利用する前提の環境であれば、Mailtrapも良い選択肢です。

MailtrapはMailHogのように擬似SMTPサーバーを提供するクラウドサービスで、実際にはメールを送信しないで、送信内容をWebのダッシュボードで確認することができます。

考察3. ファイルのアップロード先の設定

ここでは、Djangoアプリケーションからファイルをアップロードする際に開発しやすくなる設計のコツについて説明します。

【やってはいけない】開発環境でアップロード先を本番環境と同じにする

通常、本番環境と開発環境ではファイルアップロード先は異なります。 本番環境ではnginx、Apacheなどをインストールしたサーバー、Amazon S3、Google Cloud Storageなどがあります。

開発環境でアップロード先を本番環境と同じ対象にする設計はお勧めしません。 nginxやApacheの設定、APIキーの設置などを開発環境で行う必要があり、環境構築が煩雑になります。

開発環境のアップロード先のみ、ローカルのディレクトリにしておくことをお勧めします。

本番環境でAmazon S3にアップロードする設定例

本稿では、本番環境のアップロード先をAmazon S3にする前提で、具体的な設定例を説明します。 このとき本番環境では、django-storagesを利用します。django-storagesのインストール方法は以下の通りです。

$ pip install django-storages[boto3]

django-storagesでAmazon S3にアップロードする設定については、以下のdjango-storages公式ドキュメントを参照してください。

Amazon S3 — django-storages documentation

また、あらかじめAmazon S3にアップロードする設定が書かれたDjangoプロジェクトを作成する方法もあります。これは本稿の最後で便利なプロジェクトテンプレートとして紹介します。

開発環境ではデフォルトのローカルディレクトリのまま

開発環境ではアップロード先をローカルのディレクトリにします。Djangoではデフォルトのアップロード先がローカルのディレクトリなので、開発環境用の設定ファイルの変更は不要です。

前述した「本番運用を想定した設定ファイルの構成」に従った設計にしていれば、本番環境の設定が開発環境に影響することはありません。

考察4. ログを運用しやすいシンプルな設定

Djangoでは、LOGGINGという項目でログに関する設定を行います。 設定内容が多いので、一見してベストな方法を見極めるのが難しい項目ではありますが、シンプルで運用しやすい設定について説明します。

運用で問題になる設定としては、以下の2つがあります。それぞれを順に説明していきます。

  1. ログローテーションしないでローカルファイルにログを出力する
  2. エラーをメールで通知する

【やってはいけない】1. ログローテーションしない

1.は、以下のようにlogging.FileHandlerを使ってローカルファイルにログを出力する設定のことです。

LOGGING = {
    'version': 1,
    'disable_existing_loggers': False,
    'handlers': {
        'file': {
            'level': 'DEBUG',
            'class': 'logging.FileHandler',
            'filename': '/path/to/django/debug.log',
        },
    },
    'loggers': {
        'django': {
            'handlers': ['file'],
            'level': 'DEBUG',
            'propagate': True,
        },
    },
}

上記の設定では/path/to/django/debug.logにログの内容が追記され続けて、ファイル容量が肥大化する恐れがあります。ファイルの肥大化に気づかないと、最悪の場合システムが停止することがあります。

【やってはいけない】2. エラー通知をメールで行う

2.は、以下のようにdjango.utils.log.AdminEmailHandlerを使って、エラー発生時にメールで通知させる設定のことです。

LOGGING = {
    "version": 1,
    "disable_existing_loggers": False,
    "filters": {
        "require_debug_false": {
            "()": "django.utils.log.RequireDebugFalse",
        },
    },
    "handlers": {
        "mail_admins": {
            "level": "ERROR",
            "filters": ["require_debug_false"],
            "class": "django.utils.log.AdminEmailHandler",
        }
    },
    "loggers": {
        "django": {
            "handlers": ["mail_admins"],
            "level": "INFO",
        },
    },
}

ADMINS = [("John", "john@example.com")]

エラーが発生すると、ADMINSに設定したメールアドレス宛に以下のようなメールが送られます。

エラー発生時のメールの内容
エラー発生時のメールの内容

エラーが1回発生するたびに1通送られるので、実際のサービス運用では同じ内容のメールが大量に送られる場合があります。大量のメールが届くと、現在起こっている問題を整理しづらくなるので、この設定はお勧めしません。

ローカルファイルでは代わりのロギングハンドラを使う

ローカルファイルにログを出力する場合は、logging.FileHandlerの代わりに、以下のロギングハンドラを使いましょう。

上記のロギングハンドラは古いログを別ファイルとして切り分けてくれるので、1個のファイルのサイズが肥大化するリスクを回避できます。

クラウドサービスではログ集積サービスを利用

AWSやGoogle Cloudなどのクラウドサービスを利用している場合は、logging.StreamHandlerで標準出力のログを出力し、以下のようなログ集積サービスに転送する方法もお勧めです。

logging.StreamHandlerを使ったLOGGINGの設定例を、以下で紹介します。

LOGGING = {
    "version": 1,
    "disable_existing_loggers": True,
    "formatters": {
        "verbose": {
            "format": "%(levelname)s %(asctime)s %(module)s "
            "%(process)d %(thread)d %(message)s"
        }
    },
    "handlers": {
        "console": {
            "level": "DEBUG",
            "class": "logging.StreamHandler",
            "formatter": "verbose",
        }
    },
    "root": {"level": "INFO", "handlers": ["console"]},
    "loggers": {
        "django.db.backends": {
            "level": "ERROR",
            "handlers": ["console"],
            "propagate": False,
        },
    },
}

情報を整理しやすいエラー通知サービスを使う

エラー通知では、Sentryというサービスを利用すると情報を整理しやすくなります。

sentry-sdkをインストールし、設定ファイルに以下のコードを書いておくと、エラー発生時にSentryに情報が送られます。このときLOGGINGの設定は不要です。

import sentry_sdk
from sentry_sdk.integrations.django import DjangoIntegration

sentry_sdk.init(
    dsn=...,  # Sentryのダッシュボードで生成したDSNを入れる
    integrations=[
        DjangoIntegration(),
    ],
    # Set traces_sample_rate to 1.0 to capture 100%
    # of transactions for performance monitoring.
    # We recommend adjusting this value in production.
    traces_sample_rate=1.0,
    # If you wish to associate users to errors (assuming you are using
    # django.contrib.auth) you may enable sending PII data.
    send_default_pii=True,
)

Sentryに送られた情報は、サービスのダッシュボードで以下のように表示されます。

Sentryダッシュボード上でのエラー情報
Sentryダッシュボード上でのエラー情報

Sentryはエラー情報を受け取ると、さまざまな手段でサービス運営者に通知を送ることができます。メール通知だけでなく、SlackやMicrosoft Teamsなどに通知を送ることもできます。

また、Sentryにはエラー通知の条件を細かく設定する機能があります。一度に同じエラーが大量に発生した場合、通知を1件だけ行うようにもできます。 詳細は、以下の公式ドキュメントを参照してください。

Alerts - Sentry Documentation

便利なプロジェクトテンプレートの紹介

最後に、便利なプロジェクトテンプレートcookiecutter-djangoについて紹介します。

Cookiecutterは、テンプレートをもとにプロジェクトを作成してくれるコマンドラインツールです。 Python製ですが、Pythonに限らず以下のようにさまざまな言語・フレームワークのプロジェクトを作成できます。

  • Webアプリケーションのプロジェクト
  • 各言語のライブラリ用プロジェクト
  • Sphinxのようなドキュメンテーションツール用プロジェクト

cookiecutter-djangoは、Djangoプロジェクトを作成するためのCookiecutterテンプレートです。本稿で紹介した設計で作られているので、標準のstartprojectコマンドよりも簡単に、実践的なDjangoプロジェクトを作成できます。

cookiecutter-djangoの使い方

使い方について簡単に紹介します。Cookiecutter自体のインストール方法は、以下の公式ドキュメントを参照してください。

Installation — cookiecutter documentation

cookiecutter-djangoは、cookiecutterコマンドの引数にcookiecutter-djangoのGitHubリポジトリへのURLを渡すだけで使えます。

$ cookiecutter https://github.com/cookiecutter/cookiecutter-django
project_name [My Awesome Project]: django example
project_slug [django_example]:
description [Behold My Awesome Project!]: example project
author_name [Daniel Roy Greenfeld]: Ryuji Tsutsui
(省略)
 [INFO]: .env(s) are only utilized when Docker Compose and/or Heroku support is enabled so keeping them does not make sense given your current setup.
 [SUCCESS]: Project initialized, keep up the good work!

これで、カレントディレクトリの直下にdjango_exampleというDjangoプロジェクトが作られます。

cookiecutter-djangoで作られたプロジェクトは、本稿で紹介したお勧めの設計以外にも参考になるコードがあるので、一度読んでみることをお勧めします。

筒井 隆次(Ryuji Tsutsui) twitter: @ryu22e / GitHub: ryu22e

ryu22e
Pythonを使い始めたのは2011年ごろ。2013年からDjangoを使ったWebサービス開発に従事。2022年7月から株式会社hokanに勤務。2016年にPython Boot Campの立ち上げに参加し、一般社団法人PyCon JP Association運営メンバーなどコミュニティ運営の活動多数。趣味は映画鑑賞、格闘技観戦。著書に『Pythonエンジニア育成推進協会監修 Python実践レシピ』(技術評論社、2022年、共著)
ryu22eBlog