Settings organisieren
Normalerweise haben wir mehrere Umgebungen, in denen das Projekt (später) betrieben wird: die lokale Umgebung, eine Staging-Umgebung, Produktion usw. Jede Umgebung kann ihre eigenen spezifischen Einstellungen haben (zum Beispiel: DEBUG = True, ausführlichere Protokollierung, zusätzliche Apps usw.) . Um das zu ermöglichen, brauchen wir eine bessere Organisation der Settings.
Zusätzlich haben wir noch verschiedene Passwörter für Datenbanken, Secret-Keys und sonstige Einstellungen.
12 Factors
12 Factors ist eine Sammlung von Empfehlungen zum Erstellen verteilter Web-Apps, die in der Cloud einfach bereitzustellen und zu skalieren sind.
Es behandelt neben Themen wie Codebase, Dependencies, Logs, auch das Thema Configuration. Eine der Hauptregeln ist, dass Konfiguration im Environment zu liegen hat, um Konfiguration von Code zu trennen. Passwörter werden also nicht hardcodiert in die Settings geschrieben, sondern als Umgebungsvariable abgelegt.
mehr Infos dazu hier: https://12factor.net/
Warnung
Sensible Daten
Sensitive Daten dürfen auf gar keinen Fall in die Versionskontrolle.
Diese Dateien müssen zwingend in .gitignore
eingetragen werden, damit die Versionskontrolle sie ignoriert.
Auslagern von sensitiven Daten in Umgebungsvariablen
Wir gehen dabei den Weg über Environment-Variablen und das Modul django-environ. Wir könnten auch über os.environ
direkt gehen, aber das ist nicht so komfortabel wie das Modul, zb. wenn es zu Key-Errors kommt.
django-environ
django-environ
ist ein Python-Paket, das es ermöglicht, Django-Anwendung mit Umgebungsvariablen zu konfigurieren und dabei die Zwölf-Faktor-Methode anzuwenden.
Wir können eine .env
-Datei erstellen, die von django-environ
eingelesen werden kann, und müssen die Umgebungsvariablen NICHT zwingend im
Betriebsystem ablegen. Falls im OS die entsprechenden Umgebungsvariablen aber
vorliegen sollten, haben diese Vorrang vor den Werten aus der .env
-Datei.
Mehr zu Django Environ: https://django-environ.readthedocs.io/en/latest/
Umgebungsvariablen unter Windows oder Linux
Mit folgenden Kommandos können die aktuellen Umgebungsvariablen unter Windows und Linux ausgelesen werden.
Linux:
env
Windows:
dir env:
django-environ installieren
Wir legen in der requirements.in
das folgende Paket an:
django-environ
und installieren es:
(eventenv) pip-compile requirements.in
(eventenv) pip-sync requirements.txt requirements-dev.txt
.env Datei
Wir legen die Datei event_manager/.env
an (also im Projektroot auf der
gleichen Ebene wie die manage.py) und füllen sie mit folgendem Inhalt:
DEBUG=on
SECRET_KEY=29309239stable09jalsdf02309238stable0239840
ALLOWED_HOSTS=127.0.0.1,localhost
SETTINGS_MODULE=event_manager.settings.dev
Hier sehen wir die Datei .env
auf der selben Ebene wie die manage.py:
event_project
├── event_manager
├── db.sqlite3
├── event_manager
├── .env
├── env.example
├── events
├── manage.py
....
Zeitgleich zur .env-Datei habe nwir auch noch eine env.example
angelegt.
Diese soll versioniert werden und anderen Entwicklern zeigen, welche Inhalte
die .env-Datei benötigt. Ein Beispiel könnte so aussehen:
# SECURITY WARNING: don't run with the debug turned on in production!
DEBUG=True
# comma separated list with valid hosts
ALLOWED_HOSTS=localhost,127.0.0.1
# Should robots.txt allow everything to be crawled?
ALLOW_ROBOTS=False
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY=secret
# A list of all the people who get code error notifications.
ADMINS="John Doe <john@example.com>, Mary <mary@example.com>"
# A list of all the people who should get broken link notifications.
MANAGERS="Blake <blake@cyb.org>, Alice Judge <alice@cyb.org>"
# By default, Django will send system email from root@localhost.
# However, some mail providers reject all email from this address.
SERVER_EMAIL=webmaster@example.com
Diese Beispiel-Datei bietet nicht nur Default-Werte, sondern zeigt durch Kommentare auch gleich an, was die einzelnen Konfigurationswerte im Einzelnen sind.
Hinweis: Dateien mit einem Punkt davor gelten unter unixoiden
Betriebssystemen als unsichtbar. Um sie beim Auflisten zu sehen, muss man,
je nach verwendetem Betriebssystem und Software die entsprechende Einstellung
vornehmen. So lassen sich unter Linux mit dem ls
-Kommando Dateien mit
einem Punkt nur anziegen, wenn man noch das entstprechende Argument angibt:
ls -al
In diese .env-Datei kommen alle sensitiven Daten, die das Projekt benötigt.
Passwörter für die Datenbank, Secret-Key, Port-Angaben und so weiter. Diese
Datei wird nicht versioniert, dh. jeder, der das Repository klont, benötigt
ebenfalls wieder seine eigene .env
-Datei. Damit man weiß, was in dieser
Datei drinzustehen hat, bietet es sich an, eine env.example
mit
dummy-Inhalt anzulegen. Es sollte darauf geachtet werden, diese Beispieldatei
ebenfalls up to date zu halten.
environ importieren
Wir importieren das Modul in den Settings und erstellen ein env-Objekt.
Dabei können wir beim Instantiieren auch angeben, von welchem Datentyp eine Konfigurationsvariable
sein soll, falls sie von String
abweicht.
import environ
env = environ.Env(
DEBUG=(bool, False)
)
environ.Env.read_env(BASE_DIR / ".env")
DEBUG = env("DEBUG")
SECRET_KEY = env("SECRET_KEY")
ALLOWED_HOSTS = env.list("ALLOWED_HOSTS")
Wir weisen hier der DEBUG-Konstante einen Default-Wert zu, nämlich False
.
Sollte sich diese KONSTANTE nicht in der .env-Datei
befinden, wird der
Defaultwert genommen. Sollte kein Defaultwert angegeben worden sein, gibt es
einen Key-Error, sollte sich die KONSTANTE nicht in .env
befinden.
Interessant: wir belegen auch noch SETTINGS_MODULE
mit einem Defaultwert.
Gleich werden wir nämlich diese settings.py
auflösen und den Inhalt über
mehrere Dateien hinweg aufteilen.
.env ignorieren
Wir fügen .env
in die .gitignore
-Datei ein, damit diese Datei nicht versioniert wird und Passwörter am Ende in einem frei zugänglichen Repository landen.
Wenn wir jetzt den runserver
starten, sollte das Projekt fehlerfrei laufen.
Gehen wir also jetzt zum nächsten Schritt.
Settings-Datei für jede Umgebung
Jede Umgebung, in der das Projekt betrieben wird, hat andere Einstellung: eine
andere Datenbank, ein anderer Logger usw. Im Produktivbetrieb sollte zum
Beispiel die django-Debugtoolbar nicht in den MIDDLEWARE
stehen.
Die TEMPLATES
-Liste hingegen wird u.U. auch im Produktivbetrieb die gleiche
sein, wie im lokalen Betrieb.
Wir sehen also: es gibt Konfigurationen in den settings.py
, die für alle Umgebungen gelten, und manche nur für spezielle.
Unsere Gliederung der Settings wird also so sein, dass es eine Base-Settings gibt, von der alle anderen Settings erben und die überschreiben oder anreichern kann.
Settings organisieren
ein Verzeichnis für die Settings
Wir legen ein neues Verzeichnis an: event_manager/event_manager/settings
,
in welches die Settings für die verschiedenen Umgebungen gespeichert werden.
Die alte settings.py
nennen wir um in settings_old.py
, da wir diese
nicht mehr benötigen und später löschen werden.
Settings-Dateien
In das event_manager/event_manager/settings
-Verzeichnis speichern wir drei neue Dateien:
base.py
, dev.py
und prod.py
. Wenn später noch eine weitere Umgebung dazukommt, kann diese hier angelegt werden.
event_manager
├───event_manager
├───settings
├─base.py
├─dev.py
├─prod.py
Inhalt der base.py
Diese Inhalte sind die Einstellungen, die in allen Umgebungen benötigt werden. Alle sensiblen Inhalte kommen über die Umgebungsvariablen. So sieht sie zur Zeit aus:
from pathlib import Path
import environ
BASE_DIR = Path(__file__).resolve().parent.parent.parent
env = environ.Env(DEBUG=(bool, False))
environ.Env.read_env(BASE_DIR / ".env")
DEBUG = env("DEBUG")
SECRET_KEY = env("SECRET_KEY")
ALLOWED_HOSTS = env.list("ALLOWED_HOSTS")
INSTALLED_APPS = [
"user",
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
"crispy_bootstrap5",
"crispy_forms",
"events",
"pages",
]
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",
]
ROOT_URLCONF = "event_manager.urls"
TEMPLATES = [
{
"BACKEND": "django.template.backends.django.DjangoTemplates",
"DIRS": [BASE_DIR / "event_manager" / "templates"],
"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 = "event_manager.wsgi.application"
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",
},
]
# https://docs.djangoproject.com/en/3.2/topics/i18n/
LANGUAGE_CODE = "de"
TIME_ZONE = "Europe/Berlin"
USE_I18N = True
USE_L10N = True
USE_TZ = True
CRISPY_TEMPLATE_PACK = "bootstrap5"
CRISPY_ALLOWED_TEMPLATE_PACKS = ("bootstrap5",)
# Static files (CSS, JavaScript, Images)
STATIC_URL = "/static/"
STATICFILES_DIRS = [BASE_DIR / "static"]
STATIC_ROOT = BASE_DIR / "staticfiles"
MEDIA_ROOT = BASE_DIR / "media"
MEDIA_URL = "/media/"
# Default primary key field type
# https://docs.djangoproject.com/en/3.2/ref/settings/#default-auto-field
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
AUTH_USER_MODEL = "user.User"
Inhalt der dev.py
Diese Datei importiert alle Einstellungen der base.py
, überschreibt aber bei Bedarf diejenigen Einstellungen, die im Entwicklungsbetrieb anderes sein müssen.
So sieht sie zur Zeit aus:
from event_manager.settings.base import *
# für Debug-Toolbar
INTERNAL_IPS = ("127.0.0.1",)
DATABASES = {
"default": {
"ENGINE": "django.db.backends.sqlite3",
"NAME": BASE_DIR / "db.sqlite3",
}
}
MIDDLEWARE.extend(["debug_toolbar.middleware.DebugToolbarMiddleware"])
INSTALLED_APPS.extend(
[
"debug_toolbar",
]
)
DEBUG_TOOLBAR_CONFIG = {
"INTERCEPT_REDIRECTS": False,
}
Die settings_old.py
können wir nun löschen, wenn wir alles übertragen haben.
Best Practice: Settings-Dateien versionieren
Das aktuelle Setup ermögtlicht uns, dass auch jeder User eine eigene Settings Datei haben könnte, also zb. tom.py
für den User tom. Trotzdem ist es ratsam, diese privaten, lokalen dev-Settings nicht von der Versionierung auszuschließen. Miskonfigurationen oder bad-pratices können so schneller auffallen.
Wenn wir jetzt den Runserver starten, bekommen wir einen Fehler:
(eventenv) python manage.py runserver
CommandError: You must set settings.ALLOWED_HOSTS if DEBUG is False
Bekanntmachen des settings-Folders
Django weiß von unserer neuen Struktur natürlich nichts. Per default wird nach
einer Datei settings.py
im Projektverzeichnis gesucht. Das müssen wir
ändern, da es sonst zu einem Fehler kommt.
Settings-Modul in Env eintragen
in unsere .env
-Datei legen wir alle Variablen, die wir für unseren lokalen Betrieb haben möchten.
So auch den Ort unserer Settings-Datei. Im folgenden Beispiel sagen wir, dass
unsere Settings-Datei für das aktuelle Projekt die event_manager.settings.dev.py
sein soll.
Auf einem Live-System würde man hier auf die event_manager.settings.prod.py
verweisen (die wir noch nicht angelegt haben).
DEBUG=on
SECRET_KEY=29309239stable09jalsdf02309238stable0239840
SETTINGS_MODULE=event_manager.settings.dev
ALLOWED_HOSTS=127.0.0.1 localhost
Hinweis: Wir verzichten hier zwingend auf den Dateisuffix py!
manage.py
Wir öffnen die Datei event_manager/manage.py
und fügen folgenden Inhalt ein:
# andere Imports
# environ importieren
import environ
# Settings Modul aus den Environments holen
env = environ.Env()
environ.Env.read_env()
settings_module = env('SETTINGS_MODULE')
def main():
# diese Zeile ändern
os.environ.setdefault("DJANGO_SETTINGS_MODULE", settings_module)
# anderer Code
Damit haben wir der Variable settings_module
den aktuellen Wert der
Konstante DJANGO_SETTINGS_MODULE
aus der .env-Datei zugewiesen. Sollte
dieser Konfigurationsparameter weder in der .env-Datei noch in den
Umgebungsvariablen vorhanden sein, wird es zu einem Fehler kommen und das
Programm an dieser Stelle abbrechen.
wsgi.py
Wenn der WSGI-Server im Produktivbetrieb die Anwendung lädt, muss Django das Einstellungsmodul importieren - dort ist die gesamte Anwendung definiert.
Django verwendet die Umgebungsvariable DJANGO_SETTINGS_MODULE
, um das
entsprechende Einstellungsmodul zu finden. Sie muss den Pfad in Dot-Notation zum
Einstellungsmodul enthalten.
Deshalb müssen wir für den Produktivbetrieb die
event_manager/event_manager/wsgi.py
anpassen:
import os
from pathlib import Path
# environ importieren
import environ
# Settings aus den Umgebungsvariablen laden
environ.Env.read_env(Path(__file__).resolve().parent.parent / ".env")
env = environ.Env(DEBUG=(bool, False))
settings_module = env('SETTINGS_MODULE')
from django.core.wsgi import get_wsgi_application
# diese Zeile ändern
os.environ.setdefault("DJANGO_SETTINGS_MODULE", settings_module)
Wenn jetzt der Runserver gestartet wird, sollte alles funktionieren. Wenn wir
in der Django-Debugtoolbar auf Einstellungen
gehen, sehen wir, dass die
aktuelle Settings-Datei wie gewünscht unsere event_manager.settings.dev
ist.
Auf der shell kann man sich die Settings so angucken:
>>> from django.conf import settings
>>> settings.__dict__
In einem späteren Kapitel werden wir noch die Django-Extensions
kennenlernen.
Dort ist ein Subkommando definiert, welches genau für diesen Zweck
implementiert wurde:
python manage.py print_settings
eine Helperfunktion schreiben
Wir müssen an mehreren Stellen environ
importieren und die .env-Datei laden. Dies
verstößt gegen das DRY-Prinzip
. Um dieses Problem zu Umgehen, schreiben wir
uns eine kleine Helperfunktion, die nur dafür da ist, die .env-Datei
zu laden
und die dort ausgelesenen Umgebungsvariablen bzw. das env-Objekt
zurückzugeben.
Unter event_manager/event_manager
legen wir eine Datei
utils.py
an und definieren dort folgende Funktion:
import os
from pathlib import Path
import environ
def getenv():
"""read .env-File and create environ instance."""
environ.Env.read_env(Path(__file__).resolve().parent.parent / ".env")
return environ.Env(
DEBUG=(bool, False)
)
Wir brauchen diese Funktion jetzt nur noch zu importieren und nutzen
Zum Beispiel in der event_manager/event_manager/settings/base.py
from ..utils import getenv
# anderer code
env = getenv()
# hier env nutzen
DEBUG = env("DEBUG")
bzw. in der manage.py
:
from event_manager.utils import getenv
env = getenv()
settings_module = env("SETTINGS_MODULE")
und der wsgi.py
:
from .utils import getenv
env = getenv()
settings_module = env("SETTINGS_MODULE")
Fazit
Wir haben nun unsere Settings so angepasst, dass für jede Umgebung, produktiv oder lokal, eine eigene Settings-Datei angelegt werden kann. Man muss im Betrieb nur darauf achten, den Pfad zu den Settings in der .env-Datei zu definieren. Fehlt dieser Eintrag, gibt es bewusst keinen Fallback via einem Defaultwert, sondern Django bricht den Bootvorgang ab.