Конфигурационные файлы в Python

    Конфиги. Все хранят их по разному. Кто-то в .yaml, кто-то в .ini, а кто-то вообще в исходном коде, подумав, что "Путь Django" с его settings.py действительно хорош.


    В этой статье, я хочу попробовать найти идеальный (вероятнее всего) способ хранения и использования конфигурационных файлов в Python. Ну, а также поделиться своей библиотекой для них :)


    Попытка №1


    А что насчёт того чтобы хранить конфигурацию в коде? Ну, а что, вроде удобно, да и новых языков не придётся изучать. Существует множество проектов, в которых данный способ используется, и хочу сказать, вполне успешно.


    Типичный конфиг в этом стиле выглядит так:


    # settings.py
    
    TWITTER_USERNAME="johndoe"
    TWITTER_PASSWORD="johndoespassword"
    TWITTER_TOKEN="......."

    Выглядит неплохо. Только одно настораживает, почему секьюрные данные хранятся в коде? Как мы это коммитить будем? Загадка. Разве что вносить наш файл в .gitignore, но это, конечно, вообще не решение.


    Да и вообще, почему хоть какие-то данные хранятся в коде? Как мне кажется код, он на то и код, что должен выполнять какую-то логику, а не хранить данные.


    Данный подход, на самом деле используется много где. В том же Django. Все думают, что раз это самый популярный фреймворк, который используется в самом Инстаграме, то они то уж плохое советовать не будут. Жаль, что это не так.


    Чуть более подробно об этом.


    Попытка №2


    Ладно, раз уж мы решили, что хранить данные в коде — не круто, то давайте искать альтернативу. Для конфигурационных файлов изобретено немалое количество различных форматов, в последнее время набирают большую популярность toml.


    Но мы начнём с того, что нам предлагает сам Python — .ini. В стандартной библиотеке имеется библиотека configparser.


    Наш конфиг, который мы уже писали ранее:


    # settings.ini
    [Twitter]
    username="johndoe"
    password="johndoespassword"
    token="....."

    А теперь прочитаем в Python:


    import configparser  # импортируем библиотеку
    
    config = configparser.ConfigParser()  # создаём объекта парсера
    config.read("settings.ini")  # читаем конфиг
    
    print(config["Twitter"]["username"])  # обращаемся как к обычному словарю!
    # 'johndoe'

    Все проблемы решены. Данные хранятся не в коде, доступ прост. Но… а если нам нужно читать другие конфиги, ну там json или yaml например, или все сразу. Конечно, есть json в стандартной библиотеке и pyyaml, но придётся написать кучу (ну, или не совсем) кода для этого.


    Документация.


    Попытка №3


    А сейчас, я хотел бы показать Вам свою библиотеку, которая призвана решить все эти проблемы (ну, или хотя бы уменьшить ваши страдания :)).


    Называется она betterconf и доступна на PyPi.


    Установка так же проста, как и любой другой библиотеки:


    pip install betterconf

    Изначально, наш конфиг представлен в виде класса с полями:


    # settings.py
    from betterconf import Config, field
    
    class TwitterConfig(Config):  # объявляем класс, который наследуется от `Config`
        username = field("TWITTER_USERNAME", default="johndoe")  # объявляем поле `username`, если оно не найдено, выставляем стандартное
        password = field("TWITTER_PASSWORD", default="johndoespassword") # аналогично
        token = field("TWITTER_TOKEN", default=lambda: raise RuntimeError("Account's token must be defined!")  # делаем тоже самое, но при отсутствии токенавозбуждаем ошибку
    
    cfg = TwitterConfig()
    print(cfg.username)
    # 'johndoe'

    По умолчанию, библиотека пытается взять значения из переменных окружения, но мы также можем настроить и это:


    from betterconf import Config, field
    from betterconf.config import AbstractProvider
    
    import json
    
    class JSONProvider(AbstractProvider):  # наследуемся от абстрактного класса
        SETTINGS_JSON_FILE = "settings.json"  # путь до файла с настройками
    
        def __init__(self):
            with open(self.SETTINGS_JSON_FILE, "r") as f:
                self._settings = json.load(f)  # открываем и читаем
    
        def get(self, name):
            return self._settings.get(name)  # если значение есть - возвращаем его, иначе - None. Библиотека будет выбрасывать свою исключением, если получит None.
    
    provider = JSONProvider()
    
    class TwitterConfig(Config):
        username = field("twitter_username", provider=provider)  # используем наш способ получения данных
        # ...
    
    cfg = TwitterConfig()
    # ...

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


    Хорошо, а что если у нас в конфигах есть булевые значения, или числа, они же в итоге будут все равно приходить в строках. И для этого есть решение:


    from betterconf import Config, field
    # из коробки доступно всего 2 кастера
    from betterconf.caster import to_bool, to_int
    
    class TwitterConfig(Config):
        # ...
        post_tweets = field("TWITTER_POST_TWEETS", caster=to_bool)
    
    # ...

    Таким образом, все похожие на булевые типы значения (а именно true и false будут преобразованы в питоновский bool. Регистр не учитывается.


    Свой кастер написать также легко:


    from betterconf.caster import AbstractCaster
    
    class DashToDotCaster(AbstractCaster):
        def cast(self, val):
            return val.replace("-", ".")  # заменяет тире на точки
    
    to_dot = DashToDotCaster() 
    # ...

    Репозиторий на Github с более подробной документацией.


    Итоги


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


    P.S


    Да, также можно было включить и Pydantic, но я считаю, что он слишком НЕлегковесный для таких задач.

    Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.

    А как вы храните конфиги?

    • 21,4%В исходном коде34
    • 28,9%В переменных окружения (а также .env)46
    • 49,7%В [yaml, ini, toml, etc]79
    Поделиться публикацией
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама

    Комментарии 36

      0
      Столько писанины ради хранения конфигов? Увольте! Вы забыли, кстати, про передачу параметров через командную строку
        0

        Как часто Вы передаёте конфиг приложения в виде аргументов командый строки? :) (А он, вероятно не маленький)

          +1
          Адрес и порт прокси? Имя файла со входными данными? Да постоянно!
            0

            Кажется, количество параметров в моих проектах чутка больше.

            0

            Конфигурационный файл, переменные окружения, параметры коммандной строки — все это служит для настройки работы программы. И логично, обрабатывать это все совместно.

              0

              можете порекомендовать что-то?

          +4
          Разве что вносить наш файл в .gitignore, но это, конечно, вообще не решение.

          Конечно же решение, почему нет?

            –4

            Это просто не удобно. По крайней мере для меня.

              +4

              Я не понимаю. Обычные текстовые конфиг-файлы ведь тоже нужно заносить в .gitignore, чтобы случайно не закоммитить. Что я и делаю во всех своих проектах, где есть какие угодно конфиги — хоть текстовые, хоть кодом. И даже если переменные окружения приспичит хранить где-то в env-файле рядышком, то этот файл тоже должен быть в .gitignore

                0

                Для файлов конфигурации (чаще всего .env) я просто оставляю .env.example. Конечно, это решает проблему и с config.py. Но это скорее непервостепенная проблема, мы до сих пор храним данные в коде, где им быть не надо.

                  +4
                  я просто оставляю .env.example

                  Это никак не отменяет возможности случайно закоммитить .env, прецеденты были (обычно среди неопытных погромистов, пишущих git add . не глядя, но всё же)


                  мы до сих пор храним данные в коде, где им быть не надо.

                  Ну это да. Хотя вот у меня насчитывается как минимум четыре проекта, где нужно частично передать контроль над логикой работы программы пользователю, и приходится городить «тьюринг-полный» конфиг тем или иным образом — или в Lua-песочнице пользовательские скрипты гонять, или наплевать на песочницу и Python-функции прямо в конфиг помещать, или шаблонизатор Jinja2 подключаю если его возможностей достаточно, или что-нибудь ещё в таком духе

            0

            А почему мы не можем в проекте иметь файл config.py который содержит пустые параметры. Таким образом не нужно настраивать .gitignore и бояться слива данных. А реальный файл конфигурации подавать на вход.

              0
              Господи, если ты есть, благослови таких людей которые делают внятные комментарии к коду.

              А вообще — спасибо.
              В разработке год, делаю первый большой проект, и под финал сборки задумался «ёпрст, все токены же можно будет увидеть, и на**евертить в проекте, при желании». Стал искать пути скрытия токенов и данных. Т.к. проект на Амазоне хостится, там есть KMS (Key Management System), а для других проектов, и вообще более мелких — не знал как быть с конфигами.

              Гугл выдавал 100500 статей, которые представляли из себя либо «вот 100500 способов как у вас украдут логин-пассы» либо «вот почему надо защищать логин-пассы». И статьи супер-задротным языком, которые начинающему не понять без пол-литры.

              Поэтому — спасибо за статью, теперь хоть знаю, что гуглить, чтоб детальнее вникнуть.
                0
                A Вы слышали про vault и если да то ваша библиотека умеет с ним работать из коробки?
                  –2

                  Не слышал, не умеет. Но, как и показано в статье, сделать так чтобы работала — легко.

                  +3

                  Пользуюсь dynaconf
                  Умеет в большинство популярных форматов, есть интеграции с Джангой и Флаской, умеет в Vault.

                    0
                    Используете Vault на разных окружениях? Поделитесь опытом пожалуйста :)
                    Как организовывали независимое хранение секретов для разных окружений?
                    +2
                    Конечно, есть json ..., но придётся написать кучу… кода

                    cfg = json.load(open("config.json", "r"))

                    Что не так с кучей?

                      –3

                      Не стоит вырывать из контекста. Там ещё yaml упоминался :)

                        +3
                        cfg = yaml.full_load(open("config.yaml", "r"))
                          0
                          Конечно, есть json в стандартной библиотеке и pyyaml, но придётся написать кучу (ну, или не совсем) кода для этого.

                          Я тут плавно подводил, что следующий вариант в этом плане будет поприятнее в использовании. Конечно, эта задача ни разу не сложная и решается в две строки, но ведь удобство тоже важно? :)


                          Согласен, может не совсем точная формулировка (или вообще не точная), но мне кажется работа с конфигами как с сырыми диктами выглядит не очень удобно.

                            0
                            да вы знаете, гораздо проще написать по одной строчке для каждого формата конфига(кстати, а нафига такой зоопарк? может отрефакторить?), чем тащить вот это всё. с переопределениями, кастерами и прочим ценнейшим функционалом.
                              0
                              Но… а если нам нужно читать другие конфиги, ну там json или yaml например, или все сразу. Конечно, есть json в стандартной библиотеке и pyyaml, но придётся написать кучу (ну, или не совсем) кода для этого.

                              По сравнению с тем что вы предложили, гораздо проще отнаследоваться от congigparser'а и перегрузить метод read, а там по расширению брать и подставлять стандартную реализацию или как вам предложили выше.
                              Я не осуждаю — сам написал собственный парсер, но цель была — б`ольшая производительность. А говорить, что стандартный парсер требует кучи кода (5 строк), а потом рекламировать самописку, которая действительно много требует — это странно.

                                0

                                Тоже вполне хороший вариант.

                        +1
                        Как всегда, пример из readme работает хорошо, но как только нужно применить к реальному проекту — возникают вопросы.

                        Итак, берем ту же Django. Вы помните сколько там всего можно переопределить? А еще и из библиотек добавим параметры…

                        Как просто и быстро решить эту задачу?
                        просто кусок конфига
                        OAUTH2_PROVIDER = {
                            'SCOPES': {'read': 'Read scope', 'write': 'Write scope'},
                            'ACCESS_TOKEN_EXPIRE_SECONDS': 24 * 3600,
                        }
                        
                        REST_FRAMEWORK = {
                            'PAGE_SIZE': 20,
                            'MAX_PAGE_SIZE': 1000,
                            'DEFAULT_PERMISSION_CLASSES': (
                                'rest_framework.permissions.IsAuthenticated',
                            ),
                            'EXCEPTION_HANDLER': 'apiv1.handlers.custom_exception_handler',
                        }
                        EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
                        EMAIL_HOST = 'smtp.server.name'
                        EMAIL_PORT = 25
                        EMAIL_HOST_USER = 'user'
                        EMAIL_HOST_PASSWORD = 'password'
                        EMAIL_USE_TLS = True
                        EMAIL_USE_SSL = False
                        DEFAULT_FROM_EMAIL = 'email@email'
                        SERVER_EMAIL = 'email@email'
                        


                          –4

                          Django — отдельный случай. Если вся экосистема юзает это, то использовать что-то другое — плохая идея.

                            +1
                            Может быть объясните эту мысль вместо категорического утверждения?
                              0

                              А что вам конкретно не понятно? Если всё из мира джанги завязано на этом — зачем исхитряться?

                                +1
                                Минуточку, вы начали статью с утверждения о том что в Django параметры хранятся плохо. Ну допустим не всегда удобно .py файлы держать для конфигов, я тут соглашусь (сам писал статью о своем велосипеде).
                                Затем, уже в следующем разделе, вы развиваете эту мысль. Опять же, читатель заинтригован и не может усидеть, все ждет решения этой проблемы.
                                Внезапно оказывается что для именно Джанги это ваше решение не очень удобно. И исхитряться не надо?
                                Как же так, автор?
                                  –4

                                  Всё так. А статья и не пытается помочь с конфигами в Django. Какое решение этой проблемы? — Выкинуть джангу ;)

                          +1

                          Комрадам в комментариях пропагандирующих vault. Не путайте secret storage с config storage. Первое для sensitive информации, второе для конфигурации. Если уж на то пошло то статья в целом про микс первого со вторым, соответственно логичней в таком случае говорить и про Consul, в котором как раз можно спокойно хранить конфигурацию.

                            +2

                            Но есть же dynaconf, dotenv и dotenv-linter для файлов .env. Зачем ещё один велосипед?

                              –2

                              Мир не стоит на месте! :)

                              +3
                              Хранить секреты в переменных среды небезопасно (1, 2).
                                +1

                                Эта сова натягивается на глобус с очень большими усилиями. Давайте присмотримся по-внимательнее к аргументации в этих статьях.


                                Первая ссылка


                                1) The environment is implicitly available to the process. It's hard to track access and how its content gets exposed. It's easy to grab the whole environment and print it out (can be useful for debugging) or send it as part of an error report for instance.

                                Заменяем слово environment на config file и… ничего не меняется.


                                2) The whole environment is passed down to child processes (if not explicitly filtered) which violates the principle of least privilege.

                                Здесь стоит различать ситуации, когда этот child process содержит или не содержит вредоносный код, т.е. пытается ли он атаковать наше приложение. Если не пытается — он просто не полезет в неизвестные ему переменные окружения и не увидит наши секреты. Если пытается — он и к конфиг-файлу доберётся без проблем (т.к. запущен с теми же правами, что и наше приложение).


                                "Принцип наименьших привилегий" — звучит красиво, но по сути привилегий ровно столько же, вопрос в том, через какой системный вызов их считывать — из окружения или файла.


                                So your secret keys are implicitly made available to any 3rd-party tools you're using (like ImageMagick to resize an image).

                                Лежащий на винте и доступный конфиг не менее "неявно доступен", чем переменные окружения.


                                It's hard to say what those 3rd-party tools will do with the environment, especially in rare occasions like crashes.

                                И вот капелька здравого смысла: мы не знаем, что может нечаянно сделать сторонняя утилита при креше, но предполагаем, что она с большей вероятностью как-то опубликует наши переменные окружения, нежели наш конфиг-файл.


                                3) External developers are not necessarily aware that your environment contains secret keys. By doing so your requirements (tightly controlled access) no longer match with most developers implicit expectation (nothing special in the environment, just generic system configuration). This is a dangerous mismatch.

                                Неизвестно кто делает неизвестно что без понимания как работает наша система, и, сюрприз, он может облажаться. Невероятно, да? Но не будем придираться, просто снова заменим слово environment на config file… и снова смысл не изменился.


                                Вторая ссылка (за вычетом аргументов повторяющих предыдущие)


                                It's common to have applications grab the whole environment and print it out for debugging or error reporting.

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


                                Putting secrets in ENV variables quickly turns into tribal knowledge. New engineers who are not aware of the sensitive nature of specific environment variables will not handle them appropriately/with care (filtering them to sub-processes, etc).

                                В целом это проблема не столько переменных окружения, сколько общего подхода. С одной стороны, особого смысла фильтровать переменные нет. С другой, если очень хочется их фильтровать, то надо всем переменным нашего приложения дать общий префикс, и код фильтрации написать один раз, универсальный, по этому префиксу — какой смысл удалять из окружения только секреты, если можно удалить все свои настройки разом?


                                Overall, secrets in ENV variables break the principle of least surprise, are a bad practice, and will lead to the eventual leak of secrets.

                                Опять же, что есть наименьший сюрприз? 12-factor существует уже очень давно, и очень многими он принят как подход по умолчанию… что подразумевает как раз хранение конфигурации в переменных окружения. Так что лично для меня наименьшим сюрпризом будет ожидать, что секреты передаются либо через переменные окружения, либо через специализированные сервисы вроде vault (впрочем, данные для доступа к vault всё-равно обычно будут в переменных окружения).


                                Резюмируя


                                Кто-то в интернете считает, что вероятность случайной утечки секретов из переменных окружения выше, чем из конфиг-файла. Это мнение ничем не подтверждается, никто не приводит реальную статистику утечек. Даже если это так, нет никакой оценки насколько конкретно выше эта вероятность.


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


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


                                В общем, не надо поднимать панику на пустом месте!

                                0

                                Про dynaconf тут уже вспоминали, посмотрите ещё hydra & omegaconf
                                В общем, хватает уже зрелых решений для управления конфигурацией.

                                Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                                Самое читаемое