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

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

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

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

Профили dev, staging и prod

Профили окружений Go собирают dev, staging и prod в один бинарник: viper читает config.dev.yaml или config.prod.yaml по флагу --env и переменной APP_ENV, файлы задают базу, окружение деплоя перекрывает её поверх. Разберём суффиксы и папки, MergeInConfig для переопределений, префиксы env и место секретов.

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

Сервис собирается один раз, а живёт в трёх местах: ноутбук разработчика, 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 не находит файл, и сервис едет на дефолтах — тихо и по всем правилам библиотеки. Валидация контура — это валидация имени файла, и стоит она четыре строки.

Выбор окружения и чтение профиля

GO
env := 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

GO
v := 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 заменил базовое значение, а ключи, которых в профиле нет, остались из базы. Вложенные секции сливаются по ключам, а не заменяются целиком — именно поэтому общая база выживает.

Третий слой: переменные окружения поверх файлов#

Файлы профиля задают базу, но деплой всегда знает лучше: порт из манифеста, адрес базы из секрета, уровень логов из переменной контейнера. Третий слой включается из урока про AutomaticEnv: SetEnvKeyReplacer, SetEnvPrefix и AutomaticEnv — и каждое чтение проверяет окружение. Лестница остаётся прежней: флаг выше env, env выше файла, файл выше дефолта — профиль перекрывается переменными так же, как и одиночный config.yaml. Практика деплоя, которую стоит скопировать: префикс по контуру. На staging SetEnvPrefix("STG") — процесс ищет только STG_SERVER_PORT и физически не прочитает APP_SECRET, даже если манифест собран криво и чужая переменная влетела в контейнер. Суффикс имени файла защищает от чтения чужого YAML, префикс переменных — от чтения чужих секретов; вместе они закрывают оба пути, которыми профили путаются местами.

Префикс по контуру и AutomaticEnv

GO
var 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
devconfig.dev.yamlAPP_ — локально можно и без префикса
stagingconfig.staging.yamlSTG_ — свой секрет-неймспейс контура
prodconfig.prod.yamlPRD_ — только боевые переменные
тесты CIconfig.test.yamlTEST_ — чтобы тесты не увидели деплой

Что лежит в файлах, что в переменных#

  • В файлах репозитория: порт, уровень логов, таймауты, адреса соседних сервисов — нечувствительные настройки с историей в git.
  • В переменных окружения деплоя: пароли баз, ключи API, токены — секреты не попадают в коммиты, подробности в следующем уроке.
  • В config.yaml — общая база; в config.<env>.yaml — только отличия: дифф между контурами виден с одного взгляда.
  • Каждый контур получает свой префикс: процесс staging не прочитает переменную APP_*, даже если она влетела в контейнер.
  • Флаг --env выбирает профиль локально, APP_ENV — запасной путь для контейнеров, куда флаг не прокинут.
  • Строка env=staging file=config.staging.yaml в логе на старте — дешёвая страховка от перепутанных профилей.

Живое место: 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"))

Соберите выбор профиля

Допишите чтение переменной, создание отдельного реестра и имя файла с суффиксом окружения.

env := os.("APP_ENV") if env == "" { env = "dev" } v := viper.() v.("config." + env) v.AddConfigPath("./configs") if err := v.ReadInConfig(); err != nil { log.Fatal(err) }

Соберите каркас профиля

Расставьте строки: выбор окружения, отдельный реестр, имя файла, путь поиска, чтение конфига и печать порта.

Строки

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

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

Карточки: профили окружений

Термин

SetConfigName с суффиксом

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

v.SetConfigName("config." + env) — viper ищет config.dev.yaml или config.prod.yaml по всем путям из AddConfigPath. Имя контура приходит из флага или APP_ENV.

1 / 6

Спор: конфиг на файловой системе или всё в переменных деплоя#

Две школы конфигурации спорят об одном и том же слое. Файловая школа хранит настройки в 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 из запуска и посмотрите в логе, какой файл прочитался на самом деле.

Личные заметкиПусто
Хранятся только в вашем браузере (Viper · Профили dev, staging и prod)

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

Нужен ли отдельный бинарник или образ на каждое окружение?

Нет — в этом смысл профилей. Один бинарник читает config.dev.yaml или config.prod.yaml по флагу или APP_ENV. Собирать отдельный образ под контур — путь к рассинхрону: рано или поздно на prod уедет сборка для staging.

Что делать с секретами в профилях?

Не класть их в файлы вовсе: в config.prod.yaml живут только нечувствительные отличия вроде порта и уровня логов. Пароли и ключи приходят переменными окружения деплоя с префиксом контура — полный разбор в следующем уроке про секреты.

MergeInConfig заменяет значения или складывает?

Заменяет скаляры и глубоко сливает карты: ключ из профиля затирает базу, вложенные секции объединяются по ключам, а списки заменяются целиком. Проверьте на своём YAML: две минуты с базой и профилем дают точную картину слияния.

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

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

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

Chi25 уроков~9 чпрактика25 уроков уже доступны

Chi: роутер для Go HTTP-сервисов

Интерактивный курс Chi — роутера Go на net/http: маршруты, middleware, группы, контекст, JSON API, таймауты, graceful shutdown. 25 уроков с примерами и практикой.

  • Строить маршруты chi: методы, параметры пути, wildcards
  • Писать middleware и понимать порядок их выполнения
  • Группировать маршруты: Route, Group и подроутеры
  • Прокидывать данные в хендлеры через context.Context