Преамбула


Этот модуль родился в результате переосмысления (или недопонимания) мной вот этого пространного документа: Splitting up the settings file, размещённого на официальном сайте Django.

Постановка задачи


При старте веб-приложения на Django (как посредством запуска отладочного сервера, так и в качестве WSGI-приложения) фреймворк первым делом выполняет модуль, задающий начальные настройки проекта. Источник кода задаётся переменной окружения DJANGO_SETTINGS_MODULE. При создании Django-проекта стандартным способом, например:
$ django-admin startproject myproject
создаётся и модуль настроек. Это файл ‘myproject/myproject/settings.py’. Изменяя и дополняя его, программист настраивает проект, добавляет в него собственные и сторонние компоненты и т. д.

В простых проектах, разрабатываемых одним бэкенд-программистом, бывает вполне разумно ограничиться таким модулем настроек. Однако по мере роста проекта возникают следующие проблемы:

  1. Настройки проекта для развёртывания в боевой или тестовой среде очень отличаются от настроек, с которыми проект запускают разработчики. Например, в бою приложение требует «большой» SQL-сервер (PostgreSQL или MySQL) и дополнительную базу данных «ключ-значение» для хранения кэшируемых данных (memcached или Redis), в то время как разработчик на своём компютере привык обходиться SQLite. Зато настройки разработчика включают дополнительные модули для отладки проекта (например, debug_toolbar), которые не должны попаcть в production.
  2. Переменные, устанавливающие режим отладки в Django (DEBUG, TEMPLATE_DEBUG и др.), а также аналогичные переменные для сторонних компонентов, должны быть включены при разработке и выключены в продакшне. За этим довольно муторно следить при коммитах.
  3. В модуле 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-репозитории):

  1. Создайте в каталоге главного приложения подкаталог ‘settings’. Путь к нему будет выглядеть так: ‘myproject/myproject/settings/’.
  2. Переместите ваш старый ‘settings.py’ в созданный в п. 1 каталог ‘myproject/myproject/settings/’ и переименуйте его в ‘common.py’. Будем ссылаться на этот файл в дальнейшем как на «общие настройки».

    Если вы используете в проекте относительные пути от файла настроек, увеличьте глубину вложенности на один каталог. Например, код типа:
    BASE_DIR = dirname(dirname(abspath(__file__)))
    
    должен превратиться в
    BASE_DIR = dirname(dirname(dirname(abspath(__file__))))
    
  3. Создайте файл ‘myproject/myproject/settings/local.py’. В него сразу добавьте следующий код:
    from myproject.settings.common import *
    
    Вы можете добавить локальные настройки, начиная со следующей строки.

    Например, если вы ? разработчик, и хотите использовать замечательный инструмент Django Debug Toolbar, вы можете добавить следующую строку:
    INSTALLED_APPS += ('debug_toolbar', )
    
  4. Создайте файл ‘myproject/myproject/settings/__init__.py’ и внесите в него следующий код:
    try:
        from myproject.settings.local import *
    except ImportError:
        from myproject.settings.common import *
    
    Этот вариант рассчитан на тот случай, когда настройки, содержащиеся в common.py, вполне достаточны для того, чтобы проект запустился. Это вряд ли будет соответствовать истине в большом проекте. Как минимум, из общих настроек следует изъять SECRET_KEY по соображениям безопасности.

    Если без файла локальной конфигурации запуск проекта не имеет смысла, можно не пытаться обойтись глобальными настройками, а выбросить исключение:
    try:
        from myproject.settings.local import *
    except ImportError:
        raise Exception('Please create local.py file')
    
  5. Добавьте файл ‘myproject/myproject/settings/local.py’ в исключения git. Это последний, но от этого не менее важный шаг.

Готово! Мы разбили файл настроек на общую, прототипную часть (common.py), и локальную часть, наследующую настройки от общей (local.py). Теперь всё дело за правильной декомпозицией настроек.

Комментарии (22)


  1. Crandel
    25.02.2016 15:49
    +3

    Для хранения секретных данных лучше использовать django-environ и файл вида ".env" который копируется вручную. Также можно все настройки хранить в папке settings в виде отдельных файлов(apps.py, middlewares.py, celery.py и тд.), которые инклюдятся в init.py.


    1. Tanner
      25.02.2016 16:07

      Прошу прощения, промахнулся по ссылке. Ответ ниже.


  1. Tanner
    25.02.2016 16:06
    -1

    Встречал в дикой природе django-environ. Забраковал его, и вот почему.

    1. Он требует переработать файл настроек согласно документации. В моём варианте в settings.py вносятся очень незначительные изменения, можно даже оставить его на первых порах без изменений.
    2. Он вводит отдельный формат хранения настроек. Мой метод, как я уже писал, лишних сущностей не создаёт.

    А файл с секретами так и так руками копировать надо. В общем, незачот.

    Также можно все настройки хранить в папке settings в виде отдельных файлов(apps.py, middlewares.py, celery.py и тд.)
    Да, такой подход имеет место быть, но меня интересовало именно разделение настроек на глобальную и локальную части, а не по какому-то иному принципу. Если инклюдить всё в инит, теряется возможность порождать локальный сабсет настроек от глобального.


    1. Crandel
      25.02.2016 16:22
      +1

      Мои советы предназначены для проектов, в которых от 2 разработчиков и больше. django-environ не требует никаких изменений в настройках, просто секретные данные лежат в отдельном файле, а у вашем варианте придеться дублировать константы в local.py файле. Можно, например, при дебаге парсить ".local" — файл с секретными переменными, а на проде ".prod". Все остальные настройки либо в одном файле, либо в отдельных(последнее предпочтительнее)


      1. Tanner
        25.02.2016 16:34

        Нет, мне однозначно не нужны в проекте лишние классы и парсеры. Это неоправданные накладные расходы. В моём случае все потери сводятся к одному ‘import’, и я хотел бы на этом остановиться. Ну, в крайнем случае ? несколько ‘import’.

        а у вашем варианте придеться дублировать константы в local.py файле

        Не понял этого момента. Какие именно константы придётся дублировать в local.py? Я же показываю в статье, что его содержимое ничего не дублирует, а только уточняет.


        1. Crandel
          25.02.2016 16:38

          Например настройки базы данных или ключи от твиттера и фейсбука, все эти константы придется переопределять в локальном файле.


          1. Tanner
            25.02.2016 16:50

            Разумеется, если продакшн использует одну СУБД, а девелопер ? другую, то в local.py на сервере придётся определить одни DATABASES, а в local.py на машине девелопера ? другие. Но если у нас имеются, например, 20 девелоперов, которые одинаковым способом используют SQLite и два продакшн-сервера с разными базами данных, то можно определить настройки трижды: для разработчиков ? в common.py, а для серверов ? в их персональных local.py.

            Так же и с django-environ, как я понял: в env-файле будут храниться одни настройки, а в settings.py ? другие.


            1. Crandel
              25.02.2016 16:56
              +1

              Вы неправильно поняли, в файле ".env" хранятся только данные, например логин, пароль к базе, ключ твиттера или фейсбука. А в settings.py у вас константа базы прописывается один раз, а заполняется в зависимости от содержания файла ".env" или любого другого, имя которого вы укажете при инициализации environ


              1. Tanner
                25.02.2016 17:01

                То есть проблема в том, что у меня “DATABASES = {…}” будет написано не один раз, а несколько? Ну, это я точно переживу. =)


    1. MechanisM
      26.02.2016 20:43

      А файл с секретами так и так руками копировать надо. В общем, незачот.

      На хероку, например, настройки в админке заполняются. Никакие файлы .env с настройками не нужно иметь в репозитории.


  1. 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(Там про это точно есть отдельная глава)


    1. Tanner
      25.02.2016 18:24
      -1

      Говоря о дублировании, я имел в виду вот этот и подобные варианты. Вариант с общими/частными настройками уже ближе к тому, что использую я. “Two Scoops of Django” не читал. :(


  1. baldr
    25.02.2016 22:40
    -1

    Я использую файлик settings.yaml, в котором переопределяю все константы типа пароля к базе и тп. В settings.py просто читается файл и импортруется в globals() все что найдет, втч с сохранением структуры если хэш.
    Файлик лежит не в папке проекта, а в /usr/local/etc/ и права на доступ имеет только тот пользователь, под которым работает приложение. В принципе, можно класть и в home.

    Хранить любые неверсионируемые файлы в папке проекта не очень люблю, поскольку при деплое обычно заменяется существующая папка новой — и все (или просто симлинк меняется). Бегать перекладывать файлики — это еще вспомнить их надо, а если придет другой человек — он и потерять их может.


    1. Tanner
      26.02.2016 01:50

      Это хорошая идея, тем более, что у моих проектов и так есть их специфический каталог etc с настройками uwsgi, supervisor и др. Разве что я бы скорее использовал JSON вместо YAML.


  1. dadon
    26.02.2016 02:27

    После того как перешел на envdir, с болью вспоминаю разные файлы настроек для разных окружений local / staging / production.
    Кстати, если в продакшене используется postgres и redis, крайне желательно, чтобы при локальной разработке было все тоже самое. Современные инструменты позволяют легко это сделать.


    1. Tanner
      26.02.2016 03:05

      The way you structure your envdir is left to you

      Мне как-то проще структурировать саму конфигурацию, чем привлекать дополнительную сущность и её структурировать.

      А насчёт PG/Redis в дев. среде согласен, у меня установлено на десктопе всё, что используется в продакшне. Но я всё-таки держу в голове ситуацию, когда проект надо запустить на другой машине по-быстрому, с минимумом зависимостей.


      1. ahmpro
        26.02.2016 11:01

        оберните проект в Vagrant и исчезнет боль с разными дев-окружениями
        для staging/production использую отдельный репозиторий с ansible ролями и плейбуками, это упрощает деплой и хранение приватных настроек
        для settings использую похожую схему как у вас, только везде явно указывается какой файл настроек надо использовать(нет неявного импорта local_settings.py)


  1. 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 — другая).


  1. 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


    1. 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),
          }


      1. baldr
        26.02.2016 20:22

        Ну, вообще-то, использовать переменные окружения для передачи паролей и секретных параметров — не очень секьюрно.

        Любой сторонний процесс из-под другого пользователя может проявить любопытство:

        cat /proc/<pid>/environ

        Я не совсем понимаю кто поставил минус за идею хранить все в отдельном файле с урезанными правами, возможно у него есть аргументы против.


        1. Crandel
          26.02.2016 21:26

          А вот и нет, если используется файл ".env", то там видны только общие переменные пользователя