Содержание урока · 10 разделов
- Remote-провайдеры viper: etcd, Consul и Firestore
- ReadRemoteConfig: что возвращает и когда молчит
- Секреты в KV: AddSecureRemoteProvider и GPG-кейринг
- WatchRemoteConfig: поллинг вместо fsnotify
- Файл против KV-хранилища
- Признаки, что файлу пора в KV
- Спор: KV-конфиг против env из оркестратора
- Песочница урока
- Коротко: что запомнить
- Вопросы и ответы
Пятый инстанс сервиса залили в кластер, и выяснилось, что он должен брать те же лимиты и адреса, что и первые четыре, — а config.yaml лежит в репозитории и разъезжается по инстансам вместе с образом. Правка «поменять таймаут» превращается в релиз: коммит, пайплайн, выкат в пять мест, и где-то обязательно остаётся старый образ со старым значением. Удалённый конфиг снимает это структурно: viper умеет читать настройки из etcd и Consul — KV-хранилищ, которые придуманы ровно для одного набора значений на много потребителей. Правка делается один раз в хранилище, инстансы видят её на следующем чтении или вотче, репозиторий остаётся для кода. В этом уроке — remote-провайдеры viper: подключение через AddRemoteProvider и ReadRemoteConfig, секреты в KV как продолжение разговора из урока про секреты, вотч с честной оговоркой про его механику и спор о том, что выбрать: KV или env из оркестратора.
Remote-провайдеры viper: etcd, Consul и Firestore#
Remote-провайдер — слой источников, который viper проверяет после файлов и до дефолтов: в лестнице из урока про приоритеты источников он значится как external KV stores. Список провайдеров зашит в переменную SupportedRemoteProviders и по исходникам свежих версий выглядит так: etcd (старый v2-клиент), etcd3, consul, firestore и nats — последний добавили недавно, поэтому в старой документации его нет. Подключение начинается не с конфига, а с импорта: remote-поддержка вынесена в отдельный пакет, и без blank-импорта пакета viper/remote любые вызовы вернут ошибку с подсказкой «enable the remote features» — viper намеренно не тянет клиенты etcd в проекты, которые им не пользуются. Дальше три вызова: AddRemoteProvider объявляет источник, SetConfigType говорит, в каком формате лежат байты, ReadRemoteConfig тянет их и разбирает в реестр. Провайдеров можно добавить несколько — viper опрашивает их в порядке добавления.
Три вызова до значения из etcd
GOimport _ "github.com/spf13/viper/remote" // включает remote-поддержку
v := viper.New()
v.AddRemoteProvider("etcd", "http://127.0.0.1:4001", "/config/app.json")
v.SetConfigType("json") // у байтов из KV нет расширения — формат задаём сами
if err := v.ReadRemoteConfig(); err != nil {
log.Fatalf("remote config: %v", err)
}
fmt.Println(v.GetString("database.host")) // db.internal из etcdПорядок жёсткий: сначала blank-импорт пакета remote — он регистрирует драйверы хранилищ, затем AddRemoteProvider с типом хранилища, адресом и путём к ключу. SetConfigType обязателен: в отличие от файла, у удалённых байтов нет расширения, и viper не догадается, что перед ним json. ReadRemoteConfig возвращает ошибку — глотать её нельзя, ниже покажу почему. Воспроизведите пример локальным Go: etcd поднимается докером одной командой, и весь каркас оживает за пару минут.
ReadRemoteConfig: что возвращает и когда молчит#
ReadRemoteConfig идёт по списку добавленных провайдеров в порядке добавления и берёт первого, у кого путь ответил: у etcd и etcd3 адрес — это http://ip:port, у Consul — ip:port без схемы, у Firestore — идентификатор проекта и путь collection/document. Несколько хостов перечисляются через точку с запятой — http://127.0.0.1:4001;http://127.0.0.1:4002, это задел на отказоустойчивость. Тихих места здесь два. Первое: AddRemoteProvider с пустым provider или endpoint ошибку не вернёт — провайдер просто не добавится, а ReadRemoteConfig потом отчитается уже невнятным «No Remote Providers», и вы будете искать сетевые проблемы там, где опечатка в адресе. Второе: если ни один провайдер не ответил, вернётся ошибка «No Files Found» — по формулировке не очевидно, что речь про KV, а не про файл. Поэтому адреса и пути к ключам я выношу в константы рядом с объявлением провайдера: они опечатываются чаще, чем кажется.
Фолбэк на файл, когда KV недоступен
GOv := viper.New()
// дефолты объявляем до чтения: пусть сервис стартует даже без KV
v.SetDefault("database.host", "localhost")
v.SetDefault("database.port", 5432)
v.AddRemoteProvider("etcd", "http://etcd.internal:2379", "/config/app.json")
v.SetConfigType("json")
if err := v.ReadRemoteConfig(); err != nil {
log.Printf("KV недоступен, работаю на дефолтах: %v", err)
// тот же реестр умеет читать файл — это фолбэк, а не второй источник
v.SetConfigName("config")
v.AddConfigPath("./configs")
if err := v.ReadInConfig(); err != nil {
log.Fatalf("ни KV, ни файл: %v", err)
}
}Фолбэк держится на том, что реестр один: ReadRemoteConfig и ReadInConfig наполняют его одинаково, и лестница приоритетов из урока про источники работает как обычно. Дефолты объявлены до чтения — они последние на лестнице и не перекроют ни KV, ни файл. Лог с ошибкой обязателен: строка «работаю на дефолтах» — не шум, а то, по чему ночью находят причину странных значений в ответах сервиса.
Живое место: локально всё работает, а в проде сетевая политика не пускает контейнер до etcd — порт 2379 закрыт, о чём никто не вспомнил. ReadRemoteConfig возвращает ошибку, её записывают в лог и идут дальше, сервис поднимается на дефолтах: хост базы localhost, лимиты зашитые. Pod зелёный, трафик идёт, ответы неверные — и ищут причину в коде, а не в сети. Лечение: для обязательных настроек log.Fatal сразу при ошибке чтения KV, для опциональных — явный флаг degraded и строка уровня error, а не info.
Секреты в KV: AddSecureRemoteProvider и GPG-кейринг#
KV-хранилище — естественное место для секретов из урока про секреты конфигурации: значение задаёт оператор с ACL, до кода оно доезжает через память процесса, в репозитории не появляется. Для шифрованного содержимого у viper есть AddSecureRemoteProvider — тот же контракт, что у AddRemoteProvider, плюс четвёртый аргумент: путь к GPG-секретному кейрингу, например /etc/secrets/mykeyring.gpg. Под капотом работает пакет crypt: положить значение можно его утилитой командой crypt set -plaintext /config/app.json config.json, а viper при чтении расшифрует сам, если кейринг правильный. Теперь честная оговорка, которую я обещал: README не документирует ручную регистрацию jsoncrypt-алгоритмов — форматы jsoncrypt и yamlcrypt живут внутри crypt и срабатывают автоматически, а сам пакет в истории переезжал с spf13/crypt на sagikazarmark/crypt, так что API крипто-слоя между версиями гулял. Всё, что сложнее штатного сценария «зашифровал crypt-ом, прочитал с кейрингом», сверяйте с документацией своей версии viper — а не со статьями, включая эту.
WatchRemoteConfig: поллинг вместо fsnotify#
Локальный вотч из урока про WatchConfig держится на fsnotify: файловая система сама сообщает об изменении. У удалённого хранилища такого механизма в viper нет — есть WatchRemoteConfig, который при каждом вызове просто заново тянет конфиг из KV. Поэтому документация показывает цикл: горутина, sleep на несколько секунд, WatchRemoteConfig, при ошибке лог и продолжение, затем Unmarshal свежих значений. В исходниках рядом живёт WatchRemoteConfigOnChannel, но в README он не показан, а колбэка вроде OnRemoteConfigChange, который встречается в обсуждениях, в API актуальных версий нет — я проверял по v1.20 и v1.21. Вывод практический: механизм удалённого вотча исторически нестабилен, и надёжнее писать собственный цикл поллинга с той частотой, которая вам нужна, — заодно вы контролируете нагрузку на хранилище. Коммент в документации «only tested with Etcd support» тоже держите в голове: поведение вотча на остальных провайдерах проверяйте сами.
Цикл вотча в горутине
GOgo func() {
for {
time.Sleep(5 * time.Second)
if err := v.WatchRemoteConfig(); err != nil {
log.Printf("remote watch: %v", err)
continue // хранилище моргнуло — попробуем на следующем тике
}
var cfg Config
if err := v.Unmarshal(&cfg); err != nil {
log.Printf("unmarshal: %v", err)
continue
}
atomic.StoreInt32(&ready, 1) // потребители видят свежий срез целиком
}
}()Цикл повторяет официальный пример: пауза, WatchRemoteConfig, обработка ошибки, Unmarshal. Значение собирается в свежую структуру, а не поверх старой — чтобы половина полей не осталась от прошлого раза, если схема в хранилище изменилась. Ошибка вотча сервис не роняет: KV мог моргнуть, и следующий тик исправит ситуацию. Проверьте локальным Go: поднимите etcd, поменяйте ключ обычным put — через пять секунд значение в структуре обновится без рестарта.
Файл против KV-хранилища#
| Аспект | Файл в репозитории | KV: etcd или Consul |
|---|---|---|
| Кого обновляет правка | деплой: образ пересобирается в каждом инстансе | одно значение в хранилище, видят все инстансы |
| Изменение без релиза | часы: коммит, пайплайн, выкатка | минуты: put в хранилище и вотч у сервиса |
| Откат | git revert и новый релиз | предыдущее значение тем же put |
| Секреты | нельзя: git-история и клоны | можно: ACL и шифрование через кейринг |
| История значений | в git, вместе с кодом | зависит от хранилища, чаще нет — логируйте сами |
| Когда хватает | один сервис, настройки меняются с релизом | много инстансов и сервисов, общие значения |
В SupportedRemoteProviders сидят и etcd, и etcd3, и это не опечатка: под первым скрывается старый v2-клиент, под вторым — clientv3. Указанный тип должен совпадать с тем, что реально стоит в вашем etcd и с зависимостью, которую вы готовы тянуть. Ошибка в типе провайдера проявится только на чтении, странной ошибкой подключения. Адреса нескольких нод перечисляются через точку с запятой прямо в строке endpoint.
Признаки, что файлу пора в KV#
- Настроек становится больше, чем сервисов, которые их читают: одно значение на десяток инстансов.
- Правка конфига требует релиза, хотя код не менялся, — и релизы ради таймаутов всем надоели.
- Инстансы расходятся: в одном образе свежий таймаут, в другом — собранный на неделе раньше.
- Секретам нужны ACL и ротация, а env деплоя уже не справляется — продолжение темы из урока про секреты.
- Появился второй язык или стек, которому нужны те же значения, — общий KV дешевле синхронизации файлов.
- Изменения должны доезжать без рестарта: вотч раздаёт новые значения живым процессам.
Проверка: SetConfigType
Почему для удалённого провайдера обязателен вызов SetConfigType?
Проверка: ошибка «enable the remote features»
Код remote-чтения написан верно, но ReadRemoteConfig стабильно возвращает ошибку «enable the remote features». Что забыли?
Что выведет программа?
Прочитайте код и выберите, что он выведет:
// etcd на 127.0.0.1:4001 не запущен
v := viper.New()
v.AddRemoteProvider("etcd", "http://127.0.0.1:4001", "/config/app.json")
v.SetConfigType("json")
v.SetDefault("server.port", 8080)
if err := v.ReadRemoteConfig(); err != nil {
log.Println("remote:", err)
}
fmt.Println(v.GetInt("server.port"))Соберите подключение Consul
Допишите подключение Consul: добавление провайдера, явный тип содержимого и само чтение из хранилища.
Соберите секцию подключения
Расставьте строки по порядку: реестр, провайдер, тип содержимого, дефолт, чтение и печать значения.
Строки
Ваша программа
Нажимайте на строки слева, чтобы собрать программу
Карточки: удалённые хранилища
Спор: KV-конфиг против env из оркестратора#
Моё мнение, с которым коллеги в k8s-проектах спорят охотно: оркестратор уже сделал env-конфиг удобнее, чем когда-либо, и это отъело у KV большую часть сценариев. ConfigMap монтируется или подставляется в переменные, значения версионируются вместе с манифестами, правки проходят через git-review, и ещё одной системы в эксплуатации не появляется. Если у вас один-два сервиса в k8s и настройки меняются вместе с релизом — env честно закрывает потребность, и KV будет решением в поисках проблемы. Там же, где KV всё ещё выигрывает: много сервисов разного стека, гибридная инфраструктура без единого оркестратора, значения, общие для десятков потребителей, и правки, которые обязаны доезжать без выкатки. Граница, которую я провожу: появился второй язык или первый инстанс, которому нужен тот же конфиг, — начинайте смотреть в KV; пока конфиг приватен одному сервису — env и никаких церемоний. И помните про bootstrap: адрес и секрет доступа к самому хранилищу всё равно живут в env, от этой пары KV не уводит.
Удалённый конфиг собран: провайдер объявлен, тип задан, ошибка чтения обработана, вотч крутится в горутине, секреты едут через кейринг. Осталось не забыть про дежурного, который будет это эксплуатировать: положите рядом описание того, что происходит при недоступности KV — фолбэк или Fatal, — и закрепите его тестом. Дальше по курсу разговор пойдёт о том, когда viper вообще не нужен: сравним env-конфиг и библиотеки вроде koanf с честными критериями выбора. Общий маршрут по темам — на странице курса viper. И задание на сейчас: посчитайте, сколько инстансов читают ваш config.yaml, — если больше одного, пример из урока уже применим.
Песочница урока
Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.
Запустите код, чтобы увидеть вывод.
Коротко: что запомнить
- Удалённый конфиг go закрывает случай «один набор значений на много инстансов»: правка делается в KV один раз вместо релиза.
- Remote-поддержка включается blank-импортом viper/remote; без него ReadRemoteConfig отвечает ошибкой с точной подсказкой.
- Контракт из трёх вызовов: AddRemoteProvider, обязательный SetConfigType и ReadRemoteConfig; ошибка чтения — строка в логе или Fatal, но не тишина.
- Секреты едут через AddSecureRemoteProvider с GPG-кейрингом, расшифровку делает пакет crypt; ручная регистрация jsoncrypt-кодеков документацией не подтверждена.
- WatchRemoteConfig — поллинг: горутина с паузой сама тянет изменения; колбэков в документации нет, WatchRemoteConfigOnChannel в README не показан.
- Файл дешевле для одного сервиса, KV — для многих потребителей, правок без деплоя и общих секретов с ACL.
- Env из оркестратора забрал большинство сценариев KV: KV оправдан, когда конфиг общий для многих сервисов и стеков.
Домашнее задание
Поднимите etcd докером и подключите к каркасу из урока про пакет config remote-провайдер: AddRemoteProvider, SetConfigType, ReadRemoteConfig с проверкой ошибки и фолбэком на файл. Затем вынесите database.password в AddSecureRemoteProvider с кейрингом и проверьте циклом WatchRemoteConfig, как правка значения доезжает до сервиса без рестарта.
Личные заметкиПусто
Вопросы и ответы по уроку
WatchRemoteConfig — это push или pull, насколько он надёжен?
Строго pull: вызов заново тянет конфиг из хранилища, событий как у fsnotify нет. Официальный пример — горутина с паузой и повтором; ошибки не роняют сервис. Колбэков вроде OnRemoteConfigChange в API актуальных версий нет, а WatchRemoteConfigOnChannel в README не показан, поэтому цикл поллинга — самый надёжный путь.
Нужно ли шифровать значения в Consul или хватит ACL?
ACL решает, кто читает и пишет, но не защищает значение на диске хранилища и в его логах. AddSecureRemoteProvider с GPG-кейрингом шифрует само содержимое: без ключа расшифровки дамп KV бесполезен. Для секретов базы я совмещаю оба уровня: ACL на пути и шифрование содержимого.
С чего начинать: etcd, Consul, Firestore или NATS?
Начинайте с etcd или Consul: их примеры описаны дольше всех, и они привычнее в self-hosted эксплуатации. Firestore — вариант, если вы уже в Google Cloud, NATS появился в списке провайдеров недавно. Для первого опыта разница невелика: контракт из трёх вызовов у всех одинаков.
Термины урока
Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.
Изучайте дальше
- Диагностика: почему viper не видит ключ
- Viper и альтернативы: env, koanf и stdlib
- Паттерны конфигурации сервисов
- Итоговый проект: конфигурируемый сервис
- Тестирование конфигурации: New и Reset
- Валидация конфигурации до запуска
- Пакет config: оборачиваем viper в свой API
- Секреты: почему пароли не живут в YAML
- Все курсы по Go
Следующая ступень учебного пути
Chi: роутер для Go HTTP-сервисов
Интерактивный курс Chi — роутера Go на net/http: маршруты, middleware, группы, контекст, JSON API, таймауты, graceful shutdown. 25 уроков с примерами и практикой.
- Строить маршруты chi: методы, параметры пути, wildcards
- Писать middleware и понимать порядок их выполнения
- Группировать маршруты: Route, Group и подроутеры
- Прокидывать данные в хендлеры через context.Context