golang

Про конфигурацию go приложений и при чём тут vault

  • суббота, 10 мая 2025 г. в 00:00:12
https://habr.com/ru/articles/907966/

Когда-то я начинал свой путь как node.js разработчик и столкнулся с необходимостью конфигурировать приложение (кто бы мог подумать). Из простых решений, которые сразу приходят на ум, можно выделить:

  • файлы (json, .env, toml, yaml, xml, ini и прочие)

  • переменные окружения

  • аргументы процесса

В моём понимании это не совсем "конфигурация", а скорее способ передачи в неё данных. Так вот чтобы это всё заставить представлять из себя полноценную конфигурацию, с которой удобно работать, понадобилось поискать готовое решение. Среди прочих для себя выделил node-config. Помимо всего вышесказанного данная утилита вводит понятие иерархичности источников. Эта интересная особенность в купе с простым api и хорошей документацией перевесила в свою сторону.

Спустя время я начал разрабатывать на python. К сожалению там не нашёл подобной утилиты, но так как привык к подобной гибкости то решил написать простенький аналог (под капотом использовался ChainMap). И меня вполне устроило решение.

Время идёт дальше, а я всё больше разрабатываю на go. И теперь подошел его черёд. Что могу выделить по текущим решениям в конфигурации среди go приложений? Ну во первых есть хорошие библиотеки, которые справляются со своими обязанностями. Например config, или goconfig. Есть совсем мастодонтские библиотеки вроде viper - здесь есть всё, и этого всего так много что лично мне кажется избыточным. Есть даже возможность настроить remote provider.

Стоит оговориться что всё таки есть библиотеки на go, которые обеспечивают иерархичность. Например go-ucfg или gonfig. Но честно говоря они не поддерживают многие фишки, имеют весьма многословный api во всех случаях и выглядят скорее как конструктор лего. Почему бы не использовать определенное соглашение по именованию файлов для иерархии чем например выстраивать его самому через вызовы? Особенно учитывая что уже всё продумано на все случаи. В node-config достаточно взглянуть просто на папку с файлами, а не изучать то как она будет сконфигурирована программно. Да и в целом зачем такое развитое и многословное api если по факту всё что нужно сделать это просто прочитать значение.

Очень интересное совпадение, но буквально на днях вышла схожая статья про конфигуратор zerocfg - https://habr.com/en/articles/906636/. Видимо не у одного меня возникают трудности и вопросы. По мне так хорошее решение, чем-то схожее с предыдущими, но с полностью перенятым дизайном api с библиотеки flag, плюс добавлены проверки при работе с конфигурацией, но есть нюансы. И давайте я кратко расскажу про них со своей точки зрения, т.к. эти нюансы частично разделяют и выше упомянутые библиотеки:

  • Есть различные источники конфигурации с программной приоритезацией, что заставляет заглядывать в код, т.к. от него будет зависеть дальнейшая логика работы. Если поменяется код то вся конфигурация может разъехаться. Это как раз и есть та точка которая будет источником неожиданных сюрпризов в будущем. А хотелось бы иметь возможность просто перенести папку с файлами конфигурации, чтобы она везде работала одинаково вне зависимости от управляющего кода. Логика приоритезации просто может быть заранее объявлена в самой библиотеке и понятна без кода (лучший код тот, которого нет).

  • Мне понравилась идея некоторого статического анализа конфигурации в поисках ошибок. От себя лично хочу добавить что некоторые ошибки вроде опечаток можно легко закрыть использованием плагинов IDE для проверки орфографической корректности (например Code Spell Checker, которым пользуюсь). Это конечно не поможет если забыть поменять название поля в одном из файлов при этом меняя его в другом. В этом случае желательно проверять получаемые значения, хотя статический анализ очевидно может подстраховать.

  • Перенятый дизайн api с библиотеки flag хорошее решение и в целом согласен что однажды созданная конфигурация не должна меняться в рамках существования процесса (иначе зачем вообще все эти файлы). Однако может возникнуть потребность в пересоздании инстанса, создании нескольких инстансов или его передаче через DI. Интересно что для тестов есть возможность создавать инстансы конфигурации, однако это не попало в публичное api. Развивая идею дальше можно прийти к выводу что разработать какой-то кастомный парсер (возможность расширения библиотеки) завязанный на динамическом подхвате данных при текущем api потребует перезапуска процесса, что вряд-ди будет удобно например для запущенного сервера. С случае с flag всё очевидно - однажды переданные параметры процесса не могут поменяться пока процесс не будет пересоздан. Отсюда его статический дизайн со всеми вытекающими удобствами.

В целом хочу сказать что мне понравилось решение zerocfg. И мне кажется оно подойдёт для cli приложений в первую очередь. Советую обратить на него внимание как на альтернативу. А мы идём дальше.

Среди всего этого разнообразия я снова решил обратиться к простоте, заложенной изначально в концепцию node-config. И в этот раз решил написать полноценную библиотеку на go, перенося его логику. Единственное чего не хватало лично мне - интеграции с vault.

Вольт

Почему именно vault? Наверное на данный момент это одно из популярнейших решений для хранения секретов, по крайней мере с которым мне пришлось работать. Отличная документация, куча интеграций и в целом всё что только может и не может понадобиться. Фактически при помощи одного только vault можно загружать данные хоть с postgresql, при этом вообще не меняя ни конфигурацию ни код. Логически и чисто практически - почему он не может быть ещё одним источником данных для конфигурации наряду с переменными окружения? В общем не мудрствуя лукаво просто решил добавить его поддержку в библиотеку. Да конечно, можно просто писать самому эту прослойку, или загружать значения заранее, до запуска приложения и передавать данные через переменные окружения, аргументы или даже файлы. В таком случае придется написать дополнительный пласт логики, который надо куда-то выделить и протестировать. Но собственно зачем это делать, если сама утилита уже это умеет.

Вместе с поддержкой vault у меня под рукой уже полноценный инструмент для конфигурации. Он знает про иерархию источников данных. У него донельзя простой api и возможность встроить конфигурацию непосредственно в приложение. И вот собственно сама библиотека - goconfig.

Концепция

Давайте разберём простенький пример чтобы разобраться в чём же смысл этой конкретно иерархической конфигурации. В данном примере я буду использовать файлы toml просто потому что мне нравится этот синтаксис, но можно использовать также yaml или json.

Представим себе что нужна конфигурация базы данных postgresql под несколько сред - dev, stage и production. Возьмём за значения по-умолчанию значения dev среды и будем их переопределять для других сред. Все файлы закидываем в папку config (если другая папка то надо указать путь до неё):

default.toml:

[postgresql]
domain = "localhost"
port = 5432
username = "admin"
password = "qwerty123456"
database = "db"

stage.toml:

[postgresql]
domain = "stage.postgres.company.ru"
username = "stage_admin"
database = "stage_db"

production.toml:

[postgresql]
domain = "postgres.company.ru"
username = "prod_admin"
database = "prod_db"

Так как default файл имеет наименьший приоритет, то остальные значения ложатся поверх них, если конечно таковые есть. Например порт всегда будет 5432. Далее добавляем environment переменные, значения которых не должны находиться в кодовой базе (для stage/production окружений):

env.toml:

[postgresql]
username = "POSTGRESQL_USERNAME"
password = "POSTGRESQL_PASSWORD"

В данном примере для поля postgresql.username будет браться значение из переменной окружения POSTGRESQL_USERNAME, если она существует. Если её нет то в зависимости от окружения "prod_admin"/"stage_admin" или "admin".

На этом всё с конфигурацией. Глядя на эти файлы и зная их приоритет уже понятно что будет подгружаться. Никаких дальнейших манипуляций в программном коде с переустановкой данных или изменением приоритета нет. Всё что мы делаем это просто создаём инстанс конфигурации и читаем данные.

Теперь при создании конфига нужно просто указать какая это среда. Это можно сделать через переменную окружения GO_DEPLOYMENT или передав параметр. Например так:

cfg, err := goconfig.New(ctx, goconfig.Options{
  Deployment: "production",
})

При таком параметре будут загружаться файлы env.toml, production.toml и default.toml. Затем просто используем сами значения. Эта часть остаётся неизменной для всех сред:

postgresqlConnStr := fmt.Sprintf("postgres://%s:%s@%s:%d/%s",
  cfg.MustGet(ctx, "postgresql.username"),
  cfg.MustGet(ctx, "postgresql.password"),
  cfg.MustGet(ctx, "postgresql.domain"),
  cfg.MustGet(ctx, "postgresql.port"),
  cfg.MustGet(ctx, "postgresql.database"),
)

Отметим что для чтения данных можно использовать Get, который следует "go comma ok idiom" или MustGet, который паникует если путь не найден. И весь пример целиком:

package main

import (
  "context"
  "fmt"

  "github.com/boolka/goconfig"
)

func main() {
  ctx := context.Background()

  cfg, _ := goconfig.New(ctx, goconfig.Options{
    Deployment: "production",
  })

  fmt.Println(fmt.Sprintf("postgres://%s:%s@%s:%d/%s",
    cfg.MustGet(ctx, "postgresql.username"),
    cfg.MustGet(ctx, "postgresql.password"),
    cfg.MustGet(ctx, "postgresql.domain"),
    cfg.MustGet(ctx, "postgresql.port"),
    cfg.MustGet(ctx, "postgresql.database"),
  ))
}

Единственное что для stage/production сред (как мы и указали выше) надо будет передать пользователя/пароль через переменные окружения POSTGRESQL_USERNAME/POSTGRESQL_PASSWORD, так как значения по-умолчанию вряд-ли подойдут. К примеру так:

POSTGRESQL_USERNAME=production_login POSTGRESQL_PASSWORD=production_password go run main.go

В зависимости от того какой мы передали Deployment будут меняться настройки и поиск значения будет проходить по пути env.toml -> {deployment}.toml -> default.toml, падая в файл с настройками по-умолчанию если не найдено ничего в предыдущих. Помимо указания окружения (production, stage, ci/cd и т.д.) можно указать Hostname и/или Instance для multi instance сборок.

Загрузка из vault

Давайте под занавес добавим загрузку логина/пароля из vault для конфигурации нашей базы. Для этого просто добавляем файл vault.toml в папку с конфигурационными файлами такого содержания:

vault.toml:

[postgresql]
username = "secret,postgresql,username"
password = "secret,postgresql,password"

[goconfig.vault]
address = "http://localhost:8200"

[goconfig.vault.auth]
token = "root"

Vault сервер можно запустить так: docker run -p 8200:8200 hashicorp/vault server -dev -dev-root-token-id=root start. Авторизуйтесь (токен root) и создайте секрет postgresql по пути secret (он предустановлен) с ключами username/password и любыми значениями. В программном коде ничего не меняется, просто запустите ещё раз программу чтобы убедиться что данные загрузились.

Теперь в первую очередь значения username и password будут подгружаться из vault. Вся конфигурация обтекаемая, потому если по каким-то причинам не удалось загрузить из vault (например такого секрета не существует) то конфигуратор будет дальше идти по источникам вплоть до default значений.

В конфигурации vault источника особенный смысл имеет значение полей (наши username/password) - это mount + secret + key по которому хранится само значение в секрете. Выражаясь терминологией vault это /v1/secret/data/postgresql API path или -mount="secret" "postgresql" CLI path + ключ в самом секрете.

Указание полей секции [goconfig.vault] и [goconfig.vault.auth] это только один из способов сконфигурировать и авторизовать vault клиент. Их можно указать в любом файле конфигурации. Так же можно передать клиент напрямую и передать авторизационный интерфейс AuthMethod (определен в vault api). Конечно же указание токена непосредственно в конфигурации это только для примера.

P.S.

Хочется ещё много чего добавить, например различные источники для загрузки данных, вроде etcd, методы на проверку их доступности. Возможно именно вы это сделаете, если вам понравится данных подход и утилита. Потому приветствуются доработки и правки.

А как вы конфигурируете приложение ? Поделитесь своим способом. Буду рад любой критике.

Ссылки