
Существует большое количество способов конфигурирования golang приложений, и еще больше библиотек которые реализуют эти методы. В данной статье мы рассмотрим основные.
Аргументы командной строки
В golang уже есть встроенная библиотека для работы с аргументами командной строки. Самый простой пример будет выглядеть так:
зpackage main import ( "flag" "fmt" ) var name = flag.String("name", "", "set name") func main() { flag.Parse() fmt.Printf("hello %s", *name) }
Если у вас несколько аргументов и приложение несложное, например tools использующийся для разработки, то ничего более сложного не надо. Но если к вас к microservice или тем боле большое приложение работающее как демон, такой вариант вам меньше подходит, для такой конфигурации лучше использовать переменные окружения.
Переменные окружения
С данным способом тоже не должно быть много сложностей, простейший пример будет таким:
package main import ( "fmt" "os" ) func main() { port, ok := os.LookupEnv("FDEVS_BLOG_PORT") if ok { fmt.Printf("port: %s", port) } }
Переменные окружения позволяют вам или эксплуатации сервиса независимо конфигурировать ваше приложение, многие библиотеку уже используют такой подход, например jaeger использует JAEGER_AGENT_HOST
для указания адреса. Если вы используете данный способ конфигурации, не забывайте что ваш переменные могут пересекаться с другими переменными окружения, чтобы этого избежать используете префикс, например это может быть такой формат <org>_<app>_<env>
, единственное вы не сможете одно приложение запустить дважды с разными настройками на одном хосте, но как раз для таких случаев используется docker.
Файл конфигурации
Очень часто встречается конфигурация приложения через файл, при этом формат может встречаться разный от json/yaml до hcl. Json уже умеет стандартную библиотеку, но остальные форматы могут быть удобнее для конфигурирования, хотя быы по простой причине что они поддерживают больше форматов данных, например duration можно записать в виде 42m
. Есть множество библиотек для работы с разными форматами файлов, вот небольшой пример одной из таких библиотек:
package main import ( "fmt" "log" "github.com/ilyakaznacheev/cleanenv" ) type Config struct { Port int `yaml:"host" env:"HOST" env-default:"8080"` } func main() { var cfg Config err := cleanenv.ReadConfig("config.yml", &cfg) if err != nil { log.Fatal(err) } fmt.Printf("port: %d", cfg.Port) }
Плохой пример это когда в такой файл добавляют приватные данные, например логин и пароль к базе данных или ключи доступа, эти данные не должны хранится в git ни в каком виде. Еще часто в такой файл выносят вообще все настройки, в таком виде еще больше усложняется конфигурирование сервиса, обычно эти данные прописываются там как const и никогда не меняются, уже не известно используются они или так исторически сложилось. В таком виде настройки сервиса лучше хранить в const. Удачное применение такой настройки например в golangci-lint, что позволяет в каждом проект иметь свои настройки, а также иметь глобальные настройки. Но очень мало сервисов не использует приватных данных для своих настроек, для этого лучше использовать специализированные хранилища, например vault.
Vault
Считают одним из преимуществ Vault это его безопасность, гибкость. Что позволяет все приватные данные безопасно хранить. В коде можно будет их получить с помощью рекомендованной библиотеки.
package main import ( "fmt" "github.com/hashicorp/vault/api" ) func main() { client, err := api.NewClient(&api.Config{ Address: "127.0.0.1", }) s, err := client.Logical().Read("fdevs/config/database") dsn := s.Data["data"].(map[string]interface{})["dsn"] fmt.Print("database dsn: ", dsn) }
Данный пример для наглядности очень сильно упрощен, убрана как минимум авторизация и многие проверки. Даже простая настройка клиента позволяет менять токен или использовать сертификат на сервере, доступ к секретам ограничен самими настройками vault.
Etcd
С секретами и паролями мы определились, но что где стоит хранить остальные настройки, если в файле не поменять а для бизнеса важно их менять, для этого есть распределенное хранилище etcd. Данное хранилище позволяет не только получать настройки при старте сервиса но подписываться на изменения, например:
package main import ( "context" "fmt" "log" "time" client "go.etcd.io/etcd/v3/clientv3" ) func main() { ctx := context.Background() etcd, err := client.New(client.Config{ Endpoints: []string{"127.0.0.1"}, DialTimeout: time.Second, }) if err != nil { log.Fatal(err) } resp, err := etcd.Get(ctx, "fdevs/config/count_items") if err != nil { log.Fatal(err) } fmt.Print("count_items:", resp.Kvs[0].Value) watch := etcd.Watcher.Watch(ctx, "fdevs/config/count_items") for w := range watch { fmt.Print("count_items:", w.Events[0].Kv.Value) } }
Что в процессе работы приложения позволяет в реальном менять его настройки. И не все что можно использовать стоит использовать, уж точно не рекомендовал туда выносит постоянные настройки.
Тут были перечисленные не все методы настройки вашего приложения, например для хранения секретов есть еще docker secrets. Какой выбирать и в каком в любом случае решать вам и прийдется уже его придерживаться единого подхода. Но чтобы для разработки нечего не изменилось, вам же не важно где хранятся настройки, есть библиотека 4devs config.
Динамическая конфигурация
Библиотека 4devs config позволяет использовать настройки не зависимо от того где они хранятся, при этом настройки могут быть как быть в переменой окружения так и в etcd, какую использовать решаете вы в процессе конфигурации.
package main import ( "context" "fmt" "log" "gitoa.ru/go-4devs/config" "gitoa.ru/go-4devs/config/provider/arg" "gitoa.ru/go-4devs/config/provider/env" "gitoa.ru/go-4devs/config/provider/etcd" "gitoa.ru/go-4devs/config/provider/json" "gitoa.ru/go-4devs/config/provider/vault" "gitoa.ru/go-4devs/config/test" ) func main() { ctx := context.Background() cfg := configure(ctx) dsn, err := cfg.Value(ctx, "example:dsn") if err != nil { log.Fatal(err) } fmt.Printf("dsn from vault: %s\n", dsn.String()) port, err := cfg.Value(ctx, "listen") if err != nil { log.Fatal(err) } fmt.Printf("listen from env: %d\n", port.Int()) enabled, err := cfg.Value(ctx, "maintain") if err != nil { log.Fatal(err) } fmt.Printf("maintain from etcd: %v\n", enabled.Bool()) err = cfg.Watch(ctx, "maintain", func(ctx context.Context, old, new config.Variable) { fmt.Println("update ", old.Provider, " variable:", old.Name, ", old: ", old.Value.Bool(), " new:", new.Value.Bool()) }) if err != nil { log.Fatal(err) } } func configure(ctx context.Context) *config.WatchClient { // configure etcd client etcdClient, err := test.NewEtcd(ctx) if err != nil { log.Fatal(err) } // configure vault client vaultClient, err := test.NewVault() if err != nil { log.Fatal(err) } // read json config jsonConfig := test.ReadFile("config.json") providers := []config.Provider{ arg.New(), env.New(), etcd.NewProvider(etcdClient), vault.NewSecretKV2(vaultClient), json.New(jsonConfig), } return config.NewWatch(test.Namespace, test.AppName, providers) }
В зависимости от инициализации провайдеров мы можем менять приоритет переменных, также можем добавить возможность следить за конфигурацией по определенному периоду, если env переменная меняется менять ее в приложении. Данная библиотека позволяет использовать конфигурацию более гибко при разработке, и решать где хранить переменные уже в процессе эксплуатации.
Вывод
Конфигурировать приложения в golang очень просто, есть встроенные библиотеки которые еще больше упрощают данный процесс. Выбирать их надо в зависимости от поставленной задачи. Есть мнение что конфигурировать сервисы нужно через переменные окружения а логи писать в stdout, что позволить вам легко разворачивать ваши приложения. Если же у вас много сервисов, сложная бизнес логика, то на каком-то одном не получится остановится, нужно использовать несколько, даже golangci-lint позволяет использовать файл и аргументы командной строки.