Перейти к содержимому
ГоМастер

Поиск по урокам

Найдите урок, курс, термин или раздел

Урок 18 из 25 ~16 мин +50 XP

Конфигурация сервисов

Композиционный корень из урока 17 собран, но в нём остались зашитые значения: порты :8081 и :8082, адрес Consul, доля сэмплинга Jaeger. Собранный бинарь живёт ровно в одной среде — на твоей машине, а пересобирать артефакт ради стейджинга — антипаттерн, который ломает и аудит, и откаты. Тема урока — конфигурация микросервисов: ты вынесешь все значения наружу через флаги и env-переменные, соберёшь их в одну структуру Config, добавишь безопасные дефолты, валидацию с fail fast и честно поговорим про viper и секреты. К концу урока один и тот же go-артефакт будет запускаться на ноутбуке, в CI и в проде без единой правки кода — а конфиг go сервиса будет собирать ровно одно место: композиционный корень.

Содержание урока · 11 разделов

Один артефакт, много сред#

Пройди по проекту глазами конфигуратора: транспорт из урока 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-хелперы поверх дефолтов

GO
const 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(): правила, а не догмы

GO
func (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.

func envString(key, def string) string { if v, ok := os.(prefix + key); ok { return v } return } func Flags() (Config, error) { var c Config c.HTTPAddr = envString("HTTP_ADDR", ":8081") flag.(&c.HTTPAddr, "http-addr", c.HTTPAddr, "HTTP listen address") flag.Parse() if err := c.(); err != nil { return Config{}, err } return c, nil }

Путь конфигурации при старте сервиса

Расставь шаги старта по порядку: от процесса до слушателей.

Строки

Ваша программа

Нажимайте на строки слева, чтобы собрать программу

Источники конфигурации#

ИсточникИнструментКогда оправданГлавный риск
Дефолты в коделитералы в Flags()локальный запуск и дев без настроекпрод остался на дефолтах — лови валидацией
env-переменныеos.LookupEnv + префиксконтейнеры, Kubernetes, CIстроковые значения — парсить и валидировать
Флагиflagлокальный запуск, docker run, отладкавидны в ps — секретам здесь не место
Файл yaml/tomlviperдесятки полей и непрограммист за настройкамисекрет в git — репозиторий leak'ает
Secret storeVault, secrets оркестраторатокены, пароли, ключисложность и доступы — начинать не с него
Hot reloadviper WatchConfigлимиты и доли сэмплинга на живом трафикесостояние меняется на лету без рестарта

Термины урока

Термин

Twelve-Factor III

нажмите, чтобы перевернуть
Определение

Конфигурация хранится в окружении, а не в коде: один артефакт живёт во всех средах, меняется только env — пункт свода правил контейнерных платформ.

1 / 6

Что дальше#

Сервис научился рождаться в любой среде: 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 ./... собирался в чистой среде без единой переменной.

Личные заметкиПусто
Хранятся только в вашем браузере (go-kit · Конфигурация сервисов)

Вопросы и ответы по уроку

Что сильнее — 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 и никогда не попадают в дефолты, код и флаги.

Термины урока

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

Следующая ступень учебного пути

gRPC25 уроков~14 чпродвинутый25 уроков уже доступны

gRPC-Go: высокопроизводительный RPC

Курс gRPC-Go: proto3, кодогенерация, unary и streaming вызовы, interceptors, TLS, дедлайны, load balancing. 25 уроков и итоговый проект.

  • Писать .proto-схемы и генерировать код
  • Реализовывать unary и streaming сервисы
  • Управлять метаданными, дедлайнами и отменами
  • Строить interceptors для логов и аутентификации