Всем доброго времени суток! Имея за плечами многолетний опыт разработки в Java, а точнее в Spring Framework и начав разрабатывать на языке Go в промышленных масштабах, я стал сталкиваться с такой проблемой, что мне действительно не хватает многих фишек из Spring'a. И одна из этих проблем: указание переменной среды в качестве параметра в конфигурационных yaml-файлах
Для автоматизации деплоя, использования различных правил сборки проблема вставала все острее и острее. Я поисследовал различные модули и библиотеки. Нашел несколько интересных решений в cleanenv и даже в viper, но пришел к выводу: "а почему бы не сделать что-то свое?!" Сказано - сделано.
Проблема
Задача стояла следующим образом: я не знаю какую библиотеку я буду использовать в будущем, может быть напишу свою, может на рынке появится что-то более интересное. Но библиотека должна быть отделимой и работать в любой момент времени при наличии только двух сущностей: структуры и названия переменной в структуре. Можно обойтись просто кодом.
Решение
У нас есть некий конфиг для удобства. Называется local.yaml. Представляет собой простой yaml-файл с набором переменных. Все переменные окружения прописываются через специальные символы ${MY_VAR}. Прямо как в Spring'e!
properties:
host: ${SERVER_HOST}
port: ${SERVER_PORT}
routes:
- name: Host1
target: ${HOST1_TARGET}
- name: Host2
target: ${HOST2_TARGET}
Теперь понятно как будут выглядеть структуры
type (
ApiServer struct {
Host string `mapstructure:"host"`
Port string `mapstructure:"port"`
Routes []Route `mapstructure:"routes"`
}
Route struct {
Name string `mapstructure:"name"`
Target string `mapstructure:"target"`
}
)
Ну и осталось дело за малым. Проинициализировать структуры нашим файлом конфигураций и прикрепить разработанное расширение. Инициализировать будем viper'ом, но на самом деле это не имеет никакого значения. Приблизительно, не вдаваясь в подробности инициализации viper, это будет выглядеть как-то так
if err := viper.ReadInConfig(); err != nil {
log.Fatalf("could not load configuration: %v", err)
}
viper.AutomaticEnv()
config := &ApiServer{}
if err := viper.UnmarshalKey("properties", config); err != nil {
panic(err)
}
Самое главное тут получить проинциализированный config переменную, которая является нашей структурой к которой мы будем прикручивать уже переменные среды. А это, как показывает код очень и очень просто
import (
gobindenv "github.com/dissdoc/go-bindenv"
)
// Инициализируем переменные
gobindenv.BindEnv(config)
// И теперь делаем что хотим
fmt.Println(config.Host, config.Port)
О расширении пару слов
Называется данное расширение go-bindenv. Устанавливается на ваш вкус, как хотите, как пример. За собой не тянет дополнительных модулей. Все работает максимально "экологично" ;-)
go get github.com/dissdoc/go-bindenv@v0.1.0
Изначально то, что реализовано сейчас - мне хватает более чем. Но если, на ваш взгляд, чего-то недостает, я готов выслушать и реализовать.
Библиотека содержит в себе несколько бизнес-слоев, каждый отвечает за свой функционал, чтобы в дальнейшем было проще расширять.
readenv.go - содержит функционал чтения переменных среды. В случае, если переменная определена в конфиге, но не передано значение - вызывается panic
rule.go - одна из возможных интерпретаций определения переменных. В моем случае я пользуюсь только регулярными выражениями
wrapper.go - рефлексия определяющая в каком типе переменных определено правило и на основе этого инициализирует данную переменную значением
В заключении
Данный функционал мне помогает сократить количество шагов для деплоя, а так же немного поубирать лишний код. С другой стороны, хочется улучшать и расширять функционал данного модуля, но не хочется из него делать очередной швейцарский нож. Надеюсь, что данный модуль будет вам полезен!
P.S. никаких телеграм-каналов, блогов не веду. Делаю все в свое удовольствие. До новых встреч!
UPD: основная задача данного расширения - развязать зависимости кода от конфигурации в части чтения переменных. Теперь, в случае изменения названия переменной - сам код приложения переписывать не нужно
Комментарии (9)
genteelknight
24.11.2023 22:32Можно же просто обойти все настройки в конфиге viper и строковые значения пропустить через
os.ExpandEnv
(https://pkg.go.dev/os#ExpandEnv) без велосипедов и рефлексии
dshemin
24.11.2023 22:32А смотрели ли вы https://github.com/spf13/viper? Выглядит так, как-будто решает проблему. Можно пойти дальше и взять https://github.com/spf13/cobra как фреймворк для написания CLI.
akurilov
В общем, в Go так не делается. Проще всего - передавать конфиг через переменные среды в структуру с помощью, например, https://github.com/kelseyhightower/envconfig
Kolymbarii Автор
Я видел данные подходы, такое же есть и в https://github.com/ilyakaznacheev/cleanenv. Вся проблема этих подходов в том, что если завтра у команды DevOps поменяются правила на названия переменных среды, то меня обязывают идти и исправлять это внутри кода. А это уже процесс проверки/отладки приложения.
Основная задача, которая я поставил - это развязать зависимости кода и конфигурации. И мое расширение это решает, при этом встраиваясь в любые решения работы с конфигурацией
akurilov
Переменные среды для микросервиса обычно определяются разработчиком в deployment.yaml/statefulset.yaml/...
Каким боком здесь devops?
gohrytt
Вам повезло если так
Kolymbarii Автор
Обратите внимание на мое изложение.
Переменные, которые определяются разработчиком НЕ РАВНО название переменных среды. Вы, как разработчик, определяете левую часть, а правая не должна вас касаться (пример: host: ${SERVER_HOST}) по всем принципам разработки. Если вы работаете в крупном проекте, то у вас и доступа нет до того же прода. Вы просто пилите свою часть и все. А название переменной SERVER_HOST вообще может меняться в рамках деплоймента как угодно (не только значение, но и название) Например, поменялись стандарны названия переменных и т.д. То, что вы пытаетесь доказать - это путь к тому, что нужно будет условно лет через 5 брать и пересобирать и затем проверять приложение. Надеюсь, объяснил понятным языком.
akurilov
Если это среда k8s, то, как правило, разработчик сам определяет имена переменных для своего сервиса. Не могу представить, зачем делать иначе. К чему вся эта ненужная гибкость? Только все усложняет
krig
Это уже не DevOps, а старый-добрый (не очень) Operations, если кто-то решает за разработчиков как им называть переменные окружения для конфигурирования их же приложения ;)