Pular para o conteúdo

Trilha de Go · parte 10 de 37

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

Tamanho da fonte:
Largura da coluna:

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:

Precedência Viper
figura 1 · Precedência Viper
  1. Sobrescrita Explícita (viper.Set): Valores configurados manualmente no código Go em tempo de execução.
  2. Flags de Linha de Comando (pflag): Parâmetros passados na inicialização do binário (ex: --port=9090).
  3. Variáveis de Ambiente (ENV): Variáveis do sistema operacional ou injetadas por Kubernetes ConfigMaps/Secrets (APP_PORT=8080).
  4. Arquivos de Configuração: Arquivos no disco (YAML, JSON, TOML, .env).
  5. Valores Padrão (Defaults): Configurados via viper.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:

Mapeamento Tipado com Viper
figura 2 · Mapeamento Tipado com Viper
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.infra no contêiner Docker, mas ao executar viper.Unmarshal(&cfg), o campo cfg.Database.Host ficou 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") e SetEnvKeyReplacer(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 .env ao .gitignore e disponibilizar .env.example.

↑ Voltar ao topo

Atalhos de teclado

Abrir busca
/
Alternar tema claro/escuro
t
Aula anterior na série
←
Próxima aula na série
→
Lista de atalhos
?