Программирование с ИИ
ENRU
13.4

Конфиги и окружения

три беды одного маленького файла

Что это

Окружение — это набор настроек, с которыми запускается один и тот же код. Обычно их три: твоя машина, тестовый сервер, боевой сервер. Отличаются они не кодом, а значениями: адрес базы, ключи API, включена ли отладка.

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

Как выглядит в коде

Файл настроек, который выглядит нормально

import os

DATABASE_URL = os.environ.get("DATABASE_URL", "sqlite:///test.db")
PORT = os.environ.get("PORT", 8000)
DEBUG = os.environ.get("DEBUG", False)

Три строки, три беды. Причём на твоей машине, где переменных нет, всё безупречно:

измерено
база   : sqlite:///test.db
порт   : 8000 int
отладка: False bool
  -> режим отладки выключен
порт + 1 = 8001

Теперь то же самое с переменными — так, как запускается на сервере:

PORT=8080 DEBUG=false DATABASE_URL=postgresql://prod/db python3 app.py
измерено
база   : postgresql://prod/db
порт   : '8080' str
отладка: 'false' str
  -> режим отладки ВКЛЮЧЁН
порт + 1 = TypeError: can only concatenate str (not "int") to str

Разберём по одной.

1. DEBUG=false включил отладку. Строка 'false' — непустая, а значит, истинная. Проверка if DEBUG: срабатывает. На боевом сервере это означает подробные страницы с ошибками, трассировки наружу и всё, что обычно видят только разработчики. Ошибка в одну строку, последствия — утечка.

2. Порт стал строкой. TypeError — ещё удачный случай: он хотя бы громкий. Хуже, когда строка молча проходит дальше.

3. Тип зависит от того, задана ли переменная. На твоей машине PORT — число, на сервере — строка. Один и тот же код, разные типы. Проверить это локально нельзя в принципе.

И отдельно — самая тихая из трёх: DATABASE_URL со значением по умолчанию sqlite:///test.db. Забыли задать переменную на сервере — приложение не упало, не пожаловалось, а начало писать в локальный файл. Узнаёшь об этом, когда данные ищут и не находят.

Как надо

from pydantic import Field
from pydantic_settings import BaseSettings


class Settings(BaseSettings):
    database_url: str
    api_key: str
    port: int = 8000
    debug: bool = False
    timeout: float = Field(default=5.0, gt=0)


settings = Settings()

Разница вся в типах. port: int — значит, при запуске строка '8080' превратится в число или программа не запустится. debug: bool — значит, false станет настоящим False. У database_url и api_key умолчания нет — значит, без них запуска не будет.

Запуск без переменных:

измерено
pydantic_core._pydantic_core.ValidationError: 2 validation errors for Settings
database_url
  Field required [type=missing, input_value={}, input_type=dict]
api_key
  Field required [type=missing, input_value={}, input_type=dict]

Обе недостающие переменные названы сразу, при старте. Не по одной, не в середине работы, не через час после выкладки.

С переменными:

измерено
база   : postgresql://prod/db
порт   : 8080 int
отладка: False bool
порт+1 : 8081

DEBUG=false стал False, как и ожидает любой здравый смысл. А если значение окажется мусором:

измерено
port
  Input should be a valid integer, unable to parse string as an integer
  [type=int_parsing, input_value='вось', input_type=str]

Названа настройка, названо значение, названа причина. Это и есть разница между «программа упала» и «программа объяснила».

Что лежит в репозитории, а что нет

измерено
.env           ← настоящие значения. В .gitignore. Никогда не коммитится.
.env.example   ← те же имена, пустые или примерные значения. Коммитится.

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

измерено
DATABASE_URL=postgresql://user:password@localhost/dbname
API_KEY=
PORT=8000
DEBUG=false

И напоминание из статьи 12.4: в шаблонах проектов строки .env в .gitignore может не оказаться. Проверяется это один раз, командой git ls-files, и лучше в первый день.

Три окружения — один код

Правило простое: код не знает, где он работает. Нет if environment == "production", разбросанных по проекту; есть настройки, которые в разных местах разные.

Единственное исключение, которое оправдано, — одна переменная вроде ENV=production и несколько решений на её основе в одном месте: уровень логов, показывать ли подробные ошибки, включать ли документацию API.

Как сказать ИИ

Запрос
Сделай модуль настроек на pydantic-settings.
Нужны: database_url (строка, обязательная),
api_key (строка, обязательная), port (int, по умолчанию 8000),
debug (bool, по умолчанию false).
Умолчаний для обязательных не ставь.
Рядом — .env.example со всеми именами.
Запрос
Найди в проекте все обращения к os.environ и os.getenv.
Для каждого скажи: тип значения, есть ли умолчание
и что произойдёт, если переменная не задана.
Таблицей. Код пока не меняй.

Этот запрос стоит выполнить на любом проекте, которому больше месяца: обращения к окружению расползаются незаметно.

Запрос
Проверь этот конфиг на три ошибки: строка вместо числа,
строка 'false' вместо булева значения, умолчание там,
где его быть не должно. Что нашёл — перечисли.
Запрос
Проект должен падать при старте, если не заданы
обязательные настройки. Сейчас он падает позже.
Сделай проверку при запуске, с понятным сообщением,
в котором перечислено всё недостающее сразу.

На что смотреть

os.environ.get с умолчанием для адреса базы или ключа. Самая тихая из ошибок этой статьи.

Настройка используется как число или как флаг без приведения типа. Из окружения приходит строка, всегда.

DEBUG=true как строка. Любая непустая строка истинна, включая 'false' и '0'.

Настройки читаются в десяти местах. Один модуль настроек, остальной код обращается к нему.

.env в репозитории. Проверяется git ls-files.

Нет .env.example. Проект нельзя запустить, не угадав список переменных.

Ключи заданы прямо в коде «временно». Временное живёт дольше всего, а git помнит всё (статья 5.7).

if production: по всему проекту. Код знает, где он работает, — значит, на тестовом сервере проверяется не то, что поедет в бой.

Шпаргалка

  • Всё, что приходит из окружения, — строка. Всегда.

  • DEBUG=false без приведения типа включает отладку.

  • Умолчание для обязательной настройки прячет ошибку, а не помогает.

  • Типизированные настройки называют все недостающие переменные сразу.

  • Настройки проверяются при старте, а не в момент использования.

  • .env — в .gitignore, .env.example — в репозиторий.

  • Один модуль настроек; остальной код читает его, а не окружение.

  • Код не должен знать, на каком сервере он работает.