Конфигурация и секреты
Один и тот же образ едет в dev, staging и прод. Различаться должно только то, что снаружи.
Конфигурация — всё, что отличается между средами (dev, staging, prod) и между запусками одного и того же артефакта: адреса баз, таймауты, лимиты, ключи, флаги фич.
Зачем это нужно на практике
Методология 12-factor (свод правил для облачных приложений) формулирует правило III предельно жёстко: конфиг хранится в окружении, а не в коде. Проверочный вопрос простой: можно ли прямо сейчас выложить репозиторий в открытый доступ, ничего не скомпрометировав? Если в нём лежит пароль от прод-базы — ответ «нет», и у вас проблема.
Практический смысл ещё важнее идеологического. Собранный образ должен быть один — тот самый, что прогнали через тесты и staging, — и он должен без пересборки поехать в прод. Как только вы вшиваете URL базы внутрь образа, вы получаете три разных артефакта на три среды, и в прод едет то, что никто не тестировал. Всё веселье с «на стейдже работало» растёт отсюда.
Для двадцати сервисов вопрос перестаёт быть теоретическим: у каждого свои настройки, у каждой среды свои значения, значений сотни. Это уже не «пара переменных», это самостоятельная инженерная задача.
Что считать конфигом, а что — кодом
Граница проходит ровно по одному признаку: различается ли значение между средами.
| Конфиг (наружу) | Не конфиг (в коде) |
| URL базы, брокера, соседних сервисов | Схема таблиц, названия очередей в коде-контракте |
| Таймауты, размеры пулов, лимиты | Алгоритм расчёта скидки |
| Ключи API, пароли, сертификаты | Маршруты HTTP-хендлеров |
| Уровень логирования | Формат лога |
| Флаги включения фич | Сама реализация фичи |
Обратите внимание на распространённое заблуждение: «раз это настройка, вынесем в конфиг» — плохой критерий. Внутренние константы, одинаковые во всех средах (например, максимальная длина имени пользователя), в конфиге только мешают: их нельзя отревьюить как код и легко расстроить руками в проде.
Уровни конфигурации и приоритет
Взрослый сервис читает настройки из нескольких источников с чётким порядком переопределения — от самого общего к самому конкретному:
- Разумные дефолты в коде (сервис должен подниматься локально «из коробки»).
- Файл конфигурации (
application.yml,settings.toml). - Переменные окружения — их подставляет платформа для конкретной среды.
- Централизованное хранилище / config server.
- Флаги фич — самый динамичный слой, меняется на лету.
Вот минимальный, но правильный подход к чтению конфига: значения берутся из окружения, есть дефолты, есть валидация на старте — и сервис честно падает, если чего-то не хватает.
import os
def load_config():
cfg = {
"db_url": os.environ.get("DB_URL", "postgresql://localhost:5432/shop"),
"timeout_ms": int(os.environ.get("TIMEOUT_MS", "2000")),
"log_level": os.environ.get("LOG_LEVEL", "INFO"),
"payments_url": os.environ.get("PAYMENTS_URL", ""),
}
# fail-fast: лучше не запуститься, чем упасть ночью в проде
missing = [k for k, v in cfg.items() if v == ""]
if missing:
raise SystemExit(f"Не заданы обязательные настройки: {missing}")
return cfg
try:
load_config()
except SystemExit as e:
print("Старт прерван:", e)Результат:
Старт прерван: Не заданы обязательные настройки: ['payments_url']Сервис не поднялся и громко сказал почему. Это в сто раз лучше, чем стартовать с пустым адресом платежей и словить NullPointerException в три часа ночи на первом же заказе.
В Kubernetes значения обычно приезжают из ConfigMap:
apiVersion: v1
kind: ConfigMap
metadata:
name: orders-config
data:
LOG_LEVEL: "INFO"
TIMEOUT_MS: "2000"
PAYMENTS_URL: "http://payments.prod.svc.cluster.local:8080"Почему переменных окружения не всегда хватает
Env — прекрасный минимум, но у него есть жёсткие потолки, о которые системы бьются по мере роста.
- Меняются только через рестарт. Захотели поднять таймаут — надо перезапустить под. Во время инцидента, когда всё горит, лишний рестарт — последнее, чего вам хочется.
- Плоские строки. Никакой вложенности, никаких списков и типов. Сложная структура вырождается в
SERVICE_A_RETRY_POLICY_MAX_ATTEMPTS=3и портянку из сорока переменных. - Нет истории и аудита. Кто и когда поменял лимит? Обычно ответ — «непонятно».
- Дублирование. Двадцать сервисов знают адрес брокера. Переехали — правьте в двадцати местах и молитесь, что нигде не опечатались.
- Утекают в логи. Дамп окружения при падении,
/proc/1/environ, отправка контекста в систему мониторинга — и ваш пароль уже в стороннем сервисе.
| Способ | Плюс | Минус | Когда брать |
| Переменные окружения | просто, работает везде | рестарт, плоско, риск утечки | почти всегда — как база |
| Файл в поде (ConfigMap/volume) | структура, можно перечитать без рестарта | нужен код перечитывания | объёмный конфиг |
| Config server (Consul KV, Spring Cloud Config, etcd) | централизация, история, watch | ещё один компонент, который может упасть | десятки сервисов |
| Feature flags | смена поведения за секунды, процентный раскат | флаги копятся и гниют | раскатка фич, аварийный рубильник |
Секреты — это не просто конфиг
Пароль от базы формально тоже настройка, но обращаться с ним как с LOG_LEVEL нельзя: у секрета есть цена утечки, и она измеряется в деньгах и репутации.
Первое, что важно понять про Kubernetes: Secret — это не шифрование. Значение в нём просто закодировано в base64, то есть читается любым, у кого есть доступ к API кластера или к дампу etcd. Настоящий Secret требует включённого шифрования etcd at rest, аккуратного RBAC и, в идеале, внешнего хранилища.
Взрослый подход — специализированное хранилище (HashiCorp Vault, AWS Secrets Manager, облачный KMS) плюс один из способов доставки:
| Способ доставки | Комментарий |
| Env-переменная | Просто, но секрет виден в дампах окружения и в описании пода. Худший из приемлемых вариантов. |
| Файл в примонтированном volume | Лучше: файл не попадает в логи, права можно ограничить, значение легко перечитать при обновлении. |
| Sidecar-агент (например, vault-agent) | Агент сам ходит в хранилище, обновляет файл и умеет продлевать аренду. Приложение просто читает файл. |
| Динамические креды | Vault генерирует временный логин/пароль к базе на 1 час. Утёк — протух сам. Высший пилотаж. |
Ротация
Секрет, который не меняли три года, — это секрет, который знают все бывшие сотрудники. Ротация — регулярная замена. Ключевая идея, которую всегда упускают: нельзя просто заменить старое значение на новое, иначе в момент подмены часть подов ещё со старым ключом получит отказ. Работает только схема с перекрытием:
- Заводим новый ключ, старый пока оставляем валидным (система принимает оба).
- Раскатываем новый ключ по всем потребителям, дожидаемся, пока все переедут.
- Отзываем старый.
Проверить, что схема рабочая, можно только одним способом — отозвав старый ключ и убедившись, что ничего не сломалось. Если этот шаг страшно делать — значит, ротации у вас нет, есть иллюзия ротации.
Как это работает
Типичный запуск сервиса в зрелой системе:
- Платформа монтирует в под ConfigMap (несекретное) и Secret (секретное, лучше — файлом).
- Приложение читает переменные окружения и файлы, накладывает их поверх дефолтов из кода.
- Валидирует получившийся конфиг и падает с внятным сообщением, если чего-то нет (fail-fast, как в примере выше).
- Если есть config server — подтягивает оттуда общие настройки и подписывается на изменения (watch).
- При изменении применяет то, что можно применить на лету (уровень логов, таймауты, флаги), и логирует сам факт изменения — с именем ключа, но без значения.
- Sidecar-агент секретов обновляет файл при ротации, приложение перечитывает его и пересоздаёт пул соединений.
Частые ошибки
- Секрет в git. Даже если удалить коммитом сверху — он остаётся в истории навсегда. Утёк — считайте скомпрометированным и ротируйте, а не «подчищайте».
- Конфиг внутри образа. Разные образы для разных сред — значит, в прод едет непротестированный артефакт.
- Нет валидации на старте. Сервис поднимается с полупустым конфигом и ломается через три часа в самом неудобном месте. Правило: не хватает обязательного значения — не стартуем.
- Логирование конфига целиком. Удобная строчка
logger.info(config)при старте — и пароль от прод-базы навсегда в вашей системе логов, у которой доступ есть у половины компании. Маскируйте. - Config server как единая точка отказа. Он упал — двадцать сервисов не могут стартовать. Обязателен локальный кэш последнего рабочего конфига и способность подняться на нём.
- Динамический конфиг без ревью. Возможность менять прод мышкой в веб-интерфейсе — это прекрасно, пока кто-то в пятницу вечером не выставит таймаут в 0 мс. Изменения критичных значений должны проходить ревью и попадать в аудит-лог.
- Дрейф сред. В проде значение поменяли руками, в git его нет. Через полгода среду пересоздают — и «загадочно» всё ломается. Источник истины должен быть один.
Итоги
- 12-factor: конфиг живёт вне кода. Один образ — все среды, различия приезжают снаружи.
- Конфиг — это то, что отличается между средами. Остальное — код, и ему место в репозитории.
- Переменных окружения хватает не всегда: нужен рестарт, нет структуры, нет истории, дублирование между сервисами, риск утечки. Дальше — файлы, config server, feature flags.
- Валидируйте конфиг на старте и падайте громко (fail-fast) — это дешевле ночной аварии.
- Kubernetes Secret — это base64, а не шифрование. Секреты — в Vault/KMS, доставка файлом, ещё лучше — динамические короткоживущие креды.
- Ротация работает только через перекрытие: сначала два валидных ключа, потом отзыв старого. Не проверили отзыв — ротации нет.