TL; DR: Sztuką jest zmodyfikowanie os.environment
przed zaimportowaniem settings/base.py
dowolnego settings/<purpose>.py
, co znacznie uprości rzeczy.
Samo myślenie o tych wszystkich powiązanych plikach sprawia mi ból głowy. Łączenie, importowanie (czasem warunkowo), przesłonięcie, łatanie tego, co było już ustawione w przypadku DEBUG
zmiany przypadku później. Co za koszmar!
Przez lata przeszedłem przez różne rozwiązania. Wszystkie działają nieco , ale bardzo bolesne w zarządzaniu. WTF! Czy naprawdę potrzebujemy całego tego problemu? Zaczęliśmy od tylko jednego settings.py
pliku. Teraz potrzebujemy dokumentacji, aby poprawnie połączyć wszystkie te elementy we właściwej kolejności!
Mam nadzieję, że w końcu trafiłem na (mój) najsłodszy punkt dzięki poniższemu rozwiązaniu.
Podsumujmy cele (niektóre wspólne, niektóre moje)
Zachowaj tajemnicę w tajemnicy - nie przechowuj ich w repozytorium!
Ustaw / czytaj klucze i sekrety poprzez ustawienia środowiska, styl 12-czynnikowy .
Mają rozsądne domyślne ustawienia rezerwowe. Idealnie do lokalnego rozwoju nie potrzebujesz niczego więcej oprócz domyślnych.
… Ale staraj się zachować bezpieczeństwo domyślnej produkcji. Lepiej pominąć zastąpienie ustawienia lokalnie, niż pamiętać o dostosowaniu ustawień domyślnych bezpiecznych dla produkcji.
Mają możliwość włączania DEBUG
/ wyłączania w sposób, który może mieć wpływ na inne ustawienia (np. Używając kompresji javascript lub nie).
Przełączanie między ustawieniami celu, takimi jak lokalny / testowanie / inscenizacja / produkcja, powinno opierać się tylko na DJANGO_SETTINGS_MODULE
niczym innym.
… Ale pozwalają na dalszą parametryzację poprzez ustawienia środowiska takie jak DATABASE_URL
.
… Pozwalają również na używanie różnych ustawień celu i uruchamianie ich lokalnie obok siebie, np. konfiguracja produkcji na lokalnym komputerze dewelopera, aby uzyskać dostęp do bazy danych produkcji lub arkuszy stylów skompresowanych testów dymu.
Błąd, jeśli zmienna środowiskowa nie jest jawnie ustawiona (wymagająca co najmniej pustej wartości), szczególnie w środowisku produkcyjnym, np. EMAIL_HOST_PASSWORD
.
Odpowiedz na domyślny DJANGO_SETTINGS_MODULE
zestaw w manage.py podczas start-projektu django-admin
Przechowywać warunkowe do minimum, jeśli warunek jest przeznaczona typ środowiska (np. Do produkcji nastawionej pliku dziennika i to obrót), zastępują ustawienia w pliku związanego przeznaczona ustawień.
Nie jest
Nie pozwól django odczytać ustawienia DJANGO_SETTINGS_MODULE z pliku.
Ugh! Pomyśl, jak to jest meta. Jeśli potrzebujesz pliku (np. Docker env), przeczytaj go w środowisku przed rozpoczęciem procesu django.
Nie zastępuj DJANGO_SETTINGS_MODULE w kodzie projektu / aplikacji, np. na podstawie nazwy hosta lub nazwy procesu.
Jeśli jesteś leniwy, aby ustawić zmienną środowiskową (jak dlasetup.py test
), zrób to w oprzyrządowaniu tuż przed uruchomieniem kodu projektu.
Unikaj magii i łatania tego, jak django odczytuje swoje ustawienia, wstępnie przetwarzaj ustawienia, ale nie przeszkadzaj później.
Żadnych skomplikowanych bzdur opartych na logice. Konfiguracja powinna być stała i zmaterializowana, a nie obliczana w locie. Zapewnienie zastępczych ustawień domyślnych jest tutaj wystarczającą logiką.
Czy naprawdę chcesz debugować, dlaczego lokalnie masz poprawny zestaw ustawień, ale w produkcji na zdalnym serwerze, na jednej ze stu maszyn, coś obliczonego inaczej? O! Testy jednostkowe? Dla ustawień? Poważnie?
Rozwiązanie
Moja strategia polega na doskonałej Django Environ używanego z ini
plikami styl, zapewniając os.environment
domyślne dla rozwoju lokalnego, niektóre minimalne i krótkie settings/<purpose>.py
pliki, które mają
import settings/base.py
POos.environment
został ustawiony z INI
pliku. To skutecznie daje nam rodzaj zastrzyku ustawień.
Sztuczka polega na zmodyfikowaniu os.environment
przed zaimportowaniemsettings/base.py
.
Aby zobaczyć pełny przykład, wykonaj repozytorium: https://github.com/wooyek/django-settings-strategy
.
│ manage.py
├───data
└───website
├───settings
│ │ __init__.py <-- imports local for compatibility
│ │ base.py <-- almost all the settings, reads from proces environment
│ │ local.py <-- a few modifications for local development
│ │ production.py <-- ideally is empty and everything is in base
│ │ testing.py <-- mimics production with a reasonable exeptions
│ │ .env <-- for local use, not kept in repo
│ __init__.py
│ urls.py
│ wsgi.py
ustawienia / .env
Domyślne ustawienia lokalne. Tajny plik, w którym najczęściej ustawiane są wymagane zmienne środowiskowe. Ustaw je na puste wartości, jeśli nie są wymagane w rozwoju lokalnym. Zapewniamy wartości domyślne tutaj, a nie wsettings/base.py
ulegną awarii na żadnym innym komputerze, jeśli brakuje ich w środowisku.
ustawienia / local.py
To, co się tutaj dzieje, to ładowanie środowiska settings/.env
, a następnie importowanie wspólnych ustawień settings/base.py
. Następnie możemy zastąpić kilka, aby ułatwić rozwój lokalny.
import logging
import environ
logging.debug("Settings loading: %s" % __file__)
# This will read missing environment variables from a file
# We wan to do this before loading a base settings as they may depend on environment
environ.Env.read_env(DEBUG='True')
from .base import *
ALLOWED_HOSTS += [
'127.0.0.1',
'localhost',
'.example.com',
'vagrant',
]
# https://docs.djangoproject.com/en/1.6/topics/email/#console-backend
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
# EMAIL_BACKEND = 'django.core.mail.backends.dummy.EmailBackend'
LOGGING['handlers']['mail_admins']['email_backend'] = 'django.core.mail.backends.dummy.EmailBackend'
# Sync task testing
# http://docs.celeryproject.org/en/2.5/configuration.html?highlight=celery_always_eager#celery-always-eager
CELERY_ALWAYS_EAGER = True
CELERY_EAGER_PROPAGATES_EXCEPTIONS = True
settings / production.py
W przypadku produkcji nie powinniśmy oczekiwać pliku środowiska, ale łatwiej go mieć, jeśli coś testujemy. Ale w każdym razie, aby nie wprowadzać kilku wartości domyślnych, więc settings/base.py
mogą odpowiednio zareagować.
environ.Env.read_env(Path(__file__) / "production.env", DEBUG='False', ASSETS_DEBUG='False')
from .base import *
Głównym przedmiotem zainteresowania są DEBUG
i ASSETS_DEBUG
zastępują, zostaną one zastosowane do pytonaos.environ
TYLKO, jeśli brakuje go ze środowiska i pliku.
Będą to nasze domyślne ustawienia produkcyjne, nie trzeba ich umieszczać w środowisku lub pliku, ale w razie potrzeby można je zastąpić. Schludny!
settings / base.py
Są to twoje najczęściej waniliowe ustawienia django, z kilkoma warunkami warunkowymi i dużą ilością czytania ich ze środowiska. Prawie wszystko jest tutaj, utrzymując spójne i możliwie najbardziej zbliżone środowiska.
Główne różnice są poniżej (mam nadzieję, że są to oczywiste):
import environ
# https://github.com/joke2k/django-environ
env = environ.Env()
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
BASE_DIR = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
# Where BASE_DIR is a django source root, ROOT_DIR is a whole project root
# It may differ BASE_DIR for eg. when your django project code is in `src` folder
# This may help to separate python modules and *django apps* from other stuff
# like documentation, fixtures, docker settings
ROOT_DIR = BASE_DIR
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/1.11/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = env('SECRET_KEY')
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = env('DEBUG', default=False)
INTERNAL_IPS = [
'127.0.0.1',
]
ALLOWED_HOSTS = []
if 'ALLOWED_HOSTS' in os.environ:
hosts = os.environ['ALLOWED_HOSTS'].split(" ")
BASE_URL = "https://" + hosts[0]
for host in hosts:
host = host.strip()
if host:
ALLOWED_HOSTS.append(host)
SECURE_SSL_REDIRECT = env.bool('SECURE_SSL_REDIRECT', default=False)
# Database
# https://docs.djangoproject.com/en/1.11/ref/settings/#databases
if "DATABASE_URL" in os.environ: # pragma: no cover
# Enable database config through environment
DATABASES = {
# Raises ImproperlyConfigured exception if DATABASE_URL not in os.environ
'default': env.db(),
}
# Make sure we use have all settings we need
# DATABASES['default']['ENGINE'] = 'django.contrib.gis.db.backends.postgis'
DATABASES['default']['TEST'] = {'NAME': os.environ.get("DATABASE_TEST_NAME", None)}
DATABASES['default']['OPTIONS'] = {
'options': '-c search_path=gis,public,pg_catalog',
'sslmode': 'require',
}
else:
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
# 'ENGINE': 'django.contrib.gis.db.backends.spatialite',
'NAME': os.path.join(ROOT_DIR, 'data', 'db.dev.sqlite3'),
'TEST': {
'NAME': os.path.join(ROOT_DIR, 'data', 'db.test.sqlite3'),
}
}
}
STATIC_ROOT = os.path.join(ROOT_DIR, 'static')
# django-assets
# http://django-assets.readthedocs.org/en/latest/settings.html
ASSETS_LOAD_PATH = STATIC_ROOT
ASSETS_ROOT = os.path.join(ROOT_DIR, 'assets', "compressed")
ASSETS_DEBUG = env('ASSETS_DEBUG', default=DEBUG) # Disable when testing compressed file in DEBUG mode
if ASSETS_DEBUG:
ASSETS_URL = STATIC_URL
ASSETS_MANIFEST = "json:{}".format(os.path.join(ASSETS_ROOT, "manifest.json"))
else:
ASSETS_URL = STATIC_URL + "assets/compressed/"
ASSETS_MANIFEST = "json:{}".format(os.path.join(STATIC_ROOT, 'assets', "compressed", "manifest.json"))
ASSETS_AUTO_BUILD = ASSETS_DEBUG
ASSETS_MODULES = ('website.assets',)
Ostatni bit pokazuje moc tutaj. ASSETS_DEBUG
ma sensowną wartość domyślną, którą można zastąpićsettings/production.py
a nawet to, które można zastąpić ustawieniem środowiska! Tak!
W efekcie mamy mieszaną hierarchię ważności:
- settings / .py - ustawia wartości domyślne na podstawie celu, nie przechowuje tajemnic
- settings / base.py - jest w większości kontrolowany przez środowisko
- ustawienia środowiska procesowego - dziecko 12-czynnikowe!
- ustawienia / .env - lokalne ustawienia domyślne dla łatwego uruchamiania