Djangoのsettings.pyとurls.pyを環境に応じて分割・切り替えよう！

## 概要
Djangoのsettings.pyにいろんな設定を書いていくうちに肥大化していくかと思います
また、settings.py内でif文でDEBUGがTrueもしくはfalseの時に設定を切り替えることができますがかなり冗長になってしまう上にどれが共通の設定でどれが本番環境用か直感的にわかりずらいなと私自身感じていました
そこで、今回は環境別(例えばlocal用、dev用)に
- settings.py
- urls.py

の分割および切り替えをする方法について解説したいと思います

## 前提
- .envファイル内に環境変数を入れるのと環境構築を容易にするためにDockerを使うことを推奨します
- Djangoのrunserverなどの基本的なコマンドをある程度知っていること
- Swaggerを使用するためにdrf-spectacularを使用しますが、設定方法については詳細に説明しません

## ファイル構成
settings.pyとurls.pyを分割する際のファイル構成は以下の通りです
プロジェクト名はproject, アプリケーション名はapplicationとします
- settings.py
- urls.py

を分割していきましょう
また、settings.pyの分割に伴い、プロジェクトの設定ファイルのパスが変わってしまうため、
- manage.py
- asgi.py
- wsgi.py

もあわせて修正する必要があリます

```:ディレクトリ構成
└── application
    ├── application
    │   ├── __init__.py
    │   ├── admin.py
    │   ├── apps.py
    │   ├── migrations
    │   ├── models
    │   ├── urls.py
    │   ├── utils
    │   └── views
    ├── manage.py
    ├── poetry.lock
    ├── project
    │   ├── __init__.py
    │   ├── asgi.py
    │   ├── settings
    │   │   ├── __init__.py
    │   │   ├── base.py
    │   │   ├── local.py
    │   │   └── dev.py
    │   ├── urls
    │   │   ├── __init__.py
    │   │   ├── base.py
    │   │   └── local.py
    │   └── wsgi.py
    └── pyproject.toml                                                          
```

## settings
settings.pyを
- base.py(共通の設定)
- local.py
- dev.py

に分割していきます

### base.py
base.pyに共通の設定を記載していきます
私はrest_frameworkとカスタムユーザのModelを使用していますが共通で使用したいアプリケーションなどは基本的にこちらに記載していきます

```project/settings/base.py
"""
Django settings for application project.

Generated by 'django-admin startproject' using Django 4.1.3.

For more information on this file, see
https://docs.djangoproject.com/en/4.1/topics/settings/

For the full list of settings and their values, see
https://docs.djangoproject.com/en/4.1/ref/settings/
"""

import os
from pathlib import Path

# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent


# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/4.1/howto/deployment/checklist/

# SECRET_KEYを.envから取得
SECRET_KEY = os.environ.get("SECRET_KEY")

# ALLOWED_HOSTSを.envから取得
ALLOWED_HOSTS = os.environ.get("ALLOWED_HOSTS").split()

# Application definition

DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"

REST_FRAMEWORK = {
    "DEFAULT_AUTHENTICATION_CLASSES": [
        "rest_framework.authentication.BasicAuthentication",
        "rest_framework.authentication.SessionAuthentication",
    ]
}


INSTALLED_APPS = [
    "django.contrib.admin",
    "django.contrib.auth",
    "django.contrib.contenttypes",
    "django.contrib.sessions",
    "django.contrib.messages",
    "django.contrib.staticfiles",
    "rest_framework",
    "application.apps.ApplicationConfig",
]

MIDDLEWARE = [
    "django.middleware.security.SecurityMiddleware",
    "django.contrib.sessions.middleware.SessionMiddleware",
    "django.middleware.common.CommonMiddleware",
    "django.middleware.csrf.CsrfViewMiddleware",
    "django.contrib.auth.middleware.AuthenticationMiddleware",
    "django.contrib.messages.middleware.MessageMiddleware",
    "django.middleware.clickjacking.XFrameOptionsMiddleware",
]

TEMPLATES = [
    {
        "BACKEND": "django.template.backends.django.DjangoTemplates",
        "DIRS": [],
        "APP_DIRS": True,
        "OPTIONS": {
            "context_processors": [
                "django.template.context_processors.debug",
                "django.template.context_processors.request",
                "django.contrib.auth.context_processors.auth",
                "django.contrib.messages.context_processors.messages",
            ],
        },
    },
]

WSGI_APPLICATION = "project.wsgi.application"


# Database
# https://docs.djangoproject.com/en/4.1/ref/settings/#databases
# 今回はMySQLをDBとして使用します
DATABASES = {
    "default": {
        "ENGINE": "django.db.backends.mysql",
        # コンテナ内の環境変数をDATABASESのパラメータに反映
        "NAME": os.environ.get("MYSQL_DATABASE"),
        "USER": os.environ.get("MYSQL_USER"),
        "PASSWORD": os.environ.get("MYSQL_PASSWORD"),
        "HOST": os.environ.get("MYSQL_HOST", "db"),
        "PORT": os.environ.get("MYSQL_PORT", 3306),
    }
}


# Password validation
# https://docs.djangoproject.com/en/4.1/ref/settings/#auth-password-validators

AUTH_PASSWORD_VALIDATORS = [
    {
        "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
    },
    {
        "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
    },
    {
        "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",
    },
    {
        "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",
    },
]


# Internationalization
# https://docs.djangoproject.com/en/4.1/topics/i18n/

LANGUAGE_CODE = "ja"

TIME_ZONE = "Asia/Tokyo"

USE_I18N = True

USE_TZ = True


# STATIC_ROOTを設定
STATIC_ROOT = "/static/"
STATIC_URL = "/static/"

# Default primary key field type
# https://docs.djangoproject.com/en/4.1/ref/settings/#default-auto-field

DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
# カスタムユーザModelの設定
AUTH_USER_MODEL = "application.User"
```

### local.py
local環境でのみ使う設定を記載していきます
今回はlocal用にSwaggerを使うのでSwaggerの設定はlocal.pyにのみ記載していきます
Swagger以外に下記のように検証用で使うMailCatcherなどの設定も記載します
また、デバッグモードでDjangoを使用したいので
```python
DEBUG = True
```
にします

```project/settings/local.py
"""LOCAL環境用の設定"""
# 共通の設定をimport
from .base import *

DEBUG = True

# REST_FRAMEWORKのdictにSwaggerの設定を追加するためにupdateを使用
REST_FRAMEWORK.update({"DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema"})

SPECTACULAR_SETTINGS = {
    "TITLE": "プロジェクト名",
    "DESCRIPTION": "詳細",
    "VERSION": "1.0.0",
}

INSTALLED_APPS += [
    "drf_spectacular",
]

# 後述するurls.pyをlocal用・dev用に分ける際にlocal用を使用
ROOT_URLCONF = "project.urls.local"

# Djangoのメールの設定
EMAIL_HOST = "mail"
EMAIL_HOST_USER = ""
EMAIL_HOST_PASSWORD = ""
# SMTPの1025番ポートを指定
EMAIL_PORT = 1025
EMAIL_USE_TLS = False
```

### dev.py
dev環境でのみ使う設定を記載していきます
例えばAWSのSESやS3の設定などは以下のように記載します
```project/settings/dev.py
"""DEV環境用の設定"""
from .base import *

DEBUG = False
# 後述するurls.pyをlocal用・dev用に分ける際に今回はbase(local・dev共通)用を使用
ROOT_URLCONF = "project.urls.base"

INSTALLED_APPS += [
    "django_ses",
    "storages",
]

# SESの設定
EMAIL_BACKEND = "django_ses.SESBackend"
AWS_SES_REGION_NAME = os.environ.get("AWS_SES_REGION_NAME")
AWS_SES_REGION_ENDPOINT = os.environ.get("AWS_SES_REGION_ENDPOINT")
DEFAULT_FROM_EMAIL = os.environ.get("DEFAULT_FROM_EMAIL")
# S3の設定
DEFAULT_FILE_STORAGE = "storages.backends.s3boto3.S3Boto3Storage"
STATICFILES_STORAGE = "storages.backends.s3boto3.S3StaticStorage"
AWS_STORAGE_BUCKET_NAME = os.environ.get("AWS_STORAGE_BUCKET_NAME")
```

## urls
続いてurls.pyを
- base.py(共通の設定)
- local.py

に分割していきます

### base.py
共通で使用するurlを記載します
adminのパスがないとDjangoが動いてくれないので必ず記載しましょう

```project/urls/base.py
"""application URL Configuration

The `urlpatterns` list routes URLs to views. For more information please see:
    https://docs.djangoproject.com/en/4.1/topics/http/urls/
Examples:
Function views
    1. Add an import:  from my_app import views
    2. Add a URL to urlpatterns:  path('', views.home, name='home')
Class-based views
    1. Add an import:  from other_app.views import Home
    2. Add a URL to urlpatterns:  path('', Home.as_view(), name='home')
Including another URLconf
    1. Import the include() function: from django.urls import include, path
    2. Add a URL to urlpatterns:  path('blog/', include('blog.urls'))
"""
from django.contrib import admin
from django.urls import include, path

urlpatterns = [
    path("admin/", admin.site.urls),
    path("api/", include("application.urls")),
]
```

### local.py
Swaggerのパスは開発でのみ使用するのでこちらに記載します
```project/urls/local.py
"""LOCAL環境用のURL"""

from django.urls import path
from drf_spectacular.views import (
    SpectacularAPIView,
    SpectacularRedocView,
    SpectacularSwaggerView,
)
# project.urls.baseからurlpatternsをimportする必要があります
from project.urls.base import urlpatterns

urlpatterns += [
    # Swaggerの設定
    path("api/schema/", SpectacularAPIView.as_view(), name="schema"),
    path(
        "api/docs/",
        SpectacularSwaggerView.as_view(url_name="schema"),
        name="swagger-ui",
    ),
    path(
        "api/redoc/",
        SpectacularRedocView.as_view(url_name="schema"),
        name="redoc",
    ),
]
```

## manage.py
DJANGO_SETTINGS_MODULEにsettings.pyのパスを記載しますがsettings.pyを分割してしまったので
project.settings.localに変更します
また、.envファイルなどに`DJANGO_SETTINGS_MODULE`の値を格納している方は忘れずに変更しておきましょう
```application/manage.py
#!/usr/bin/env python
"""Django's command-line utility for administrative tasks."""
import os
import sys


def main():
    """Run administrative tasks."""
    os.environ.setdefault("DJANGO_SETTINGS_MODULE", "project.settings.local")
    try:
        from django.core.management import execute_from_command_line
    except ImportError as exc:
        raise ImportError(
            "Couldn't import Django. Are you sure it's installed and "
            "available on your PYTHONPATH environment variable? Did you "
            "forget to activate a virtual environment?"
        ) from exc
    execute_from_command_line(sys.argv)


if __name__ == "__main__":
    main()
```

## asgi.py
同様にproject.settings.localに変更します
```application/application/asgi.py
"""
ASGI config for project project.

It exposes the ASGI callable as a module-level variable named ``application``.

For more information on this file, see
https://docs.djangoproject.com/en/4.1/howto/deployment/asgi/
"""

import os

from django.core.asgi import get_asgi_application

os.environ.setdefault("DJANGO_SETTINGS_MODULE", "project.settings.local")

application = get_asgi_application()
```

## wsgi.py
同様にproject.settings.localに変更します
```application/application/wsgi.py
"""
WSGI config for project project.

It exposes the WSGI callable as a module-level variable named ``application``.

For more information on this file, see
https://docs.djangoproject.com/en/4.1/howto/deployment/wsgi/
"""

import os

from django.core.wsgi import get_wsgi_application

os.environ.setdefault("DJANGO_SETTINGS_MODULE", "project.settings.local")

application = get_wsgi_application()
```

## 実際に動かしてみよう！
### local用
.envファイル内のDJANGO_SETTINGS_MODULEの値でDEBUGモードを分岐させることができます
local用でSwaggerを使えるか確認するために以下のようにします

```.env
DJANGO_SETTINGS_MODULE=project.settings.local
```

127.0.0.1/api/docs にアクセスすると以下のようにSwaggerが表示されたら成功です

![スクリーンショット 2023-03-05 10.58.24.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/625980/4bfea6df-e426-ac02-5d26-3db31b1dd28a.png)

### dev用
dev用でSwaggerが使えないことを確認するために以下のようにします

```.env
DJANGO_SETTINGS_MODULE=project.settings.local
```

127.0.0.1/api/docs にアクセスすると以下のようにNot Foundと表示されたら成功です

![スクリーンショット 2023-03-05 11.07.20.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/625980/1dac0264-acbd-877f-4e20-6b2a71a646ed.png)

### entrypoint.shによる分岐
シェルスクリプトを使ってDJANGO_SETTINGS_MODULEの値別に切り替える際は以下のようにすることもできます

```entrypoint.sh
#!/bin/sh
set -eu

poetry run python manage.py makemigrations
poetry run python manage.py migrate

if [ $DJANGO_SETTINGS_MODULE = "project.settings.local" ]
then
    poetry run python manage.py runserver 0.0.0.0:8000
else
    poetry run gunicorn project.wsgi:application --bind 0.0.0.0:8000
fi
```

## まとめ
以上のやり方でsettings.py内に無駄なif文の分岐を使わずに環境別の設定が直感的にわかるようになリました
プロダクトが大きくなるにつれてsettings.pyが見づらくなってしまうのが悩みだったので対処法がわかってよかったです