Конфигурирование golang приложений

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

Читайте также:

Разворачивание golang приложения с помощью gitlab-ci и docker swarm

У меня есть несколько своих проектов, которые соответственно приходится самому поддерживать разворачивать и тп, к примеру данный блог и другие простые проекты. Когда я использовал symfony и php, развернуть можно просто с помощью capifony, даже не зная Ruby. В связи с тем что я решил перейти на golang то данные инструменты не совсем подходят. Почему не использовать к примеру в проектах тот же php, чаще быстрее разрабатывать на том что используешь в работе. Для начала решил попробовать обычный способ с помощью docker swarm и gitlab ci, есть конечно решения с heroku, но я пока решил использовать vps например vscale. K8s для маленьких проектов возможно будет излишним.

Введение в go mod

С недавнего времени в golang появилась система управления зависимости. В данной статье будут только основные моменты, как можно решать базовые задачи управления зависимостями.

Объединение строк в golang

Одной из частых операций может оказаться объединения(concatenation) строк, есть много библиотек для решения задач, мы рассмотрим несколько самых распространенных примеров.