Преамбула
Этот модуль родился в результате переосмысления (или недопонимания) мной вот этого пространного документа: Splitting up the settings file, размещённого на официальном сайте Django.
Постановка задачи
При старте веб-приложения на Django (как посредством запуска отладочного сервера, так и в качестве WSGI-приложения) фреймворк первым делом выполняет модуль, задающий начальные настройки проекта. Источник кода задаётся переменной окружения DJANGO_SETTINGS_MODULE. При создании Django-проекта стандартным способом, например:
$ django-admin startproject myproject
создаётся и модуль настроек. Это файл ‘myproject/myproject/settings.py’. Изменяя и дополняя его, программист настраивает проект, добавляет в него собственные и сторонние компоненты и т. д.В простых проектах, разрабатываемых одним бэкенд-программистом, бывает вполне разумно ограничиться таким модулем настроек. Однако по мере роста проекта возникают следующие проблемы:
- Настройки проекта для развёртывания в боевой или тестовой среде очень отличаются от настроек, с которыми проект запускают разработчики. Например, в бою приложение требует «большой» SQL-сервер (PostgreSQL или MySQL) и дополнительную базу данных «ключ-значение» для хранения кэшируемых данных (memcached или Redis), в то время как разработчик на своём компютере привык обходиться SQLite. Зато настройки разработчика включают дополнительные модули для отладки проекта (например, debug_toolbar), которые не должны попаcть в production.
- Переменные, устанавливающие режим отладки в Django (DEBUG, TEMPLATE_DEBUG и др.), а также аналогичные переменные для сторонних компонентов, должны быть включены при разработке и выключены в продакшне. За этим довольно муторно следить при коммитах.
- В модуле settings хранятся чувствительные данные (SECRET_KEY, секреты/пароли для аутентификации приложения на различных сервисах и т. д.), которые становится небезопасно хранить в одном репозитории с кодом. Это особенно важно для open source-проектов, а также для крупных проектов, в которых доступ к кодовой базе имеет много разработчиков.
Проблемы 1 и 2 частично решает хранение в репозитории нескольких файлов настроек на разные случаи жизни и выбор нужного через установку переменной DJANGO_SETTINGS_MODULE. Недостаток такого решения в том, что данные в этих файлах почти полностью дублируются. По мере развития проекта разработчик обязан вносить одинаковые изменения в несколько различных файлов конфигурации, что утомительно, приводит к ошибкам и т. д., ? короче говоря, противоречит принципу DRY.
Решение
Мой модуль settings обладает максимальной обратной совместимостью с дефолтовым ‘myproject/myproject/settings.py’: все ссылки на myproject.settings, если они действительно необходимы, остаются в силе. В то же время моё решение позволяет администратору проекта обезопасить приватные данные, а разработчикам ? организовать себе наиболее комфортную среду на собственный вкус, независимо от коллег. Дополнительным плюсом является механизм наследования настроек: в локальных настройках можно получить доступ к общим настройкам.
Минус: для хранения локальных настроек нужно придумать какой-то отдельный метод, так как репозиторий использовать не получится. Решение этого вопроса обычно лежит в организационной плоскости: передавать секрет от более опытных коллег менее опытным, публиковать сэмпловый ‘local.py’ в приватном разделе wiki и т. п.
Зато мой метод крайне прост и быстр, не вмешивается в процесс парсинга настроек фреймворком и не создаёт лишних сущностей вроде специальных *.ini/*.conf-файлов с парсерами, классов настроек или модифицирующих настройки функций.
Hands-on
Вот последовательность действий по «апгрейду» классического модуля настроек (подразумевается, что код хранится в git-репозитории):
- Создайте в каталоге главного приложения подкаталог ‘settings’. Путь к нему будет выглядеть так: ‘myproject/myproject/settings/’.
- Переместите ваш старый ‘settings.py’ в созданный в п. 1 каталог ‘myproject/myproject/settings/’ и переименуйте его в ‘common.py’. Будем ссылаться на этот файл в дальнейшем как на «общие настройки».
Если вы используете в проекте относительные пути от файла настроек, увеличьте глубину вложенности на один каталог. Например, код типа:
должен превратиться вBASE_DIR = dirname(dirname(abspath(__file__)))
BASE_DIR = dirname(dirname(dirname(abspath(__file__))))
- Создайте файл ‘myproject/myproject/settings/local.py’. В него сразу добавьте следующий код:
Вы можете добавить локальные настройки, начиная со следующей строки.from myproject.settings.common import *
Например, если вы ? разработчик, и хотите использовать замечательный инструмент Django Debug Toolbar, вы можете добавить следующую строку:
INSTALLED_APPS += ('debug_toolbar', )
- Создайте файл ‘myproject/myproject/settings/__init__.py’ и внесите в него следующий код:
Этот вариант рассчитан на тот случай, когда настройки, содержащиеся в common.py, вполне достаточны для того, чтобы проект запустился. Это вряд ли будет соответствовать истине в большом проекте. Как минимум, из общих настроек следует изъять SECRET_KEY по соображениям безопасности.try: from myproject.settings.local import * except ImportError: from myproject.settings.common import *
Если без файла локальной конфигурации запуск проекта не имеет смысла, можно не пытаться обойтись глобальными настройками, а выбросить исключение:
try: from myproject.settings.local import * except ImportError: raise Exception('Please create local.py file')
- Добавьте файл ‘myproject/myproject/settings/local.py’ в исключения git. Это последний, но от этого не менее важный шаг.
Готово! Мы разбили файл настроек на общую, прототипную часть (common.py), и локальную часть, наследующую настройки от общей (local.py). Теперь всё дело за правильной декомпозицией настроек.
Комментарии (22)
Tanner
25.02.2016 16:06-1Встречал в дикой природе django-environ. Забраковал его, и вот почему.
- Он требует переработать файл настроек согласно документации. В моём варианте в settings.py вносятся очень незначительные изменения, можно даже оставить его на первых порах без изменений.
- Он вводит отдельный формат хранения настроек. Мой метод, как я уже писал, лишних сущностей не создаёт.
А файл с секретами так и так руками копировать надо. В общем, незачот.
Также можно все настройки хранить в папке settings в виде отдельных файлов(apps.py, middlewares.py, celery.py и тд.)
Да, такой подход имеет место быть, но меня интересовало именно разделение настроек на глобальную и локальную части, а не по какому-то иному принципу. Если инклюдить всё в инит, теряется возможность порождать локальный сабсет настроек от глобального.
Crandel
25.02.2016 16:22+1Мои советы предназначены для проектов, в которых от 2 разработчиков и больше. django-environ не требует никаких изменений в настройках, просто секретные данные лежат в отдельном файле, а у вашем варианте придеться дублировать константы в local.py файле. Можно, например, при дебаге парсить ".local" — файл с секретными переменными, а на проде ".prod". Все остальные настройки либо в одном файле, либо в отдельных(последнее предпочтительнее)
Tanner
25.02.2016 16:34Нет, мне однозначно не нужны в проекте лишние классы и парсеры. Это неоправданные накладные расходы. В моём случае все потери сводятся к одному ‘import’, и я хотел бы на этом остановиться. Ну, в крайнем случае ? несколько ‘import’.
а у вашем варианте придеться дублировать константы в local.py файле
Не понял этого момента. Какие именно константы придётся дублировать в local.py? Я же показываю в статье, что его содержимое ничего не дублирует, а только уточняет.Crandel
25.02.2016 16:38Например настройки базы данных или ключи от твиттера и фейсбука, все эти константы придется переопределять в локальном файле.
Tanner
25.02.2016 16:50Разумеется, если продакшн использует одну СУБД, а девелопер ? другую, то в local.py на сервере придётся определить одни DATABASES, а в local.py на машине девелопера ? другие. Но если у нас имеются, например, 20 девелоперов, которые одинаковым способом используют SQLite и два продакшн-сервера с разными базами данных, то можно определить настройки трижды: для разработчиков ? в common.py, а для серверов ? в их персональных local.py.
Так же и с django-environ, как я понял: в env-файле будут храниться одни настройки, а в settings.py ? другие.Crandel
25.02.2016 16:56+1Вы неправильно поняли, в файле ".env" хранятся только данные, например логин, пароль к базе, ключ твиттера или фейсбука. А в settings.py у вас константа базы прописывается один раз, а заполняется в зависимости от содержания файла ".env" или любого другого, имя которого вы укажете при инициализации environ
Tanner
25.02.2016 17:01То есть проблема в том, что у меня “DATABASES = {…}” будет написано не один раз, а несколько? Ну, это я точно переживу. =)
MechanisM
26.02.2016 20:43А файл с секретами так и так руками копировать надо. В общем, незачот.
На хероку, например, настройки в админке заполняются. Никакие файлы .env с настройками не нужно иметь в репозитории.
overmes
25.02.2016 18:13+2Недостаток такого решения в том, что данные в этих файлах почти полностью дублируются
А зачем их дублировать?
Создали base_settings, там все что дублируется записали или прописали настройки по умолчанию
Потом создали production_settings:
from base_settings import * DEBUG = False
и debug_settings:
from base_settings import * DEBUG = True
Если я не ошибаюсь такой же путь рекомендуют в Django Two Scope(Там про это точно есть отдельная глава)
baldr
25.02.2016 22:40-1Я использую файлик settings.yaml, в котором переопределяю все константы типа пароля к базе и тп. В settings.py просто читается файл и импортруется в globals() все что найдет, втч с сохранением структуры если хэш.
Файлик лежит не в папке проекта, а в /usr/local/etc/ и права на доступ имеет только тот пользователь, под которым работает приложение. В принципе, можно класть и в home.
Хранить любые неверсионируемые файлы в папке проекта не очень люблю, поскольку при деплое обычно заменяется существующая папка новой — и все (или просто симлинк меняется). Бегать перекладывать файлики — это еще вспомнить их надо, а если придет другой человек — он и потерять их может.Tanner
26.02.2016 01:50Это хорошая идея, тем более, что у моих проектов и так есть их специфический каталог etc с настройками uwsgi, supervisor и др. Разве что я бы скорее использовал JSON вместо YAML.
dadon
26.02.2016 02:27После того как перешел на envdir, с болью вспоминаю разные файлы настроек для разных окружений local / staging / production.
Кстати, если в продакшене используется postgres и redis, крайне желательно, чтобы при локальной разработке было все тоже самое. Современные инструменты позволяют легко это сделать.Tanner
26.02.2016 03:05The way you structure your envdir is left to you
Мне как-то проще структурировать саму конфигурацию, чем привлекать дополнительную сущность и её структурировать.
А насчёт PG/Redis в дев. среде согласен, у меня установлено на десктопе всё, что используется в продакшне. Но я всё-таки держу в голове ситуацию, когда проект надо запустить на другой машине по-быстрому, с минимумом зависимостей.ahmpro
26.02.2016 11:01оберните проект в Vagrant и исчезнет боль с разными дев-окружениями
для staging/production использую отдельный репозиторий с ansible ролями и плейбуками, это упрощает деплой и хранение приватных настроек
для settings использую похожую схему как у вас, только везде явно указывается какой файл настроек надо использовать(нет неявного импорта local_settings.py)
immaculate
26.02.2016 14:10Был миллион подобных статей, на habrahabr в том числе, и ваш вариант далеко не самый оптимальный.
Например, я использую production.py и development.py в каталоге settings.
В local.py хранятся пароли для базы данных и т.п.
в manage.py прописано использование settings.development, в wsgi.py — settings.production. Поэтому не нужно на production включать/выключать DEBUG.
Хотя даже здесь в комментариях описали как можно зайти еще дальше, и я собственно в разных проектах использую разные подходы (по желанию или необходимости, например, в проектах под buildout одна структура настроек, под virtualenv — другая).
MechanisM
26.02.2016 18:34У меня настройки сделаны классами с помощью django-configuraions(ну люблю я когда один файлик settings.py а не куча всяких файлов с настройкми).
Все настройки через переменные окружения(использую свой django-confy)
В uwsgi у меня отдельные секции для development и production. например:
`
[uwsgi]
; тут общие настройки
; и потом идут секции с настройками кэширования, статистики, спулера и разные другие
[production]
env = DEBUG=False
env = DJANGO_CONFIGURATION=Production
ini = :uwsgi
ini = :cache
ini = :stats
ini = :spools
disable-logging
ignore-write-errors
ignore-sigpipe
print = Loaded production settings!
[development]
logto = %dlogs/%c.uwsgi.log
env = DEBUG=True
env = DJANGO_CONFIGURATION=Development
venv = /server/.py/%c
ini = :uwsgi
ini = :cache
ini = :spools
ini = :staticfiles
py-autoreload = 2
show-config
print = Loaded development settings!
`
То есть из uwsgi передается параметр в каком режиме загружать проект.
Запускаю как-то так(содержимое Procfile):
web: newrelic-admin run-program uwsgi --ini uwsgi.ini:production
MechanisM
26.02.2016 18:39Если есть что-то такое, что мне нужно быстро включить или выключить, я это тоже делаю через переменные окружения. Наример
if env('OPBEAT'): INSTALLED_APPS += ['opbeat.contrib.django',] MIDDLEWARE_CLASSES += [ 'opbeat.contrib.django.middleware.OpbeatAPMMiddleware', ] OPBEAT = { 'ORGANIZATION_ID': env('OPBEAT_ORGANIZATION_ID'), 'APP_ID': env('OPBEAT_APP_ID'), 'SECRET_TOKEN': env('OPBEAT_SECRET_TOKEN'), } if env('RAVEN'): INSTALLED_APPS += ['raven.contrib.django.raven_compat',] MIDDLEWARE_CLASSES += [ 'raven.contrib.django.middleware.Sentry404CatchMiddleware', 'raven.contrib.django.middleware.SentryResponseErrorIdMiddleware', ] RAVEN_CONFIG = { 'dsn': env('SENTRY_DSN'), # 'release': raven.fetch_git_sha(root), }
baldr
26.02.2016 20:22Ну, вообще-то, использовать переменные окружения для передачи паролей и секретных параметров — не очень секьюрно.
Любой сторонний процесс из-под другого пользователя может проявить любопытство:
cat /proc/<pid>/environ
Я не совсем понимаю кто поставил минус за идею хранить все в отдельном файле с урезанными правами, возможно у него есть аргументы против.Crandel
26.02.2016 21:26А вот и нет, если используется файл ".env", то там видны только общие переменные пользователя
Crandel
Для хранения секретных данных лучше использовать django-environ и файл вида ".env" который копируется вручную. Также можно все настройки хранить в папке settings в виде отдельных файлов(apps.py, middlewares.py, celery.py и тд.), которые инклюдятся в init.py.
Tanner
Прошу прощения, промахнулся по ссылке. Ответ ниже.