TL; DR: L'astuce est de modifier os.environment
avant d'importer settings/base.py
n'importe quoi settings/<purpose>.py
, cela simplifiera grandement les choses.
Le simple fait de penser à tous ces fichiers entrelacés me donne mal à la tête. Combiner, importer (parfois de manière conditionnelle), remplacer, corriger ce qui était déjà défini au cas où le DEBUG
paramètre serait modifié ultérieurement. Quel cauchemard!
Au fil des ans, je suis passé par toutes les différentes solutions. Ils fonctionnent tous un peu , mais sont si pénibles à gérer. WTF! Avons-nous vraiment besoin de tout ce tracas? Nous avons commencé avec un seul settings.py
fichier. Maintenant, nous avons besoin d'une documentation juste pour combiner correctement tous ces éléments dans un ordre correct!
J'espère que j'ai finalement atteint le (mon) sweet spot avec la solution ci-dessous.
Récapitulons les objectifs (certains communs, certains miens)
Gardez les secrets secrets - ne les stockez pas dans un repo!
Définissez / lisez les clés et les secrets via les paramètres d'environnement, style 12 facteurs .
Avoir des valeurs par défaut de secours raisonnables. Idéalement pour le développement local, vous n'avez besoin de rien de plus que les valeurs par défaut.
… Mais essayez de sécuriser la production par défaut. Il vaut mieux manquer un remplacement de paramètre localement, que de se rappeler de régler les paramètres par défaut en toute sécurité pour la production.
Avoir la possibilité de changer DEBUG
/ désactiver d'une manière qui peut avoir un effet sur d'autres paramètres (par exemple en utilisant javascript compressé ou non).
Le basculement entre les paramètres d'objectif, comme local / test / mise en scène / production, doit être basé uniquement sur DJANGO_SETTINGS_MODULE
rien de plus.
… Mais permettre un paramétrage plus poussé via des paramètres d'environnement tels que DATABASE_URL
.
… Leur permet également d'utiliser différents paramètres de finalité et de les exécuter localement côte à côte, par exemple. configuration de la production sur la machine du développeur local, pour accéder à la base de données de production ou aux feuilles de style compressées du test de fumée.
Échec si une variable d'environnement n'est pas explicitement définie (nécessitant une valeur vide au minimum), en particulier en production, par exemple. EMAIL_HOST_PASSWORD
.
Répondre à l' DJANGO_SETTINGS_MODULE
ensemble par défaut dans manage.py pendant le démarrage du projet django-admin
Gardez les conditions au minimum, si la condition est le type d'environnement prévu (par exemple, pour le fichier journal de l'ensemble de production et sa rotation), remplacez les paramètres dans le fichier de paramètres proposé.
Ne pas
Ne laissez pas django lire le paramètre DJANGO_SETTINGS_MODULE dans un fichier.
Pouah! Pensez à quel point c'est méta. Si vous avez besoin d'un fichier (comme docker env), lisez-le dans l'environnement avant de démarrer un processus django.
Ne remplacez pas DJANGO_SETTINGS_MODULE dans votre code de projet / application, par exemple. basé sur le nom d'hôte ou le nom du processus.
Si vous êtes paresseux pour définir la variable d'environnement (comme poursetup.py test
), faites-le dans l'outillage juste avant d'exécuter votre code de projet.
Évitez la magie et la correction de la façon dont django lit ses paramètres, prétraitez les paramètres mais n'interférez pas par la suite.
Pas de bêtises logiques compliquées. La configuration doit être fixe et matérialisée et non calculée à la volée. Fournir un défaut par défaut est juste assez logique ici.
Voulez-vous vraiment déboguer, pourquoi localement vous avez un ensemble de paramètres correct mais en production sur un serveur distant, sur une centaine de machines, quelque chose de calculé différemment? Oh! Tests unitaires? Pour les paramètres? Sérieusement?
Solution
Ma stratégie consiste en un excellent environnement django utilisé avec ini
des fichiers de style, fournissant des os.environment
valeurs par défaut pour le développement local, quelques settings/<purpose>.py
fichiers minimaux et courts qui ont un
import settings/base.py
APRÈS leos.environment
a été créée à partir d' un INI
fichier. Cela nous donne effectivement une sorte d'injection de paramètres.
L'astuce consiste à modifier os.environment
avant d'importersettings/base.py
.
Pour voir l'exemple complet, faites le repo: 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
settings / .env
A par défaut pour le développement local. Un fichier secret, pour définir principalement les variables d'environnement requises. Définissez-les sur des valeurs vides si elles ne sont pas requises dans le développement local. Nous fournissons des valeurs par défaut ici et ne settings/base.py
pas échouer sur une autre machine si elles sont absentes de l'environnement.
settings / local.py
Ce qui se passe ici, c'est charger l'environnement depuis settings/.env
, puis importer les paramètres communs depuis settings/base.py
. Après cela, nous pouvons en remplacer quelques-uns pour faciliter le développement local.
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
Pour la production, nous ne devons pas nous attendre à un fichier d'environnement, mais il est plus facile d'en avoir un si nous testons quelque chose. Mais de toute façon, de peur de fournir peu de valeurs par défaut en ligne, vous settings/base.py
pouvez donc réagir en conséquence.
environ.Env.read_env(Path(__file__) / "production.env", DEBUG='False', ASSETS_DEBUG='False')
from .base import *
Le principal point d'intérêt ici est DEBUG
et ASSETS_DEBUG
remplace, ils seront appliqués au pythonos.environ
UNIQUEMENT s'ils sont MANQUANTS de l'environnement et du fichier.
Ce seront nos valeurs par défaut de production, pas besoin de les mettre dans l'environnement ou le fichier, mais elles peuvent être remplacées si nécessaire. Soigné!
settings / base.py
Ce sont vos paramètres de django principalement vanille, avec quelques conditions et beaucoup de lecture à partir de l'environnement. Presque tout est là, en gardant tous les environnements prévus cohérents et aussi similaires que possible.
Les principales différences sont ci-dessous (j'espère qu'elles sont explicites):
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',)
Le dernier bit montre la puissance ici. ASSETS_DEBUG
a une valeur par défaut raisonnable, qui peut être remplacée settings/production.py
et même celle qui peut être remplacée par un paramètre d'environnement! Yay!
En effet, nous avons une hiérarchie mixte d'importance:
- settings / .py - définit les valeurs par défaut en fonction de l'objectif, ne stocke pas les secrets
- settings / base.py - est principalement contrôlé par l'environnement
- paramètres d'environnement de processus - 12 facteurs bébé!
- settings / .env - valeurs par défaut locales pour un démarrage facile