Один артефакт, много сред#
Пройди по проекту глазами конфигуратора: транспорт из урока 04-http-transport слушает ":8081", gRPC из урока 13 — ":8082", consul-регистратор из урока 12-discovery ходит в "localhost:8500", Jaeger-сэмплер из урока 08-tracing трассирует с вероятностью из константы. Каждая строка — решение среды, спрятанное в код. Twelve-Factor, свод правил, по которому живут контейнерные платформы, формулирует это в пункте III — фундаменте конфигурации микросервисов: конфигурация хранится в окружении, а не в коде — один и тот же артефакт продвигается от дева к проду, меняя только окружение. В go-kit эта идея ложится без трения, потому что место для неё уже приготовлено: раскладка из урока 17 отвела сборку зависимостей композиционному корню, а урок 03-service запретил читать env в глубине методов. Конфигурация — это не слой сервиса и не библиотечный модуль: это значения, которыми main кормит конструкторы. Отсюда правило урока: env go-сервис читает в одном месте — в main, и дальше по проекту едут уже обычные значения.
Три источника и приоритет#
Практика выработала три слоя источников, и у каждого своя роль. Дефолты в коде — то, что работает без всякой настройки: локальный запуск go run ./cmd/stringsvc должен поднимать рабочий сервис, не требуя ни одной переменной. Переменные окружения — язык контейнеров и оркестраторов: Kubernetes, docker run и CI передают настройки именно env, поэтому env go-сервиса — основной канал продовых значений. Флаги командной строки — язык локального запуска и разовых экспериментов: go run ./cmd/stringsvc -http-addr=:9090 читается лучше, чем экспорт переменной. Приоритет вытекает из способа чтения: сначала берётся дефолт, потом env перезаписывает его, если задан, потом флаг перезаписывает всё, если задан явно — что задано явнее, то и побеждает. Такой порядок даёт приятное свойство: прод задан в окружении, а разработчик может точечно переопределить один флаг, не трогая остальное. Важно и обратное: не плодить источники сверх трёх — файлы конфигурации добавим отдельным разговором, когда поймём, зачем они нужны, а не потому что «так принято».
Config и флаги: одна структура на сервис
GO// internal/config/config.go — единственный пакет, знающий про окружение.
type Config struct {
HTTPAddr string // слушатель HTTP из урока 04
GRPCAddr string // слушатель gRPC из урока 13
ConsulAddr string // агент Consul из урока 12
SampleRate float64 // вероятность сэмплинга Jaeger из урока 08
LogLevel string
}
func Flags() (Config, error) {
var c Config
flag.StringVar(&c.HTTPAddr, "http-addr", ":8081", "HTTP listen address")
flag.StringVar(&c.GRPCAddr, "grpc-addr", ":8082", "gRPC listen address")
flag.StringVar(&c.ConsulAddr, "consul-addr", "localhost:8500", "Consul agent")
flag.Float64Var(&c.SampleRate, "sample-rate", 0.01, "Jaeger sampling probability")
flag.StringVar(&c.LogLevel, "log-level", "info", "log level")
flag.Parse()
if err := c.validate(); err != nil {
return Config{}, err
}
return c, nil
}Вся конфигурация сервиса — одна структура и один конструктор: поле с дефолтом, обращённым в прошлые уроки (порты 04 и 13, Consul 12, сэмплер 08). flag.Parse читает аргументы командной строки; каждый флаг получает дефолт, совпадающий с локальной конвенцией курса, — так безопасный дефолт возникает сам собой. validate() вызывается прямо в конструкторе: невалидная сборка не выходит из пакета config — об этом ниже. Структура возвращается значением, не указателем: Config после Parse больше не меняется, и передавать его по проекту безопасно.
env-переменные и префиксы#
env-канал строится поверх дефолтов: каждая переменная переопределяет значение, если задана. Штатный способ — os.LookupEnv вместо os.Getenv: LookupEnv различает «переменная пустая» и «переменной нет», а os.Getenv возвращает пустую строку в обоих случаях, и ты не отличишь явную настройку от её отсутствия. Формула чтения простая: если есть — взять, нет — оставить дефолт. Чтобы переменные одного сервиса не столкнулись с чужими, каждый сервис носит префикс: STRINGSVC_HTTP_ADDR, STRINGSVC_CONSUL_ADDR — конвенция «ПРОПИСНЫЕ_С_ПОДЧЁРКИВАНИЯМИ», которую оркестраторы понимают из коробки. Не выдумывай формат значений: порты и адреса храни строками до самого listener, проценты — числами, списки — через запятую; парсинг делает один хелпер, а не каждый вызов в проекте. Мост к канону курса: сервис и endpoint'ы не видят ни env, ни префиксов — они получают уже разобранные значения через конструкторы из урока 17; тест из урока 03 снова собирается одной строкой в чистой среде, и никакая среда не меняет поведение сервиса незаметно.
env-хелперы поверх дефолтов
GOconst prefix = "STRINGSVC_"
func envString(key, def string) string {
if v, ok := os.LookupEnv(prefix + key); ok {
return v
}
return def
}
func envFloat(key string, def float64) float64 {
if v, ok := os.LookupEnv(prefix + key); ok {
f, err := strconv.ParseFloat(v, 64)
if err == nil {
return f
}
}
return def
}
// порядок в Flags(): сначала env поверх дефолтов, потом флаги поверх env:
c.HTTPAddr = envString("HTTP_ADDR", c.HTTPAddr)
c.SampleRate = envFloat("SAMPLE_RATE", c.SampleRate)
flag.StringVar(&c.HTTPAddr, "http-addr", c.HTTPAddr, "HTTP listen address")Хелперы инкапсулируют всю работу с os.LookupEnv — других обращений к env в проекте быть не должно. Обрати внимание на порядок вызовов: env применяется к уже заполненным дефолтам, и только потом флаги объявляются с env-значением как дефолтом — flag.Parse, вызванный после, перезапишет только явно переданные аргументы. Ошибка парсинга в envFloat честно игнорируется: кривое значение оставить без действия опасно, но validate() ниже всё равно не пропустит невалидные диапазоны; в проде кривое значение надо логировать — добавь лог через переданный logger, если хочешь громче.
Безопасные дефолты и fail fast#
Дефолт — это не «значение на всякий случай», а дизайн-решение: правильный дефолт делает первый запуск успешным, а неправильный — превращает прод в отладку. Проверь себя двумя вопросами: что случится, если запустить сервис с нулём настроек (должен подняться локально и залогировать, куда ходит), и что случится, если продовая переменная опечаталась (должен упасть на старте, а не на сотом запросе). Второй вопрос — принцип fail fast: невалидная конфигурация обнаруживается в validate() до поднятия слушателей, до регистрации в Consul из урока 12 и до первого запроса; ошибка возвращается, main из урока 17 пишет её в лог и завершает процесс с ненулевым кодом выхода. Оркестратор увидит честный крах, перезапустит под и покажет красный статус — вместо зомби, который «работает», но ходит не туда. Что проверять: адреса непустые, SampleRate в диапазоне от 0 до 1 (канон сэмплеров из урока 08), LogLevel из белого списка уровней из урока 06, для лимитера из урока 09 — rate больше нуля, для breaker из урока 10 — таймауты положительные. И у каждой будущей настройки то же правило: когда в уроке 19 появится GracePeriod — время дренажа, он придёт в Config и в validate, а не магической константой в коде. Каждое правило — одна строка и один понятный текст ошибки. Так работает конфигурация микросервисов: невалидно — значит, не стартуем.
validate(): правила, а не догмы
GOfunc (c Config) validate() error {
switch {
case c.HTTPAddr == "" || c.GRPCAddr == "":
return errors.New("config: адреса слушателей обязательны")
case c.ConsulAddr == "":
return errors.New("config: адрес Consul обязателен")
case c.SampleRate < 0 || c.SampleRate > 1:
return fmt.Errorf("config: sample-rate %v вне [0, 1]", c.SampleRate)
case c.LogLevel != "info" && c.LogLevel != "debug" && c.LogLevel != "warn":
return fmt.Errorf("config: неизвестный log-level %q", c.LogLevel)
}
return nil
}
// main из урока 17 получает fail fast бесплатно:
func main() {
if err := run(); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1) // оркестратор видит крах и рестартует под
}
}Четыре правила покрывают канон курса: слушатели из уроков 04 и 13, Consul из урока 12, сэмплер из урока 08, уровни kitlog-логгера из урока 06. Текст ошибки называет поле и плохое значение — при ночном инциденте это экономит минуты. Важно: validate вызывается ДО сборки зависимостей — неконфигурируемый сервис не должен успеть открыть соединение с базой или зарегистрироваться в реестре. os.Exit(1) — честный сигнал для supervisor'а: exit-код — тоже конфигурационный контракт между сервисом и оркестратором.
viper и файлы: когда они нужны#
Когда полей становится десятки, а сред — много, рождается желание вынести конфигурацию в yaml или toml и менять её без пересборки и перезапуска флагов; стандартный инструмент экосистемы — spf13/viper. Он читает файлы, env с префиксами и дефолты в одном конвейере: viper.SetDefault задаёт безопасный дефолт, viper.SetEnvPrefix и viper.AutomaticEnv подключают env-канал, viper.SetEnvKeyReplacer мапит точки в подчёркивания, viper.ReadInConfig разбирает файл. Честная сторона медали: viper — тяжёлая зависимость для сервиса, у которого четыре поля, и в go-kit конфигурация не вынесена в отдельный модуль именно потому, что конфигурация — не слой сервиса, а обязанность main из урока 17; библиотека, которая диктовала бы способ конфигурирования, нарушала бы собственную философию из урока 01-filosofiya. Возьми viper, когда выполняются ОБА условия: полей больше двадцати и есть непрограммист, которому нужен читаемый файл. А вот WatchConfig и hot reload применяй с осторожностью: большинство значений (адреса слушателей, сэмплер) читаются один раз на старте, и живая перезагрузка нужна разве что лимитам — их в курсе умеет пересобирать срез endpoint.Middlewares из урока 02-endpoint, который «собирается по флагам запуска или конфигу»: пересобери срез — и лимитер из урока 09 начнёт вести себя иначе без рестарта.
Секреты: не в флагах, не в git, не в логах#
Токены, пароли и ключи — особый класс значений, и у него свои запреты. Флаги не годятся физически: их видно в ps aux каждому пользователю машины и в history shell'а. Git не годится по определению: секрет в репозитории leak'ает при первом же форке, и чистка истории не помогает. Env — приемлемый минимум, так передают значения почти все оркестраторы; взрослее — файлы, смонтированные платформой, и secret-менеджеры вроде Vault, из которых main читает значение при старте. Но есть канал утечки, о котором забывают чаще: логи. Логгер из урока 06-logirovanie пишет структурные пары, и один невинный logger.Log("config", cfg) вывалит всё, включая секреты, в централизованный сбор — читатели логов обычно не те же люди, что владельцы секретов. Правило: логируй конфигурацию ПОЛЯМИ, а не структурой целиком, а секретные поля маскируй или пропускай; validate() как раз место, где удобно залогировать собранную конфигурацию без чувствительных значений. И никогда не подставляй секреты в дефолты: дефолт живёт в коде, значит — в git.
Стратегия сбора: STRINGSVC_HTTP_ADDR=:9090 задан в окружении, флаг -http-addr не передан, в коде дефолт :8081. На чём поднимется слушатель?
Где по канону курса живёт чтение os.LookupEnv?
Оператор опечатался в продовой переменной STRINGSVC_CONSUL_ADDR. Что сделает правильно настроенный сервис?
sample-rate вне диапазона
Прочитайте код и выберите, что он выведет:
$ go run ./cmd/stringsvc -sample-rate=1.5
// Flags(): env нет, флаг прочитан, SampleRate = 1.5
// дальше вызывается c.validate()Пустая переменная против отсутствующей
Прочитайте код и выберите, что он выведет:
// На машине экспортирована пустая переменная:
export STRINGSVC_SAMPLE_RATE=
// дефолт: 0.01
// envFloat читает LookupEnv и видит ok=true, ParseFloat("") даёт ошибку
// envString на пустой переменной вернёт "" — и это валидное переопределениеСобери конфигурацию из трёх источников
Восстанови четыре строки. Пропуски — по порядку: функция чтения env, различающая «пусто» и «нет», возвращаемое значение при отсутствии переменной, конструктор флага для строки, метод валидации перед выдачей Config.
Путь конфигурации при старте сервиса
Расставь шаги старта по порядку: от процесса до слушателей.
Строки
Ваша программа
Нажимайте на строки слева, чтобы собрать программу
Источники конфигурации#
| Источник | Инструмент | Когда оправдан | Главный риск |
|---|---|---|---|
| Дефолты в коде | литералы в Flags() | локальный запуск и дев без настроек | прод остался на дефолтах — лови валидацией |
| env-переменные | os.LookupEnv + префикс | контейнеры, Kubernetes, CI | строковые значения — парсить и валидировать |
| Флаги | flag | локальный запуск, docker run, отладка | видны в ps — секретам здесь не место |
| Файл yaml/toml | viper | десятки полей и непрограммист за настройками | секрет в git — репозиторий leak'ает |
| Secret store | Vault, secrets оркестратора | токены, пароли, ключи | сложность и доступы — начинать не с него |
| Hot reload | viper WatchConfig | лимиты и доли сэмплинга на живом трафике | состояние меняется на лету без рестарта |
Флаг -db-password живёт в ps aux и в shell history: любой локальный пользователь прочтёт пароль. Секрет в git чистится плохо: форк, кэш CI, зеркала — история уже разошлась. Лог с полной структурой Config уезжает в централизованный сбор, где читатели не совпадают с владельцами секрета. Рабочий минимум: секреты только через env или secret store, в логах — маскированные поля, в дефолтах — никогда. Прогони grep по репозиторию на парольные слова перед первым продом — дешевле, чем ротация всех ключей.
Соблазн — прокинуть Config во все конструкторы: service.New(cfg), endpoint.New(cfg, ...). Не поддавайся: сервис, принимающий Config, снова знает обо всём окружении, и граница слоёв из урока 17 размывается — тесту уже нужен целый Config, чтобы собрать один метод. Канон курса: конструкторы принимают конкретные значения и зависимости (addr string, rate float64, repo Repository), а Config живёт только в main и пакете config. Тогда тест собирается заглушками, а среда не протекает в бизнес-логику.
Урок про конфигурацию проверяется запуском локально: в песочнице курса сторонние модули и процессы не исполняются, поэтому интерактивы построены на чтении кода. Локально: запусти сервис без флагов и убедись, что дефолты поднимают оба слушателя; затем задай STRINGSVC_HTTP_ADDR=:9090 и флаг -sample-rate=1.5 — первый должен переехать, второй — уронить процесс с осмысленной ошибкой до поднятия слушателей.
Термины урока
Что дальше#
Сервис научился рождаться в любой среде: Config собран из дефолтов, env и флагов, validate отсеивает кривые значения, main из урока 17 кормит конструкторы готовыми числами и строками. Но рождение — половина жизненного цикла; вторая половина — смерть, и по умолчанию она некрасивая: Ctrl+C и SIGTERM от оркестратора убивают процесс мгновенно, недоделанные запросы обрываются, а консул-запись из урока 12 остаётся зомби в реестре. Урок 19-graceful исправит это: сигналы, дренаж HTTP- и gRPC-вызовов, вычеркивание из ротации до смерти процесса и закрытие трейсера. Детали настройки наблюдаемости из конфигурации освежи в уроке 08-tracing и уроке 06-logirovanie, карту всех слоёв — на странице курса; а если хочется освежить, зачем env запрещён в методах сервиса, вернись к уроку 03-service: там у этого запрета есть причина, а не только правило.
Песочница урока
Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.
Запустите код, чтобы увидеть вывод.
Коротко: что запомнить
- Конфигурация микросервисов — значения среды, а не код: Twelve-Factor III, один артефакт живёт во всех средах без пересборки.
- Три источника с приоритетом: дефолты в коде, env-переменные, флаги; что задано явнее — то побеждает.
- Вся конфигурация — одна структура Config и один конструктор Flags() в пакете config; сервис и endpoint'ы env не видят никогда.
- os.LookupEnv вместо os.Getenv: различение «пусто» и «нет» спасает от тихих дефолтов; префикс STRINGSVC_ разводит сервисы.
- Fail fast: validate() ловит невалидные значения до слушателей и регистрации в Consul, процесс выходит с кодом 1.
- viper оправдан при десятках полей и непрограммисте за настройками; hot reload нужен редко — лимиты умеет пересобирать срез endpoint.Middlewares.
- Секреты не в флагах (ps), не в git, не в логах: env или secret store, поля логируются маскированно.
- Конструкторы принимают значения, а не *Config: иначе среда протекает в сервис и тест перестаёт собираться одной строкой.
Домашнее задание
Настрой конфигурацию stringsvc по уроку. Первое: собери пакет internal/config со структурой Config — порты HTTP и gRPC из уроков 04 и 13, адрес Consul из урока 12, сэмплер из урока 08, уровень логов из урока 06 — и конструктором Flags(), где каждый флаг имеет безопасный дефолт. Второе: добавь env-хелперы на os.LookupEnv с префиксом STRINGSVC_ и проверь каскад приоритетов: запусти без настроек (дефолты), с env STRINGSVC_HTTP_ADDR=:9090 (env победил), с env и флагом -http-addr=:9091 (флаг победил) — запиши три адреса слушателей в конспект. Третье: напиши validate() с четырьмя правилами урока и урони сервис флагом -sample-rate=2 — убедись, что exit-код ненулевой, в stderr текст с именем поля, а слушатели не поднимались. Четвёртое: прогони grep по репозиторию на парольные слова и убедись, что ни один секрет не живёт в коде или дефолтах; логируй собранную конфигурацию полями без чувствительных значений. Пятое: проверь канон урока 03 — сервис и endpoint'ы не импортируют пакет os и config; если импортируют, перенеси значения в конструкторы и добейся, чтобы go test ./... собирался в чистой среде без единой переменной.
Личные заметкиПусто
Вопросы и ответы по уроку
Что сильнее — env или флаг? В интернетах спорят.
Это не свойство среды, а решение приложения: важен порядок чтения в твоём Flags(). В схеме курса дефолт читается первым, env перезаписывает его, флаг объявляется с env-значением как дефолтом и применяется последним — значит, флаг сильнее. Контейнеры обычно вообще не передают флаги, поэтому в проде решает env, а флаги остаются инструментом локального запуска и отладки. Зафиксируй порядок в коде и не меняй его между сервисами.
Нужен ли viper маленькому сервису?
Скорее нет: для четырёх-десяти полей хватает flag и os.LookupEnv без единой сторонней зависимости. viper оправдан, когда полей десятки, конфигурацию ведёт непрограммист или нужны файлы разных форматов и hot reload. При этом горячая перезагрузка нужна меньшинству значений: адреса и сэмплер читаются один раз, а лимиты умеет пересобирать срез endpoint.Middlewares из урока 02 — для него конфиг и пишут.
Как не засветить секреты в логах при отладке конфигурации?
Логируй конфигурацию полями, а не структурой целиком: пары вида addr и sample-rate полезны, db-password — нет. Сделай метод, возвращающий версию Config с маскированными чувствительными полями, и логируй её из validate() при старте — увидишь всё, что нужно для расследования, и не сольёшь ничего. Секреты при этом живут в env или secret store и никогда не попадают в дефолты, код и флаги.
Термины урока
Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.
Изучайте дальше
Следующая ступень учебного пути
gRPC-Go: высокопроизводительный RPC
Курс gRPC-Go: proto3, кодогенерация, unary и streaming вызовы, interceptors, TLS, дедлайны, load balancing. 25 уроков и итоговый проект.
- Писать .proto-схемы и генерировать код
- Реализовывать unary и streaming сервисы
- Управлять метаданными, дедлайнами и отменами
- Строить interceptors для логов и аутентификации