Gerenciamento de Configuração com Viper
Como unir arquivos YAML, variáveis de ambiente e flags em uma configuração única, seguindo o princípio de configurar pelo ambiente.
Publicada em · 6 min de leitura
Gerenciamento de Configuração com Viper
Em aplicações nativas da nuvem e microsserviços modernos, o gerenciamento de configurações segue estritamente o terceiro princípio do manifesto The Twelve-Factor App: Armazene as configurações no ambiente. O mesmo artefato binário ou imagem Docker deve ser capaz de rodar em desenvolvimento local, homologação e múltiplos clusters Kubernetes de produção apenas alterando variáveis de ambiente ou arquivos de configuração, sem necessidade de recompilação.
No ecossistema Go, a biblioteca spf13/viper é o padrão definitivo da indústria para gerenciamento de configurações (utilizada por projetos de peso como Docker CLI, Hugo, Helm e o próprio Kubernetes). Compreender sua hierarquia de precedência, a mecânica de mapeamento tipado via mapstructure e as armadilhas clássicas entre variáveis de ambiente e Unmarshal é essencial para qualquer engenheiro de software sênior.
1. Hierarquia de Precedência de Configuração
Uma das maiores forças do Viper é a resolução determinística de valores vindos de múltiplas origens heterogêneas. Se uma mesma chave (ex: porta do servidor) estiver definida em um arquivo .yaml, em uma variável de ambiente e for passada via flag de terminal, o Viper resolve os conflitos obedecendo a uma ordem estrita de precedência:
- Sobrescrita Explícita (
viper.Set): Valores configurados manualmente no código Go em tempo de execução. - Flags de Linha de Comando (
pflag): Parâmetros passados na inicialização do binário (ex:--port=9090). - Variáveis de Ambiente (
ENV): Variáveis do sistema operacional ou injetadas por Kubernetes ConfigMaps/Secrets (APP_PORT=8080). - Arquivos de Configuração: Arquivos no disco (YAML, JSON, TOML,
.env). - Valores Padrão (
Defaults): Configurados viaviper.SetDefault("port", 8080).
2. Mapeamento Tipado: viper.Unmarshal e mapstructure
Acessar configurações espalhadas pelo código utilizando chamadas arbitrárias como viper.GetString("database.host") é um anti-pattern severo: resulta em erros de digitação não detectados em tempo de compilação, acoplamento desnecessário com a biblioteca do Viper e dificuldades em testes unitários.
O padrão arquitetural correto é consolidar todas as configurações em uma struct Go fortemente tipada logo na inicialização da aplicação:
package config
import (
"time"
)
type Config struct {
Environment string `mapstructure:"environment"`
Server ServerConfig `mapstructure:"server"`
Database DatabaseConfig `mapstructure:"database"`
Security SecurityConfig `mapstructure:"security"`
}
type ServerConfig struct {
Port int `mapstructure:"port"`
ReadTimeout time.Duration `mapstructure:"read_timeout"`
WriteTimeout time.Duration `mapstructure:"write_timeout"`
}
type DatabaseConfig struct {
Host string `mapstructure:"host"`
Port int `mapstructure:"port"`
Username string `mapstructure:"username"`
Password string `mapstructure:"password"`
Database string `mapstructure:"name"`
PoolSize int `mapstructure:"pool_size"`
}
type SecurityConfig struct {
JWTSecret string `mapstructure:"jwt_secret"`
TokenTTL time.Duration `mapstructure:"token_ttl"`
}
3. A Grande Armadilha do Viper: AutomaticEnv vs Unmarshal
Quase todo desenvolvedor Go passa por este bug em produção:
“Defini
APP_DATABASE_HOST=postgres.infrano contêiner Docker, mas ao executarviper.Unmarshal(&cfg), o campocfg.Database.Hostficou vazio!”
Por que isso acontece?
Por razões de desempenho e limites da reflexão, o método Unmarshal() itera sobre as chaves retornadas por viper.AllKeys(). No entanto, o AutomaticEnv() não popula ativamente o mapa de chaves internas com todas as variáveis do sistema operacional; ele apenas faz um lookup sob demanda quando alguém invoca viper.Get("chave").
Se a chave não existe no arquivo de configuração e não possui um valor padrão registrado, o Unmarshal não sabe que ela existe e simplesmente não a preenche a partir do ambiente!
A Solução Canônica: Registro Obrigatório de Defaults
Sempre declare os valores padrão (defaults) para todas as chaves esperadas. Isso garante que a chave exista no catálogo interno do Viper, permitindo que a variável de ambiente a sobrescreva durante o Unmarshal:
package config
import (
"fmt"
"strings"
"time"
"github.com/spf13/viper"
)
func LoadConfig(configPath string) (*Config, error) {
v := viper.New()
// 1. REGISTRO DE VALORES PADRÃO (Essencial para AutomaticEnv funcionar com Unmarshal)
v.SetDefault("environment", "development")
v.SetDefault("server.port", 8080)
v.SetDefault("server.read_timeout", "5s")
v.SetDefault("server.write_timeout", "10s")
v.SetDefault("database.host", "localhost")
v.SetDefault("database.port", 5432)
v.SetDefault("database.username", "postgres")
v.SetDefault("database.password", "")
v.SetDefault("database.name", "app_db")
v.SetDefault("database.pool_size", 10)
v.SetDefault("security.jwt_secret", "dev-insecure-secret-key-change-me")
v.SetDefault("security.token_ttl", "24h")
// 2. CONFIGURAÇÃO DE ARQUIVOS (Opcional: local ou Kubernetes ConfigMap)
if configPath != "" {
v.SetConfigFile(configPath)
} else {
v.AddConfigPath(".")
v.AddConfigPath("./config")
v.SetConfigName("config")
v.SetConfigType("yaml")
}
// Tenta ler o arquivo no disco (se não existir, não quebra; usa defaults + envs)
if err := v.ReadInConfig(); err != nil {
if _, ok := err.(viper.ConfigFileNotFoundError); !ok {
return nil, fmt.Errorf("erro ao ler arquivo de configuração: %w", err)
}
}
// 3. INTEGRAÇÃO COM VARIÁVEIS DE AMBIENTE
// Mapeia chaves aninhadas: database.host vira APP_DATABASE_HOST
v.SetEnvPrefix("APP")
v.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
v.AutomaticEnv()
// 4. DESSERIALIZAÇÃO TIPADA
var cfg Config
if err := v.Unmarshal(&cfg); err != nil {
return nil, fmt.Errorf("falha ao deserializar configurações: %w", err)
}
// 5. VALIDAÇÃO DE SEGURANÇA
if cfg.Environment == "production" && cfg.Security.JWTSecret == "dev-insecure-secret-key-change-me" {
return nil, fmt.Errorf("erro crítico de segurança: JWTSecret padrão em ambiente de produção")
}
return &cfg, nil
}
4. Hot Reloading de Configurações em Tempo de Execução
Em sistemas distribuídos, alterar configurações dinâmicas (como níveis de log, rate limits, feature flags ou timeouts) sem reiniciar o processo ou derrubar pods Kubernetes é uma enorme vantagem operacional.
O Viper suporta observação ativa do sistema de arquivos via biblioteca fsnotify:
package config
import (
"log/slog"
"github.com/fsnotify/fsnotify"
"github.com/spf13/viper"
)
// WatchDynamicConfig monitora alterações no arquivo YAML e atualiza em tempo real
func WatchDynamicConfig(v *viper.Viper, onReload func(newCfg *Config)) {
v.WatchConfig()
v.OnConfigChange(func(e fsnotify.Event) {
slog.Warn("Arquivo de configuração alterado no disco", slog.String("file", e.Name))
var updatedConfig Config
if err := v.Unmarshal(&updatedConfig); err != nil {
slog.Error("Falha ao recarregar configuração dinâmica", slog.Any("error", err))
return
}
// Dispara callback de recarga (ex: altera logLevel ou recarrega rate limiter)
onReload(&updatedConfig)
})
}
5. Estratégia de Múltiplos Ambientes
Para manter a separação clara entre ambientes de desenvolvimento, testes CI/CD e produção:
├── config/
│ ├── config.yaml # Valores padrão compartilhados
│ ├── config.development.yaml
│ └── config.production.yaml
├── .env.example # Documentação de variáveis de ambiente para desenvolvedores
Na inicialização da aplicação:
env := os.Getenv("APP_ENV")
if env == "" {
env = "development"
}
v.SetConfigName("config." + env)
No cluster Kubernetes ou ECS, não suba arquivos com credenciais de produção. Monte apenas as variáveis de ambiente necessárias a partir do SecretProviderClass ou AWS Secrets Manager, permitindo que o APP_DATABASE_PASSWORD sobrescreva os defaults diretamente da memória.
6. Checklist de Produção para Configurações em Go
- Nunca hardcodar URLs, credenciais ou portas no código fonte.
- Registrar valores padrão (
SetDefault) para todas as chaves estruturadas. - Configurar
SetEnvPrefix("APP")eSetEnvKeyReplacer(strings.NewReplacer(".", "_")). - Unmarshal direto para uma struct fortemente tipada utilizando tags
mapstructure. - Isolar a struct de configuração: passar apenas as sub-estruturas necessárias para cada serviço (Injeção de Dependências com Google Wire).
- Validar configurações mandatórias no startup da aplicação (falhar rápido / fail fast se uma credencial crítica faltar).
- Adicionar arquivo
.envao.gitignoree disponibilizar.env.example.
Links Relacionados
- Injeção de Dependências com Google Wire: Injetando
*Configem repositórios, handlers e conexões. - Logging Estruturado com slog: Configurando o nível de log (
LevelDebug,LevelInfo) dinamicamente. - Servidor HTTP Nativo Moderno: Alimentando timeouts do servidor a partir de
ServerConfig. - Graceful Shutdown e Signal Handling: Encerramento limpo respeitando limites configurados.