Конфигурация и секреты

Один и тот же образ едет в dev, staging и прод. Различаться должно только то, что снаружи.

Конфигурация — всё, что отличается между средами (dev, staging, prod) и между запусками одного и того же артефакта: адреса баз, таймауты, лимиты, ключи, флаги фич.

Зачем это нужно на практике

Методология 12-factor (свод правил для облачных приложений) формулирует правило III предельно жёстко: конфиг хранится в окружении, а не в коде. Проверочный вопрос простой: можно ли прямо сейчас выложить репозиторий в открытый доступ, ничего не скомпрометировав? Если в нём лежит пароль от прод-базы — ответ «нет», и у вас проблема.

Практический смысл ещё важнее идеологического. Собранный образ должен быть один — тот самый, что прогнали через тесты и staging, — и он должен без пересборки поехать в прод. Как только вы вшиваете URL базы внутрь образа, вы получаете три разных артефакта на три среды, и в прод едет то, что никто не тестировал. Всё веселье с «на стейдже работало» растёт отсюда.

Для двадцати сервисов вопрос перестаёт быть теоретическим: у каждого свои настройки, у каждой среды свои значения, значений сотни. Это уже не «пара переменных», это самостоятельная инженерная задача.

Что считать конфигом, а что — кодом

Граница проходит ровно по одному признаку: различается ли значение между средами.

Конфиг (наружу)Не конфиг (в коде)
URL базы, брокера, соседних сервисовСхема таблиц, названия очередей в коде-контракте
Таймауты, размеры пулов, лимитыАлгоритм расчёта скидки
Ключи API, пароли, сертификатыМаршруты HTTP-хендлеров
Уровень логированияФормат лога
Флаги включения фичСама реализация фичи

Обратите внимание на распространённое заблуждение: «раз это настройка, вынесем в конфиг» — плохой критерий. Внутренние константы, одинаковые во всех средах (например, максимальная длина имени пользователя), в конфиге только мешают: их нельзя отревьюить как код и легко расстроить руками в проде.

Уровни конфигурации и приоритет

Взрослый сервис читает настройки из нескольких источников с чётким порядком переопределения — от самого общего к самому конкретному:

  1. Разумные дефолты в коде (сервис должен подниматься локально «из коробки»).
  2. Файл конфигурации (application.yml, settings.toml).
  3. Переменные окружения — их подставляет платформа для конкретной среды.
  4. Централизованное хранилище / config server.
  5. Флаги фич — самый динамичный слой, меняется на лету.

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

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 час. Утёк — протух сам. Высший пилотаж.

Ротация

Секрет, который не меняли три года, — это секрет, который знают все бывшие сотрудники. Ротация — регулярная замена. Ключевая идея, которую всегда упускают: нельзя просто заменить старое значение на новое, иначе в момент подмены часть подов ещё со старым ключом получит отказ. Работает только схема с перекрытием:

  1. Заводим новый ключ, старый пока оставляем валидным (система принимает оба).
  2. Раскатываем новый ключ по всем потребителям, дожидаемся, пока все переедут.
  3. Отзываем старый.

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

Как это работает

Типичный запуск сервиса в зрелой системе:

  1. Платформа монтирует в под ConfigMap (несекретное) и Secret (секретное, лучше — файлом).
  2. Приложение читает переменные окружения и файлы, накладывает их поверх дефолтов из кода.
  3. Валидирует получившийся конфиг и падает с внятным сообщением, если чего-то нет (fail-fast, как в примере выше).
  4. Если есть config server — подтягивает оттуда общие настройки и подписывается на изменения (watch).
  5. При изменении применяет то, что можно применить на лету (уровень логов, таймауты, флаги), и логирует сам факт изменения — с именем ключа, но без значения.
  6. Sidecar-агент секретов обновляет файл при ротации, приложение перечитывает его и пересоздаёт пул соединений.

Частые ошибки

  • Секрет в git. Даже если удалить коммитом сверху — он остаётся в истории навсегда. Утёк — считайте скомпрометированным и ротируйте, а не «подчищайте».
  • Конфиг внутри образа. Разные образы для разных сред — значит, в прод едет непротестированный артефакт.
  • Нет валидации на старте. Сервис поднимается с полупустым конфигом и ломается через три часа в самом неудобном месте. Правило: не хватает обязательного значения — не стартуем.
  • Логирование конфига целиком. Удобная строчка logger.info(config) при старте — и пароль от прод-базы навсегда в вашей системе логов, у которой доступ есть у половины компании. Маскируйте.
  • Config server как единая точка отказа. Он упал — двадцать сервисов не могут стартовать. Обязателен локальный кэш последнего рабочего конфига и способность подняться на нём.
  • Динамический конфиг без ревью. Возможность менять прод мышкой в веб-интерфейсе — это прекрасно, пока кто-то в пятницу вечером не выставит таймаут в 0 мс. Изменения критичных значений должны проходить ревью и попадать в аудит-лог.
  • Дрейф сред. В проде значение поменяли руками, в git его нет. Через полгода среду пересоздают — и «загадочно» всё ломается. Источник истины должен быть один.

Итоги

  • 12-factor: конфиг живёт вне кода. Один образ — все среды, различия приезжают снаружи.
  • Конфиг — это то, что отличается между средами. Остальное — код, и ему место в репозитории.
  • Переменных окружения хватает не всегда: нужен рестарт, нет структуры, нет истории, дублирование между сервисами, риск утечки. Дальше — файлы, config server, feature flags.
  • Валидируйте конфиг на старте и падайте громко (fail-fast) — это дешевле ночной аварии.
  • Kubernetes Secret — это base64, а не шифрование. Секреты — в Vault/KMS, доставка файлом, ещё лучше — динамические короткоживущие креды.
  • Ротация работает только через перекрытие: сначала два валидных ключа, потом отзыв старого. Не проверили отзыв — ротации нет.
Проверьте себя
1. Почему принцип 12-factor требует держать конфигурацию вне кода?
AЧтобы код быстрее компилировался
BЧтобы один и тот же протестированный артефакт мог без пересборки поехать в любую среду, а секреты не лежали в репозитории
CЧтобы переменные окружения можно было менять без рестарта пода
DПотому что Kubernetes не умеет читать файлы конфигурации
2. Что верно про Secret в Kubernetes?
AЗначения в нём зашифрованы, поэтому дополнительных мер не требуется
BЗначения всего лишь закодированы в base64, так что нужны шифрование etcd, RBAC, а лучше — внешнее хранилище вроде Vault
CSecret нельзя примонтировать файлом, только пробросить через переменную окружения
DSecret автоматически ротирует пароли раз в сутки