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

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

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

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

Удалённые хранилища: etcd и Consul

Удалённый конфиг go нужен в тот день, когда настроек становится больше, чем инстансов: viper умеет читать etcd и Consul через remote-провайдер — AddRemoteProvider, ReadRemoteConfig и WatchRemoteConfig, и это выход, когда файл из репозитория обслуживает уже пятый сервис и каждая правка таймаута превращается в релиз.

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

Пятый инстанс сервиса залили в кластер, и выяснилось, что он должен брать те же лимиты и адреса, что и первые четыре, — а 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

GO
import _ "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 недоступен

GO
v := 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, ни файл. Лог с ошибкой обязателен: строка «работаю на дефолтах» — не шум, а то, по чему ночью находят причину странных значений в ответах сервиса.

Секреты в 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» тоже держите в голове: поведение вотча на остальных провайдерах проверяйте сами.

Цикл вотча в горутине

GO
go 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, вместе с кодомзависит от хранилища, чаще нет — логируйте сами
Когда хватаетодин сервис, настройки меняются с релизоммного инстансов и сервисов, общие значения

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

v := viper.New() v.("consul", "localhost:8500", "MY_CONSUL_KEY") v.("json") if err := v.(); err != nil { log.Fatalf("remote: %v", err) } fmt.Println(v.Get("port")) // 8080

Соберите секцию подключения

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

Строки

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

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

Карточки: удалённые хранилища

Термин

AddRemoteProvider

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

Объявление удалённого источника: тип хранилища (etcd, etcd3, consul, firestore, nats), адрес endpoint и путь к ключу. Пустые provider или endpoint молча не добавляются — ошибку вернёт уже ReadRemoteConfig.

1 / 6

Спор: 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, как правка значения доезжает до сервиса без рестарта.

Личные заметкиПусто
Хранятся только в вашем браузере (Viper · Удалённые хранилища: etcd и Consul)

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

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 появился в списке провайдеров недавно. Для первого опыта разница невелика: контракт из трёх вызовов у всех одинаков.

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

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

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

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

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

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

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