Всем доброго времени суток! Имея за плечами многолетний опыт разработки в 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)


  1. akurilov
    24.11.2023 22:32

    В общем, в Go так не делается. Проще всего - передавать конфиг через переменные среды в структуру с помощью, например, https://github.com/kelseyhightower/envconfig


    1. Kolymbarii Автор
      24.11.2023 22:32
      +1

      Я видел данные подходы, такое же есть и в https://github.com/ilyakaznacheev/cleanenv. Вся проблема этих подходов в том, что если завтра у команды DevOps поменяются правила на названия переменных среды, то меня обязывают идти и исправлять это внутри кода. А это уже процесс проверки/отладки приложения.

      Основная задача, которая я поставил - это развязать зависимости кода и конфигурации. И мое расширение это решает, при этом встраиваясь в любые решения работы с конфигурацией


      1. akurilov
        24.11.2023 22:32
        +1

        Переменные среды для микросервиса обычно определяются разработчиком в deployment.yaml/statefulset.yaml/...

        Каким боком здесь devops?


        1. gohrytt
          24.11.2023 22:32

          Вам повезло если так


        1. Kolymbarii Автор
          24.11.2023 22:32
          -1

          Обратите внимание на мое изложение.

          Переменные, которые определяются разработчиком НЕ РАВНО название переменных среды. Вы, как разработчик, определяете левую часть, а правая не должна вас касаться (пример: host: ${SERVER_HOST}) по всем принципам разработки. Если вы работаете в крупном проекте, то у вас и доступа нет до того же прода. Вы просто пилите свою часть и все. А название переменной SERVER_HOST вообще может меняться в рамках деплоймента как угодно (не только значение, но и название) Например, поменялись стандарны названия переменных и т.д. То, что вы пытаетесь доказать - это путь к тому, что нужно будет условно лет через 5 брать и пересобирать и затем проверять приложение. Надеюсь, объяснил понятным языком.


          1. akurilov
            24.11.2023 22:32
            +1

            Если это среда k8s, то, как правило, разработчик сам определяет имена переменных для своего сервиса. Не могу представить, зачем делать иначе. К чему вся эта ненужная гибкость? Только все усложняет


      1. krig
        24.11.2023 22:32
        +2

        если завтра у команды DevOps поменяются правила на названия переменных среды

        Это уже не DevOps, а старый-добрый (не очень) Operations, если кто-то решает за разработчиков как им называть переменные окружения для конфигурирования их же приложения ;)


  1. genteelknight
    24.11.2023 22:32

    Можно же просто обойти все настройки в конфиге viper и строковые значения пропустить через os.ExpandEnv (https://pkg.go.dev/os#ExpandEnv) без велосипедов и рефлексии


  1. dshemin
    24.11.2023 22:32

    А смотрели ли вы https://github.com/spf13/viper? Выглядит так, как-будто решает проблему. Можно пойти дальше и взять https://github.com/spf13/cobra как фреймворк для написания CLI.