Post
EN

Migrando aplicações Go para Kubernetes: problemas reais e soluções

E aí, pessoal!

Migrar uma aplicação Go para Kubernetes parece simples no papel. Você cria um Dockerfile, faz deploy e pronto, certo?

Errado.

Na prática, você encontra problemas que não aparecem na documentação. Aplicações que funcionavam perfeitamente em servidores tradicionais começam a se comportar de forma estranha. Requests que demoram mais. Conexões que caem. Recursos que não são liberados.

Este post é sobre esses problemas reais. E sobre como resolvê-los de verdade.

O que você vai encontrar aqui

Este guia cobre os problemas mais comuns que desenvolvedores Go enfrentam ao migrar para Kubernetes:

  1. ConfigMaps e Secrets: como gerenciar configuração
  2. Health checks: liveness e readiness probes
  3. Graceful shutdown: encerrando aplicações corretamente
  4. Service discovery: encontrando outros serviços
  5. Networking e DNS: problemas de conectividade
  6. Resource limits: CPU e memória
  7. Logs e observabilidade: o que mudou

Cada seção tem problemas reais e soluções práticas que funcionam em produção.

1. ConfigMaps e Secrets: gerenciando configuração

O problema

Sua aplicação Go provavelmente lê configuração de arquivos ou variáveis de ambiente:

1
2
3
4
5
// app.go
config := os.Getenv("DATABASE_URL")
if config == "" {
    log.Fatal("DATABASE_URL não configurada")
}

Em Kubernetes, você não pode simplesmente editar arquivos no servidor. Você precisa usar ConfigMaps e Secrets.

A solução

Opção 1: Variáveis de ambiente (mais simples)

Crie um ConfigMap:

1
2
3
4
5
6
7
apiVersion: v1
kind: ConfigMap
metadata:
  name: app-config
data:
  DATABASE_URL: "postgres://user:pass@db:5432/mydb"
  LOG_LEVEL: "info"

E use no Deployment:

1
2
3
4
5
6
7
8
9
10
11
12
13
apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-app
spec:
  template:
    spec:
      containers:
      - name: app
        image: my-app:latest
        envFrom:
        - configMapRef:
            name: app-config

Opção 2: Arquivos montados (mais flexível)

Para arquivos de configuração maiores:

1
2
3
4
5
6
7
8
9
10
spec:
  containers:
  - name: app
    volumeMounts:
    - name: config
      mountPath: /etc/app
  volumes:
  - name: config
    configMap:
      name: app-config

Para Secrets (dados sensíveis):

1
2
3
4
5
6
7
apiVersion: v1
kind: Secret
metadata:
  name: app-secrets
type: Opaque
stringData:
  API_KEY: "sua-chave-secreta"
1
2
3
envFrom:
- secretRef:
    name: app-secrets

Problema comum: ConfigMap não atualiza

ConfigMaps montados como volumes são atualizados, mas sua aplicação precisa recarregar. Para variáveis de ambiente, você precisa recriar o Pod.

Solução: Você pode implementar hot reload na sua aplicação Go para recarregar configurações automaticamente quando o ConfigMap mudar. No vídeo abaixo, mostro como fazer isso na prática:

Alternativamente, você pode usar um sidecar como Reloader que faz o reload automaticamente.

2. Health checks: liveness e readiness probes

O problema

Sua aplicação Go pode estar rodando, mas não está pronta para receber tráfego. Ou pode estar travada, mas o Kubernetes não sabe.

Sem health checks, o Kubernetes não consegue:

  • Saber quando reiniciar um container travado
  • Saber quando a aplicação está pronta para receber tráfego
  • Fazer rolling updates de forma segura

A solução

Implemente endpoints de health check na sua aplicação:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// health.go
func healthHandler(w http.ResponseWriter, r *http.Request) {
    w.WriteHeader(http.StatusOK)
    w.Write([]byte("OK"))
}

func readinessHandler(w http.ResponseWriter, r *http.Request) {
    // Verifica se está pronto (DB conectado, etc)
    if db.Ping() != nil {
        w.WriteHeader(http.StatusServiceUnavailable)
        return
    }
    w.WriteHeader(http.StatusOK)
}

Configure as probes no Deployment:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
spec:
  containers:
  - name: app
    livenessProbe:
      httpGet:
        path: /health
        port: 8080
      initialDelaySeconds: 30
      periodSeconds: 10
    readinessProbe:
      httpGet:
        path: /ready
        port: 8080
      initialDelaySeconds: 5
      periodSeconds: 5

Diferença entre liveness e readiness

1
2
3
4
5
6
7
8
9
10
11
┌─────────────────────────────────┐
│  Liveness Probe                │
│  "A aplicação está viva?"      │
│  Se falhar → reinicia o Pod    │
└─────────────────────────────────┘

┌─────────────────────────────────┐
│  Readiness Probe                │
│  "A aplicação está pronta?"     │
│  Se falhar → remove do Service  │
└─────────────────────────────────┘

Liveness: detecta se a aplicação está travada e precisa ser reiniciada.

Readiness: detecta se a aplicação está pronta para receber tráfego (DB conectado, cache carregado, etc).

Problema comum: probes muito agressivas

Se suas probes falharem muito rápido, o Kubernetes vai reiniciar seu Pod constantemente.

Solução: Ajuste initialDelaySeconds para dar tempo da aplicação inicializar.

3. Graceful shutdown: encerrando corretamente

O problema

Quando o Kubernetes precisa encerrar um Pod (rolling update, scale down), ele envia um SIGTERM. Se sua aplicação não tratar isso corretamente, você pode:

  • Perder requests em processamento
  • Não fechar conexões de banco de dados
  • Não salvar estado
  • Corromper dados

A solução

Implemente graceful shutdown na sua aplicação Go:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// main.go
func main() {
    // Cria servidor HTTP
    srv := &http.Server{
        Addr:    ":8080",
        Handler: mux,
    }

    // Canal para receber sinais do sistema
    sigChan := make(chan os.Signal, 1)
    signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)

    // Inicia servidor em goroutine
    go func() {
        if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
            log.Fatalf("servidor falhou: %v", err)
        }
    }()

    // Espera sinal
    <-sigChan
    log.Println("Shutdown iniciado...")

    // Cria contexto com timeout para shutdown
    ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
    defer cancel()

    // Shutdown graceful
    if err := srv.Shutdown(ctx); err != nil {
        log.Fatalf("shutdown forçado: %v", err)
    }

    // Fecha conexões de banco, etc
    db.Close()
    log.Println("Shutdown completo")
}

Configure o Pod para dar tempo:

1
2
3
4
5
6
7
8
spec:
  containers:
  - name: app
    lifecycle:
      preStop:
        exec:
          command: ["/bin/sh", "-c", "sleep 15"]
  terminationGracePeriodSeconds: 30

O que acontece

1
2
3
4
5
1. Kubernetes envia SIGTERM
2. Sua aplicação para de aceitar novos requests
3. Espera requests em processamento terminarem
4. Fecha conexões
5. Encerra graciosamente

terminationGracePeriodSeconds: tempo máximo que Kubernetes espera antes de forçar kill (SIGKILL).

4. Service discovery: encontrando outros serviços

O problema

Em servidores tradicionais, você pode usar IPs fixos ou hosts conhecidos. Em Kubernetes, Pods têm IPs dinâmicos. Como encontrar outros serviços?

A solução

Kubernetes tem DNS interno. Use nomes de serviços:

1
2
3
4
5
// Em vez de:
dbURL := "postgres://user:pass@192.168.1.10:5432/db"

// Use:
dbURL := "postgres://user:pass@postgres-service:5432/db"

O DNS do Kubernetes resolve automaticamente:

1
2
3
4
5
6
7
8
9
10
11
12
┌─────────────────────────────────┐
│  Nome do Service                │
│  postgres-service               │
│  ↓                              │
│  DNS do Kubernetes              │
│  ↓                              │
│  IP do Service                  │
│  ↓                              │
│  Load Balancer                  │
│  ↓                              │
│  Pods do serviço                │
└─────────────────────────────────┘

Formato: <service-name>.<namespace>.svc.cluster.local

Para serviços no mesmo namespace, só precisa do nome: postgres-service

Exemplo prático

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// config.go
func getDBURL() string {
    // Kubernetes DNS
    host := os.Getenv("DB_HOST")
    if host == "" {
        host = "postgres-service" // Nome do Service
    }
    
    port := os.Getenv("DB_PORT")
    if port == "" {
        port = "5432"
    }
    
    return fmt.Sprintf("postgres://user:pass@%s:%s/db", host, port)
}

Problema comum: DNS não resolve

Se você estiver testando localmente ou em desenvolvimento, o DNS do Kubernetes não existe.

Solução: Use variáveis de ambiente para desenvolvimento:

1
2
3
4
5
6
7
8
9
10
host := os.Getenv("DB_HOST")
if host == "" {
    if os.Getenv("KUBERNETES_SERVICE_HOST") != "" {
        // Está em Kubernetes
        host = "postgres-service"
    } else {
        // Desenvolvimento local
        host = "localhost"
    }
}

5. Networking e DNS: problemas de conectividade

O problema

Sua aplicação Go pode não conseguir conectar com outros serviços. Timeouts, conexões recusadas, DNS não resolve.

Problemas comuns e soluções

1. DNS não resolve

1
2
3
4
5
6
7
8
9
10
11
12
// Teste de conectividade
func testConnection(host string) error {
    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    defer cancel()
    
    conn, err := net.DialContext(ctx, "tcp", host)
    if err != nil {
        return fmt.Errorf("não conseguiu conectar: %v", err)
    }
    conn.Close()
    return nil
}

2. Timeouts muito curtos

Ajuste timeouts para ambiente Kubernetes:

1
2
3
4
5
6
7
8
9
10
// HTTP client com timeout adequado
client := &http.Client{
    Timeout: 30 * time.Second,
    Transport: &http.Transport{
        DialContext: (&net.Dialer{
            Timeout:   10 * time.Second,
            KeepAlive: 30 * time.Second,
        }).DialContext,
    },
}

3. Conexões não são reutilizadas

Use connection pooling:

1
2
3
4
// Para banco de dados
db.SetMaxOpenConns(25)
db.SetMaxIdleConns(5)
db.SetConnMaxLifetime(5 * time.Minute)

Debugging de rede

Se algo não funciona, verifique:

1
2
3
4
# Dentro do Pod
nslookup postgres-service
curl http://postgres-service:5432
ping postgres-service

6. Resource limits: CPU e memória

O problema

Sem limits, sua aplicação pode:

  • Consumir toda a CPU do nó
  • Esgotar memória do nó
  • Ser morta pelo OOMKiller
  • Afetar outros Pods

A solução

Configure requests e limits:

1
2
3
4
5
6
7
8
9
10
spec:
  containers:
  - name: app
    resources:
      requests:
        memory: "128Mi"
        cpu: "100m"
      limits:
        memory: "256Mi"
        cpu: "500m"

Requests: recursos garantidos (scheduling)

Limits: máximo que pode usar

GOMAXPROCS e CPU limits

Go usa GOMAXPROCS baseado em CPUs disponíveis. Em containers com CPU limits, isso pode ser problemático.

Solução: Use automaxprocs:

1
2
3
4
5
6
import _ "go.uber.org/automaxprocs"

func main() {
    // GOMAXPROCS será ajustado automaticamente
    // baseado nos CPU limits do container
}

Memory limits e GC

Com memory limits, o GC do Go precisa trabalhar mais:

1
2
3
4
// Ajuste GOGC se necessário
// GOGC=50 = mais agressivo (usa menos memória)
// GOGC=100 = padrão
// GOGC=200 = menos agressivo (usa mais memória)

Monitore uso de memória:

1
2
3
var m runtime.MemStats
runtime.ReadMemStats(&m)
log.Printf("Memória alocada: %d MB", m.Alloc/1024/1024)

7. Logs e observabilidade: o que mudou

O problema

Em servidores tradicionais, logs vão para arquivos. Em Kubernetes, Pods são efêmeros. Logs são perdidos quando Pods são recriados.

A solução

1. Logs estruturados (JSON)

1
2
3
4
5
6
7
8
import "github.com/sirupsen/logrus"

logrus.SetFormatter(&logrus.JSONFormatter{})

logrus.WithFields(logrus.Fields{
    "user_id": 123,
    "action": "login",
}).Info("Usuário fez login")

2. Log para stdout/stderr

Kubernetes captura stdout/stderr automaticamente:

1
2
3
// Use log padrão ou biblioteca de logging
log.Println("Mensagem de log")
fmt.Fprintf(os.Stderr, "Erro: %v\n", err)

3. Context para tracing

Use context para propagar trace IDs:

1
2
3
4
5
6
7
8
9
10
11
12
13
func handleRequest(w http.ResponseWriter, r *http.Request) {
    ctx := r.Context()
    
    // Adiciona trace ID ao contexto
    traceID := r.Header.Get("X-Trace-ID")
    if traceID == "" {
        traceID = generateTraceID()
    }
    ctx = context.WithValue(ctx, "trace_id", traceID)
    
    // Logs incluem trace ID
    log.WithContext(ctx).Info("Processando request")
}

Integração com observabilidade

Para métricas, use Prometheus:

1
2
3
4
5
6
7
8
9
10
11
12
import "github.com/prometheus/client_golang/prometheus"

var httpRequests = prometheus.NewCounterVec(
    prometheus.CounterOpts{
        Name: "http_requests_total",
    },
    []string{"method", "endpoint", "status"},
)

func init() {
    prometheus.MustRegister(httpRequests)
}

Problemas comuns e soluções rápidas

ProblemaCausaSolução
Pod reinicia constantementeLiveness probe falhandoAumentar initialDelaySeconds
Requests perdidos no deploySem graceful shutdownImplementar shutdown graceful
Não conecta com outros serviçosDNS não resolveUsar nomes de Services
Alto uso de memóriaSem limitsConfigurar memory limits
CPU não é usado eficientementeGOMAXPROCS erradoUsar automaxprocs
Logs perdidosLogs em arquivosLog para stdout/stderr
Timeouts frequentesTimeouts muito curtosAjustar timeouts de rede

Conclusão

Migrar para Kubernetes não é só fazer deploy. É adaptar sua aplicação para um ambiente diferente.

Os problemas que você vai encontrar são previsíveis. E as soluções são conhecidas. Este guia cobre os principais.

A chave é entender como Kubernetes funciona e adaptar sua aplicação Go para esse ambiente. Não é difícil, mas requer atenção aos detalhes.

E quando você fizer certo, você ganha:

  • Escalabilidade automática
  • Alta disponibilidade
  • Deploy sem downtime
  • Observabilidade nativa
  • Gerenciamento simplificado

Vale a pena o esforço.

Referências e fontes

Documentação oficial

Artigos e guias

Ferramentas

Código de exemplo

Esta postagem está licenciada sob CC BY 4.0 pelo autor.