Цели модуля
После модуля студент сможет:
- Проектировать конфигурацию приложения с разделением по окружениям
- Использовать viper для чтения YAML и переменных окружения
- Реализовывать валидацию конфигурации при старте (fail fast)
- Безопасно работать с секретами
- Управлять конфигурацией для local/dev/staging/prod
- Понимать 12-Factor App принципы конфигурации
4.1 Зачем нужен Configuration Management
Проблемы без управления конфигурацией
- Захардкоженные значения в коде
- Пароли и ключи в git
- Разные настройки для разных окружений в коде
- Невозможно изменить поведение без пересборки
- Ошибки конфигурации обнаруживаются в runtime
Что выносится в конфигурацию
| Категория | Примеры |
|---|---|
| Сетевые настройки | HTTP порт, gRPC порт, адреса |
| БД | DSN, размер пула, таймауты |
| Внешние сервисы | URL API, ключи доступа |
| Поведение | Log level, feature flags, лимиты |
| Безопасность | HMAC ключи, секреты, сертификаты |
12-Factor App: Config
Принцип III 12-Factor App: храните конфигурацию в окружении.
Конфигурация — это всё что различается между деплоями:
- Ресурсы (БД, кэш, очередь)
- Credentials (пароли, ключи)
- Per-deploy значения (hostname, version)
Код должен быть идентичен для всех окружений. Различия только в конфигурации.
Антипаттерны
-
Конфигурация в коде
// ПЛОХО const DBHost = "prod-db.internal" const APIKey = "sk-123456"
-
Разные файлы для разных окружений в git
// ПЛОХО config_local.yaml config_dev.yaml config_prod.yaml # с реальными секретами в git!
-
Чтение конфигурации в разных местах
// ПЛОХО: каждый сервис читает env напрямую func (s *Service) DoWork() { host := os.Getenv("DB_HOST") // не здесь }
Правильный подход:
- Конфигурация читается один раз при старте
- Валидируется
- Передаётся через DI
- Секреты не в git
4.2 env-based конфигурация
Базовый подход: os.Getenv
gopackage config import ( "fmt" "os" "strconv" ) type Config struct { HTTPAddr string PostgresDSN string LogLevel string ReadTimeout time.Duration } func Load() (*Config, error) { cfg := &Config{ HTTPAddr: getEnv("HTTP_ADDR", ":8080"), PostgresDSN: getEnvRequired("POSTGRES_DSN"), LogLevel: getEnv("LOG_LEVEL", "info"), } timeoutStr := getEnv("READ_TIMEOUT", "15s") timeout, err := time.ParseDuration(timeoutStr) if err != nil { return nil, fmt.Errorf("parse read timeout: %w", err) } cfg.ReadTimeout = timeout return cfg, nil } func getEnv(key, defaultVal string) string { if val := os.Getenv(key); val != "" { return val } return defaultVal } func getEnvRequired(key string) string { val := os.Getenv(key) if val == "" { panic(fmt.Sprintf("required env var %s is not set", key)) } return val }
Когда достаточно env
- Маленькие сервисы (до 10 параметров)
- Простая конфигурация
- Контейнерные деплои (Docker, Kubernetes)
Проблемы чистого env
- Нет структуры (все значения — строки)
- Сложные типы (массивы, вложенные объекты) неудобны
- Нет дефолтов в одном месте
- Нет валидации из коробки
Для средних и больших проектов используйте viper.
4.3 YAML + env через viper
Установка viper
bashgo get github.com/spf13/viper
Структура конфигурационного файла
yaml# configs/app.yaml server: http_addr: ":8080" read_timeout: 15s write_timeout: 15s idle_timeout: 60s shutdown_timeout: 30s database: dsn: "postgres://app:secret@localhost:5432/mydb?sslmode=disable" max_conns: 25 min_conns: 5 max_conn_lifetime: 1h max_conn_idle_time: 30m logging: level: "info" format: "json" features: enable_webhooks: true enable_metrics: true security: hmac_key: "" # из env или secret manager hmac_rotation_days: 30
Загрузка через viper
gopackage config import ( "fmt" "strings" "time" "github.com/spf13/viper" ) type Config struct { Server ServerConfig `mapstructure:"server"` Database DatabaseConfig `mapstructure:"database"` Logging LoggingConfig `mapstructure:"logging"` Features FeaturesConfig `mapstructure:"features"` Security SecurityConfig `mapstructure:"security"` } type ServerConfig struct { HTTPAddr string `mapstructure:"http_addr"` ReadTimeout time.Duration `mapstructure:"read_timeout"` WriteTimeout time.Duration `mapstructure:"write_timeout"` IdleTimeout time.Duration `mapstructure:"idle_timeout"` ShutdownTimeout time.Duration `mapstructure:"shutdown_timeout"` } type DatabaseConfig struct { DSN string `mapstructure:"dsn"` MaxConns int `mapstructure:"max_conns"` MinConns int `mapstructure:"min_conns"` MaxConnLifetime time.Duration `mapstructure:"max_conn_lifetime"` MaxConnIdleTime time.Duration `mapstructure:"max_conn_idle_time"` } type LoggingConfig struct { Level string `mapstructure:"level"` Format string `mapstructure:"format"` } type FeaturesConfig struct { EnableWebhooks bool `mapstructure:"enable_webhooks"` EnableMetrics bool `mapstructure:"enable_metrics"` } type SecurityConfig struct { HMACKey string `mapstructure:"hmac_key"` HMACRotationDays int `mapstructure:"hmac_rotation_days"` } func Load() (*Config, error) { v := viper.New() // Файл конфигурации v.SetConfigName("app") v.SetConfigType("yaml") v.AddConfigPath("./configs") v.AddConfigPath("/etc/app") // Переменные окружения v.AutomaticEnv() v.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) v.SetEnvPrefix("APP") // Дефолты setDefaults(v) // Чтение файла if err := v.ReadInConfig(); err != nil { var notFound viper.ConfigFileNotFoundError if !errors.As(err, ¬Found) { return nil, fmt.Errorf("read config: %w", err) } // Файл не найден - используем env и дефолты } // Unmarshal в структуру var cfg Config if err := v.Unmarshal(&cfg); err != nil { return nil, fmt.Errorf("unmarshal config: %w", err) } // Валидация if err := cfg.Validate(); err != nil { return nil, fmt.Errorf("validate config: %w", err) } return &cfg, nil } func setDefaults(v *viper.Viper) { v.SetDefault("server.http_addr", ":8080") v.SetDefault("server.read_timeout", "15s") v.SetDefault("server.write_timeout", "15s") v.SetDefault("server.idle_timeout", "60s") v.SetDefault("server.shutdown_timeout", "30s") v.SetDefault("database.max_conns", 25) v.SetDefault("database.min_conns", 5) v.SetDefault("logging.level", "info") v.SetDefault("logging.format", "json") }
Приоритет значений
Viper читает значения в следующем приоритете (от высшего к низшему):
- Явно установленные (v.Set)
- Флаги командной строки
- Переменные окружения
- Файл конфигурации
- Дефолты (v.SetDefault)
Это позволяет:
- Иметь базовые настройки в файле
- Переопределять через env в контейнерах
- Секреты только через env
Маппинг env переменных
С SetEnvPrefix("APP") и SetEnvKeyReplacer:
textserver.http_addr -> APP_SERVER_HTTP_ADDR database.dsn -> APP_DATABASE_DSN database.max_conns -> APP_DATABASE_MAX_CONNS security.hmac_key -> APP_SECURITY_HMAC_KEY
4.4 Валидация при старте (fail fast)
Принцип fail fast
Если конфигурация невалидна — приложение должно упасть при старте с понятной ошибкой, а не работать в некорректном состоянии.
Реализация валидации
gofunc (c *Config) Validate() error { var errs []string // Server if c.Server.HTTPAddr == "" { errs = append(errs, "server.http_addr is required") } if c.Server.ReadTimeout <= 0 { errs = append(errs, "server.read_timeout must be positive") } // Database if c.Database.DSN == "" { errs = append(errs, "database.dsn is required") } if c.Database.MaxConns <= 0 { errs = append(errs, "database.max_conns must be positive") } if c.Database.MinConns > c.Database.MaxConns { errs = append(errs, "database.min_conns must be <= max_conns") } // Logging validLevels := map[string]bool{"debug": true, "info": true, "warn": true, "error": true} if !validLevels[c.Logging.Level] { errs = append(errs, fmt.Sprintf("logging.level must be one of: debug, info, warn, error; got %s", c.Logging.Level)) } // Security if c.Security.HMACKey == "" { errs = append(errs, "security.hmac_key is required") } if len(c.Security.HMACKey) < 32 { errs = append(errs, "security.hmac_key must be at least 32 characters") } if len(errs) > 0 { return fmt.Errorf("config validation failed:\n - %s", strings.Join(errs, "\n - ")) } return nil }
Использование go-playground/validator
Для сложных валидаций можно использовать validator:
goimport "github.com/go-playground/validator/v10" type Config struct { Server ServerConfig `mapstructure:"server" validate:"required"` Database DatabaseConfig `mapstructure:"database" validate:"required"` } type ServerConfig struct { HTTPAddr string `mapstructure:"http_addr" validate:"required"` ReadTimeout time.Duration `mapstructure:"read_timeout" validate:"required,gt=0"` } type DatabaseConfig struct { DSN string `mapstructure:"dsn" validate:"required,startswith=postgres://"` MaxConns int `mapstructure:"max_conns" validate:"required,gte=1,lte=100"` } func (c *Config) Validate() error { validate := validator.New() return validate.Struct(c) }
Использование при старте
gofunc main() { cfg, err := config.Load() if err != nil { log.Fatalf("FATAL: %v", err) // падаем при старте } // Конфигурация валидна, продолжаем app, err := app.Build(context.Background(), cfg) if err != nil { log.Fatalf("FATAL: build app: %v", err) } // Запуск }
Пример ошибки при невалидной конфигурации
textFATAL: config validation failed: - database.dsn is required - security.hmac_key must be at least 32 characters - logging.level must be one of: debug, info, warn, error; got "verbose"
Приложение не запустилось. Ошибка понятна. Исправляем конфиг.
4.5 Секреты и их хранение
Что такое секреты
Секреты — чувствительные данные которые не должны быть в git:
- Пароли БД
- API ключи
- HMAC ключи
- TLS сертификаты
- OAuth токены
Правила работы с секретами
- Никогда не коммитить секреты в git
- Секреты передаются через env или secret manager
- Секреты не логируются
- Секреты ротируются периодически
- Доступ к секретам по least privilege
Хранение секретов
| Способ | Когда использовать |
|---|---|
| env vars | Локальная разработка, Docker |
| Vault (HashiCorp) | Production, большие команды |
| Lockbox (Yandex Cloud) | Yandex Cloud инфраструктура |
| AWS Secrets Manager | AWS инфраструктура |
| Kubernetes Secrets | Kubernetes деплой |
.env файл для локальной разработки
Для локальной разработки можно использовать .env файл. ВАЖНО: .env в .gitignore!
env# .env (НЕ коммитить!) APP_DATABASE_DSN=postgres://app:secret@localhost:5432/mydb APP_SECURITY_HMAC_KEY=my-local-hmac-key-at-least-32-chars # .env.example (коммитить как шаблон) APP_DATABASE_DSN=postgres://user:password@localhost:5432/dbname APP_SECURITY_HMAC_KEY=your-hmac-key-here
Загрузка .env через viper
gov.SetConfigName(".env") v.SetConfigType("env") v.AddConfigPath(".") if err := v.ReadInConfig(); err != nil { // .env не обязателен }
Secret Manager: Lockbox
Для production используется secret manager. Пример с Lockbox:
gopackage lockbox import ( "context" "github.com/yandex-cloud/go-sdk" ) type SecretProvider struct { sdk *ycsdk.SDK } func (p *SecretProvider) GetSecret(ctx context.Context, key string) (string, error) { // Запрос секрета из Lockbox // Возвращает значение секрета } // Использование при старте func LoadSecrets(ctx context.Context, provider *lockbox.SecretProvider) (*Secrets, error) { hmacKey, err := provider.GetSecret(ctx, "hmac-key") if err != nil { return nil, fmt.Errorf("load hmac key: %w", err) } return &Secrets{HMACKey: hmacKey}, nil }
Маскирование секретов в логах
go// ПЛОХО: логирование секрета log.Info("connecting to database", "dsn", cfg.Database.DSN) // ПРАВИЛЬНО: маскирование log.Info("connecting to database", "dsn", maskDSN(cfg.Database.DSN)) func maskDSN(dsn string) string { // postgres://user:password@host/db -> postgres://user:***@host/db // ... }
4.6 Конфигурация для local/dev/prod
Стратегия: один код, разные конфиги
Код идентичен для всех окружений. Различия только в конфигурации.
Local (локальная разработка)
Источник: configs/app.yaml + .env
yaml# configs/app.yaml (дефолты для local) server: http_addr: ":8080" database: dsn: "postgres://app:secret@localhost:5432/mydb?sslmode=disable" max_conns: 5 logging: level: "debug" format: "text"
Dev (разработка на сервере)
Источник: configs/app.yaml + env vars
env# env vars переопределяют файл APP_DATABASE_DSN=postgres://app:secret@dev-db:5432/mydb APP_DATABASE_MAX_CONNS=10 APP_LOGGING_LEVEL=info
Staging
Источник: configs/app.yaml + env vars (секреты из secret manager)
envAPP_DATABASE_DSN=postgres://app:secret@staging-db:5432/mydb APP_SECURITY_HMAC_KEY=<из secret manager> APP_LOGGING_LEVEL=info
Prod
Источник: configs/app.yaml + env vars (секреты из secret manager)
envAPP_DATABASE_DSN=postgres://app:secret@prod-db:5432/mydb APP_DATABASE_MAX_CONNS=50 APP_SECURITY_HMAC_KEY=<из secret manager> APP_LOGGING_LEVEL=warn APP_LOGGING_FORMAT=json
Таблица различий
| Параметр | Local | Dev | Staging | Prod |
|---|---|---|---|---|
| log level | debug | info | info | warn |
| log format | text | text | json | json |
| DB max_conns | 5 | 10 | 25 | 50 |
| HMAC key | local | dev | staging | prod |
| Feature flags | all on | all on | partial | controlled |
4.7 Структура конфигурации в шаблоне
Файлы конфигурации
textconfigs/ ├── app.yaml # Основной конфиг (коммитится) ├── app.example.yaml # Шаблон (коммитится) └── systemd/ ├── git-server.service └── git-server-worker.service
internal/config/ в шаблоне
textinternal/config/ ├── config.go # Загрузка и структура ├── defaults.go # Дефолтные значения └── validation.go # Валидация
Пример из шаблона: config.go
gopackage config import ( "fmt" "strings" "time" "github.com/spf13/viper" ) type Config struct { HTTPAddr string `mapstructure:"http_addr"` PostgresDSN string `mapstructure:"postgres_dsn"` NFSPath string `mapstructure:"nfs_path"` LogLevel string `mapstructure:"log_level"` ShutdownTimeout time.Duration `mapstructure:"shutdown_timeout"` } func Load() (*Config, error) { v := viper.New() v.SetConfigName("app") v.SetConfigType("yaml") v.AddConfigPath("./configs") v.AutomaticEnv() v.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) if err := v.ReadInConfig(); err != nil { return nil, fmt.Errorf("read config: %w", err) } var cfg Config if err := v.Unmarshal(&cfg); err != nil { return nil, fmt.Errorf("unmarshal: %w", err) } if err := validate(&cfg); err != nil { return nil, fmt.Errorf("validate: %w", err) } return &cfg, nil }
Привязка к шаблону проекта
Где конфигурация в шаблоне
textinternal/config/ ├── config.go # Загрузка и структура Config ├── defaults.go # Дефолтные значения └── validation.go # Валидация configs/ ├── app.yaml # Основной конфиг ├── app.example.yaml # Шаблон └── systemd/ # Systemd units
Что изучаем на примере шаблона
- Структура Config и вложенные конфиги
- Загрузка через viper (YAML + env)
- Валидация при старте
- Разделение секретов и обычной конфигурации
- Использование в DI-контейнере
Задание: изучите internal/config/
- Какие поля есть в Config?
- Как загружается конфигурация?
- Есть ли валидация?
- Как передаётся конфиг в DI?
💱 Компромиссы
- Чистый env vs YAML через viper:
os.Getenvне тянет зависимостей и достаточен до ~10 параметров, но все значения — строки без структуры и общих дефолтов; viper даёт вложенныйapp.yamlи приоритет env над файлом ценой зависимости иmapstructure-тегов на каждое поле. - Fail fast vs валидация при первом использовании:
Validate()при старте валит процесс со списком всех ошибок сразу, но каждое правило надо предусмотреть заранее; отложенная проверка короче — и превращается в загадочный runtime-баг в production. - Секреты в env vs secret manager: env-переменные просты для локальной разработки и Docker, но ротация и права доступа остаются ручными; Lockbox/Vault дают ротацию и least privilege ценой внешнего вызова при старте и привязки к облаку.
- Ручная валидация vs
go-playground/validator: свойValidate()— обычный Go с говорящими ошибками вродеmust be at least 32 characters; тегиvalidate:"required,gt=0"декларативнее, но прячут правила в строках и их сложнее отлаживать. - Один
app.yaml+ env-оверрайды vs файл на окружение: базовый конфиг коммитится без секретов, аAPP_*переопределяют значения в dev/prod — один источник дефолтов; наборconfig_dev.yaml/config_prod.yamlнагляднее, но плодит копии и провоцирует секреты в git.
Практическое задание
Задание 1: Базовая конфигурация
Создайте internal/config/config.go с загрузкой через viper:
gotype Config struct { HTTPAddr string `mapstructure:"http_addr"` PostgresDSN string `mapstructure:"postgres_dsn"` LogLevel string `mapstructure:"log_level"` } func Load() (*Config, error) { v := viper.New() v.SetConfigName("app") v.SetConfigType("yaml") v.AddConfigPath("./configs") v.AutomaticEnv() if err := v.ReadInConfig(); err != nil { return nil, err } var cfg Config if err := v.Unmarshal(&cfg); err != nil { return nil, err } return &cfg, nil }
Задание 2: Дефолты
Добавьте дефолтные значения:
gofunc setDefaults(v *viper.Viper) { v.SetDefault("http_addr", ":8080") v.SetDefault("log_level", "info") v.SetDefault("database.max_conns", 25) }
Задание 3: Валидация
Реализуйте валидацию Config:
gofunc (c *Config) Validate() error { if c.HTTPAddr == "" { return errors.New("http_addr is required") } if c.PostgresDSN == "" { return errors.New("postgres_dsn is required") } return nil }
Задание 4: Конфиг для разных окружений
Создайте конфиги:
textconfigs/app.yaml # базовый (local) configs/app.dev.yaml # dev overrides configs/app.prod.yaml # prod overrides
Или используйте env vars для overrides.
Задание 5: Секреты через env
Убедитесь что секреты НЕ в конфиг-файле:
yaml# configs/app.yaml - БЕЗ секретов database: dsn: "" # из env: APP_DATABASE_DSN security: hmac_key: "" # из env: APP_SECURITY_HMAC_KEY
Создайте .env.example с шаблоном:
env# .env.example (коммитится) APP_DATABASE_DSN=postgres://user:password@localhost:5432/dbname APP_SECURITY_HMAC_KEY=your-hmac-key-here-min-32-chars
Задание 6: Использование в DI
Интегрируйте конфиг в DI-контейнер:
gofunc Build(ctx context.Context, cfg *config.Config) (*App, error) { // cfg передаётся в Build, не читается внутри pool, err := postgres.NewDB(ctx, cfg.PostgresDSN) // ... } func main() { cfg, err := config.Load() if err != nil { log.Fatalf("load config: %v", err) } app, err := app.Build(context.Background(), cfg) // ... }
Quiz "Проверь себя"
database:
dsn: postgres://admin:P@ssw0rd@db.prod:5432/appAPP_DATABASE_DSN?viper.AutomaticEnv()
viper.SetEnvPrefix("APP")
viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
dsn := viper.GetString("database.dsn")✅ Итоги модуля
Студент должен:
- Проектировать конфигурацию приложения
- Использовать viper для YAML + env
- Реализовывать валидацию при старте
- Безопасно работать с секретами
- Управлять конфигами для local/dev/prod
- Понимать 12-Factor App принципы
- Ответить на все вопросы Quiz
🔗 Мостик к Модулю 5
Следующий модуль: Модуль 5. Database Access: PostgreSQL - pgx / sqlx, connection pool tuning, индексы и EXPLAIN ANALYZE, миграции (goose), repository pattern, N+1 problem. Привязка к internal/adapters/postgres/ и migrations/ шаблона.