Содержание урока · 11 разделов
- Профили окружений Go: выбор через флаг --env или APP_ENV
- База и переопределения: config.yaml плюс config.prod.yaml
- Третий слой: переменные окружения поверх файлов
- Окружение → файл → env-префикс
- Что лежит в файлах, что в переменных
- Живое место: staging отправил письма клиентам
- Спор: конфиг на файловой системе или всё в переменных деплоя
- Что дальше
- Песочница урока
- Коротко: что запомнить
- Вопросы и ответы
Сервис собирается один раз, а живёт в трёх местах: ноутбук разработчика, staging с тестовыми интеграциями и prod с боевыми базами. Настройки при этом разные — порт, уровень логов, адреса соседей, — и самый дешёвый способ развести их — файлы по суффиксу окружения: config.dev.yaml и config.prod.yaml в репозитории. Бинарник остаётся один: на старте он узнаёт контур из флага или переменной APP_ENV и читает свой профиль через SetConfigName("config." + env). В этом уроке собираем каркас профилей окружений Go: выбор контура с белым списком, общая база с переопределениями через MergeInConfig, третий слой из переменных окружения и правило, по которому секреты в файлах не живут вовсе. Заодно — история о том, как staging отправил письма клиентам, и спор о том, где вообще должен лежать конфиг.
Профили окружений Go: выбор через флаг --env или APP_ENV#
Выбор окружения делается до первого обращения к viper: контур — это аргумент того, какой файл читать, а не настройка внутри конфига. Классическая цепочка: флаг --env, затем переменная APP_ENV, затем дефолт. Флаг удобен локально и в скриптах; в контейнерах флаг прокинуть не всегда возможно, там выручает APP_ENV из манифеста деплоя. BindPFlag связывает флаг с реестром для обычных настроек, но сам выбор файла проще сделать до viper: имя контура нужно как часть имени файла, а не как значение настройки. Дальше — белый список: switch с тремя допустимыми значениями или карта валидных контуров. Без него опечатка APP_ENV=produciton превращается в SetConfigName("config.produciton"), ReadInConfig не находит файл, и сервис едет на дефолтах — тихо и по всем правилам библиотеки. Валидация контура — это валидация имени файла, и стоит она четыре строки.
Выбор окружения и чтение профиля
GOenv := flag.String("env", "", "dev, staging или prod")
flag.Parse()
if *env == "" {
*env = os.Getenv("APP_ENV")
}
if *env == "" {
*env = "dev" // провал по умолчанию — в самое безопасное место
}
switch *env {
case "dev", "staging", "prod":
default:
log.Fatalf("неизвестное окружение: %q", *env)
}
v := viper.New()
v.SetConfigName("config." + *env)
v.AddConfigPath("./configs")
if err := v.ReadInConfig(); err != nil {
log.Fatalf("профиль %s: %v", *env, err)
}
fmt.Println("окружение:", *env, "порт:", v.GetInt("server.port"))Цепочка выбора: флаг, затем APP_ENV, затем dev. Switch отбрасывает опечатки до того, как они превратятся в имя файла. SetConfigName с суффиксом ищет config.<env>.yaml по всем добавленным путям; ReadInConfig вернёт ошибку, если файла нет, — для обязательного профиля это Fatal, а не тихий проход дальше. Запустите локальным Go: go build, затем ./app --env staging и ./app без аргументов — в логе два разных файла.
База и переопределения: config.yaml плюс config.prod.yaml#
Профили по суффиксу удобны, когда контуры отличаются сильно. Если же девяносто процентов настроек общие, плодить пять почти одинаковых файлов — путь к рассинхрону: правку таймаута придётся повторять в каждом. Выход — два слоя файлов: общий config.yaml хранит базу, а config.<env>.yaml — только отличия контура. Читаются они в один реестр последовательно: сначала ReadInConfig базы, затем SetConfigName("config." + env) и MergeInConfig — она вливает карту профиля поверх уже прочитанной: скаляры заменяются, вложенные карты сливаются по ключам. Второй подряд ReadInConfig не годится: он заменяет содержимое целиком, и общие ключи базы пропадут. Отдельные реестры через viper.New() пригодятся, когда базу и оверрайд нужно видеть по отдельности — в тестах конфигурации или при сравнении контуров; там каждый файл читается в свой реестр, а сравнение значений делается обычным кодом.
Общая база и MergeInConfig
GOv := viper.New()
// база: всё общее между контурами
v.SetConfigName("config")
v.AddConfigPath("./configs")
if err := v.ReadInConfig(); err != nil {
log.Fatal("нет базового конфига: ", err)
}
// переопределения: только отличия контура
v.SetConfigName("config." + env)
if err := v.MergeInConfig(); err != nil {
log.Fatalf("нет профиля %s: %v", env, err)
}База читается первым вызовом, профиль вливается MergeInConfig поверх: retries из config.dev.yaml заменил базовое значение, а ключи, которых в профиле нет, остались из базы. Вложенные секции сливаются по ключам, а не заменяются целиком — именно поэтому общая база выживает.
MergeInConfig возвращает ConfigFileNotFoundError, если файла профиля нет. Для обязательного профиля это Fatal на старте; глотать ошибку нельзя — сервис поедет на одной базе, как будто контура не существует. Если же переопределения по задумке опциональны, проверяйте именно тип ошибки через errors.As и пропускайте только «файл не найден», а всё остальное — роняйте.
Третий слой: переменные окружения поверх файлов#
Файлы профиля задают базу, но деплой всегда знает лучше: порт из манифеста, адрес базы из секрета, уровень логов из переменной контейнера. Третий слой включается из урока про AutomaticEnv: SetEnvKeyReplacer, SetEnvPrefix и AutomaticEnv — и каждое чтение проверяет окружение. Лестница остаётся прежней: флаг выше env, env выше файла, файл выше дефолта — профиль перекрывается переменными так же, как и одиночный config.yaml. Практика деплоя, которую стоит скопировать: префикс по контуру. На staging SetEnvPrefix("STG") — процесс ищет только STG_SERVER_PORT и физически не прочитает APP_SECRET, даже если манифест собран криво и чужая переменная влетела в контейнер. Суффикс имени файла защищает от чтения чужого YAML, префикс переменных — от чтения чужих секретов; вместе они закрывают оба пути, которыми профили путаются местами.
Префикс по контуру и AutomaticEnv
GOvar prefixes = map[string]string{
"dev": "APP", "staging": "STG", "prod": "PRD",
}
v.SetEnvPrefix(prefixes[env])
v.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
v.AutomaticEnv()
// запуск staging: STG_SERVER_PORT=8081 ./app --env staging
fmt.Println(v.GetInt("server.port")) // 8081 — env перекрыл файлПрефикс выбирается из карты по контуру и действует на каждое чтение: на staging viper проверит STG_SERVER_PORT и не увидит APP_SERVER_PORT, даже если та задана. Replacer превращает server.port в SERVER_PORT — механика из урока про окружение. Строка запуска показывает третий слой в деле: переменная деплоя перекрыла файл профиля.
Окружение → файл → env-префикс#
| Окружение | Файл профиля | Префикс env |
|---|---|---|
| dev | config.dev.yaml | APP_ — локально можно и без префикса |
| staging | config.staging.yaml | STG_ — свой секрет-неймспейс контура |
| prod | config.prod.yaml | PRD_ — только боевые переменные |
| тесты CI | config.test.yaml | TEST_ — чтобы тесты не увидели деплой |
Что лежит в файлах, что в переменных#
- В файлах репозитория: порт, уровень логов, таймауты, адреса соседних сервисов — нечувствительные настройки с историей в git.
- В переменных окружения деплоя: пароли баз, ключи API, токены — секреты не попадают в коммиты, подробности в следующем уроке.
- В config.yaml — общая база; в config.<env>.yaml — только отличия: дифф между контурами виден с одного взгляда.
- Каждый контур получает свой префикс: процесс staging не прочитает переменную APP_*, даже если она влетела в контейнер.
- Флаг --env выбирает профиль локально, APP_ENV — запасной путь для контейнеров, куда флаг не прокинут.
- Строка env=staging file=config.staging.yaml в логе на старте — дешёвая страховка от перепутанных профилей.
Два способа разложить профили, и viper умеет оба. Суффиксы — config.dev.yaml в одной папке: весь конфиг на одном экране, diff между контурами и grep по ключам без прыжков по дереву. Папки — configs/dev/config.yaml с одинаковыми именами файлов: манифест или шаблон CI меняет только AddConfigPath("configs/" + env), а имя файла остаётся неизменным. Практическое правило: если конфиг чаще правят руками — суффиксы, если генерирует CI — папки.
Живое место: staging отправил письма клиентам#
История, ради которой стоит печатать окружение на старте. Команда завела переменную APP_ENV и выставила её в манифестах — но в манифесте staging строку забыли. Дефолт в коде был prod: рассуждали, что на локалке контур всё равно передают явно, а в деплое опаснее случайно упасть в dev с его заглушками. Бинарник на staging прочитал config.prod.yaml: боевой SMTP-хост, боевой адрес базы рассылок. Тестовый прогон уведомления ушёл по боевым спискам — клиентам. Разбор дал три правила, которые я теперь ставлю в каждый каркас. Первое: дефолт-окружение — dev, провал по умолчанию должен быть в самое безопасное место. Второе: в деплое выбор обязателен — пустая APP_ENV в непомеченном как локальный запуске должна ронять старт с понятной ошибкой, а не подставлять файл. Третье: печать env и имени файла на старте превращает расследование из вечера в тридцать секунд.
Проверка: пропавший манифест
В манифесте staging забыли задать APP_ENV. Код делает env := os.Getenv("APP_ENV"); if env == "" { env = "dev" }. Какой файл прочитает бинарник на staging?
Проверка: второй ReadInConfig
База прочитана через ReadInConfig. На том же реестре после SetConfigName("config.prod") вызван второй ReadInConfig. Что станет с общими настройками базы?
Что выведет программа?
Прочитайте код и выберите, что он выведет:
// configs/config.yaml: retries: 3
// configs/config.dev.yaml: retries: 5
v := viper.New()
v.SetConfigName("config")
v.AddConfigPath("./configs")
v.ReadInConfig()
v.SetConfigName("config.dev")
v.MergeInConfig()
fmt.Println(v.GetInt("retries"))Соберите выбор профиля
Допишите чтение переменной, создание отдельного реестра и имя файла с суффиксом окружения.
Соберите каркас профиля
Расставьте строки: выбор окружения, отдельный реестр, имя файла, путь поиска, чтение конфига и печать порта.
Строки
Ваша программа
Нажимайте на строки слева, чтобы собрать программу
Карточки: профили окружений
Спор: конфиг на файловой системе или всё в переменных деплоя#
Две школы конфигурации спорят об одном и том же слое. Файловая школа хранит настройки в YAML рядом с кодом: история в git, ревью на правки, дифф между релизами виден без инструментов. Плата — файловая система: в Kubernetes файлы монтировать неудобно, а соблазн положить в тот же файл секрет огромен. Школа переменных кладёт в env вообще всё: манифест деплоя становится единственным источником, маунтов нет, окружение неизменяемо. Плата — слепота инструментов: AllKeys не видит AutomaticEnv, история значений живёт только в истории манифестов, а вложенные структуры превращаются в простыни вроде DATABASE_HOST без иерархии. Мой критерий: если конфиг правят люди и контуров больше одного — файлы для базы плюс env для перекрытий и секретов, как в этом уроке. Всё в переменных оправдано, когда платформа деплоя сама валидирует схему и держит секреты отдельно, а сервису остаётся читать плоские значения с проверкой IsSet на старте.
Что дальше#
Каркас профилей собран: флаг и APP_ENV выбирают контур, база с переопределениями живёт в двух файлах, префикс отделяет секреты одного окружения от другого. Осталась незакрытой главная дыра — секреты: что делать с паролем базы, если в файл его класть нельзя, а в переменные он попадает через деплой. Следующий урок разбирает именно это: почему пароли не живут в YAML, что делает история git с однажды закоммиченным ключом и как устроить переменные деплоя, чтобы секрет доходил до реестра, не засвечиваясь в логах. Общий маршрут по темам — на странице курса viper, а каркас профиля из урока стоит прогнать у себя: два контура, два запуска, два разных файла в логе.
Песочница урока
Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.
Запустите код, чтобы увидеть вывод.
Коротко: что запомнить
- Один бинарник на все контуры: окружение выбирается флагом --env с запасной APP_ENV, белым списком и печатью выбора на старте.
- SetConfigName("config." + env) с AddConfigPath читает профиль по суффиксу; папки configs/dev/ — альтернатива с одинаковыми именами файлов.
- Второй ReadInConfig затирает базу целиком; чтобы наложить отличия контура поверх общей базы, нужна MergeInConfig.
- Переменные окружения — третий слой поверх файлов: флаг выше env, env выше файла, файл выше дефолтов.
- Префикс по контуру через SetEnvPrefix отделяет секреты: процесс staging не прочитает чужие APP_*-переменные.
- В файлах живут нечувствительные настройки с историей в git; секреты — только в окружении деплоя.
- Пустая APP_ENV — не «не задано», а ваш дефолт: провал должен быть в dev, а выбор окружения обязан быть явным в деплое.
Домашнее задание
Соберите каркас из урока: configs/config.yaml с портом и уровнем логов, config.dev.yaml и config.prod.yaml с отличиями. Выбор окружения — флаг с запасным APP_ENV и белым списком. Запустите локальным Go оба контура и сравните вывод, затем уберите APP_ENV из запуска и посмотрите в логе, какой файл прочитался на самом деле.
Личные заметкиПусто
Вопросы и ответы по уроку
Нужен ли отдельный бинарник или образ на каждое окружение?
Нет — в этом смысл профилей. Один бинарник читает config.dev.yaml или config.prod.yaml по флагу или APP_ENV. Собирать отдельный образ под контур — путь к рассинхрону: рано или поздно на prod уедет сборка для staging.
Что делать с секретами в профилях?
Не класть их в файлы вовсе: в config.prod.yaml живут только нечувствительные отличия вроде порта и уровня логов. Пароли и ключи приходят переменными окружения деплоя с префиксом контура — полный разбор в следующем уроке про секреты.
MergeInConfig заменяет значения или складывает?
Заменяет скаляры и глубоко сливает карты: ключ из профиля затирает базу, вложенные секции объединяются по ключам, а списки заменяются целиком. Проверьте на своём YAML: две минуты с базой и профилем дают точную картину слияния.
Термины урока
Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.
Изучайте дальше
- Секреты: почему пароли не живут в YAML
- Пакет config: оборачиваем viper в свой API
- Валидация конфигурации до запуска
- Тестирование конфигурации: New и Reset
- RegisterAlias: синонимы ключей и миграция схемы
- Sub(): разбираем конфиг на поддеревья
- WriteConfig и SafeWriteConfig: сохраняем состояние
- WatchConfig и OnConfigChange: живой конфиг
- Все курсы по Go
Следующая ступень учебного пути
Chi: роутер для Go HTTP-сервисов
Интерактивный курс Chi — роутера Go на net/http: маршруты, middleware, группы, контекст, JSON API, таймауты, graceful shutdown. 25 уроков с примерами и практикой.
- Строить маршруты chi: методы, параметры пути, wildcards
- Писать middleware и понимать порядок их выполнения
- Группировать маршруты: Route, Group и подроутеры
- Прокидывать данные в хендлеры через context.Context