Files
workshop/aula-17/README.md
T

16 KiB
Raw Blame History

Aula 17 - Cloud Native PostgreSQL (CNPG)

Motivação

Você está pagando $200-400/mês em AWS RDS pra rodar PostgreSQL. E ainda não tem HA cross-region — isso custa mais $100-200/mês.

O CloudNativePG (CNPG) é um operador Kubernetes que roda PostgreSQL production-grade dentro do seu cluster. Na Hetzner, com 3 instâncias (2 sync + 1 async cross-region), você obtém o equivalente ao RDS Multi-AZ + Cross-Region Read Replica por ~$56/mês.

Nesta aula, vamos instalar o CNPG do zero, criar um cluster HA com backup automático para Hetzner Object Storage, e conectar uma app demo.

Comparativo de Custo

AWS RDS Multi-AZ vs CNPG na Hetzner

Item AWS RDS Multi-AZ CNPG na Hetzner
Compute db.r6g.large ~$185/mês 3× CX33 €24/mês ($27)
Storage 200GB ~$23/mês (gp3) ~€9/mês (hcloud volumes)
Backup 7 dias (free) ~€6/mês (Object Storage 1TB)
Total mensal ~$208/mês €51/mês ($56)
HA cross-region +$100-200/mês (produto separado) Incluso (async replica em HEL)
PITR (Point-in-Time Recovery) 7 dias Ilimitado (retido no S3)
Controle total Não (gerenciado pela AWS) Sim (seu cluster, suas regras)

Economia: ~75% (só HA local) ou ~85% (HA + cross-region)

Por que Hetzner?

  • CX33: 4 vCPU Intel, 8GB RAM, 80GB NVMe → ~€8/mês
  • Object Storage: 1TB S3-compatible, 1TB egress free → ~€6/mês
  • Sem surpresas: preço fixo, sem egress cost como AWS ($0.09/GB)

Storage: por que hcloud-volumes e não NVMe local?

As instâncias CX vêm com NVMe local (80-240GB). Mas usamos hcloud-volumes (CSI) por um motivo: NVMe local não é elástico — está preso à instância. Se o node morre, o dado vai junto. O hcloud-volumes é network-attached (Ceph), vive independentemente do node, e pode ser expandido sem downtime. Pra PostgreSQL em produção, sobrevivência do dado > velocidade bruta do disco.

Comparativo Técnico: CNPG vs StackGres

Dois operadores PostgreSQL maduros para Kubernetes. Qual escolher?

Aspecto CNPG StackGres
Governance CNCF Sandbox (2025) OnGres (independente)
Sidecars por pod 0-1 (PgBouncer opcional) 4+ (Envoy, PgBouncer, Fluentd, Prometheus)
RAM overhead ~50-100MB ~300-500MB (sidecars)
YAML mínimo ~80 linhas ~150+ linhas
Backup Barman Cloud (built-in) WAL-G (built-in)
Connection pooling PgBouncer sidecar (opcional) PgBouncer sidecar (always-on)
GitOps friendly CRDs simples, ArgoCD nativo CRDs complexos, possível
Público ideal Indie/startup (lean) Enterprise "batteries included"
Comunidade Muito ativa, CNCF backing Menor, mas dedicada

Por que CNPG para indie hackers?

  1. Menos recursos = custo menor. StackGres consome 3-5× mais RAM em sidecars. Num CX33 com 8GB, isso faz diferença.
  2. Setup mais simples. Um Cluster CRD resolve. StackGres precisa de SGCluster, SGPostgresConfig, SGPgBouncerConfig, SGBackupConfig, SGPoolingConfig...
  3. CNCF Sandbox. Governança aberta, roadmap transparente, não depende de uma empresa.
  4. GitOps nativo. CRDs foram desenhados pra serem declarativos. Funciona com ArgoCD/Flux sem hacks.

StackGres é excelente se você quer PgBouncer, Envoy, Fluentd, e Prometheus já configurados. Mas pra indie hacker que precisa de custo baixo e simplicidade, CNPG é a escolha certa.

Arquitetura

┌─────────────────────────────────────────────────────────────────────┐
│                     Cluster Kubernetes (Talos)                       │
│                                                                     │
│  eu-central (FSN + NBG, ~1-2ms latência)                           │
│  ┌──────────────────────────────────────────────────────────────┐  │
│  │  CNPG Operator (cnpg-system)                                 │  │
│  │                                                              │  │
│  │  ┌──────────────┐  sync    ┌──────────────┐                  │  │
│  │  │   Primary    │◄────────►│  Replica 1   │                  │  │
│  │  │   (RW)       │  ~1ms    │  (RO, sync)  │                  │  │
│  │  └──────┬───────┘          └──────────────┘                  │  │
│  │         │                                                    │  │
│  │  Services:                                                   │  │
│  │    shared-postgres-rw → Primary (read-write)                 │  │
│  │    shared-postgres-ro → Replicas (read-only)                 │  │
│  └─────────┼────────────────────────────────────────────────────┘  │
│            │  async (~20-30ms)                                       │
│            ▼                                                        │
│  hel-southeast (HEL, Finlândia)                                    │
│  ┌──────────────────────────────────────────────────────────────┐  │
│  │  ┌──────────────┐                                             │  │
│  │  │  Replica 2   │  ← DR cross-region (async)                 │  │
│  │  │  (RO, async) │                                             │  │
│  │  └──────────────┘                                             │  │
│  └──────────────────────────────────────────────────────────────┘  │
│                                                                     │
│  Backup: WAL archiving contínuo + base backup diário               │
│          → Hetzner Object Storage (S3, zero egress cost)           │
│                                                                     │
│  Monitoring: PodMonitor → Victoria Metrics (aula-12)               │
└─────────────────────────────────────────────────────────────────────┘

HA em duas camadas

Camada Caminho Latência Propósito
Sync FSN ↔ NBG ~1-2ms Zero RPO, failover automático
Async FSN → HEL ~20-30ms DR cross-region, RPO ~30s

Separando etcd do PostgreSQL

O etcd (control plane do Talos/Kubernetes) fica em eu-central (FSN + NBG). O node em HEL é worker-only — não participa do etcd. Isso respeita o requisito de latência do etcd (< 10ms).

Node Pools e Placement

O CNPG não decide sozinho onde cada replica roda. Ele respeita as regras de scheduling do Kubernetes:

Camada Regra Tipo Efeito
Host podAntiAffinity on kubernetes.io/hostname Hard (required) Nunca 2 pods PG no mesmo node
Zone podAntiAffinity on topology.kubernetes.io/zone Soft (preferred) Espalha entre datacenters quando possível

Para a topologia cross-region (2 em eu-central + 1 em HEL), você precisa:

  1. Node pool eu-central (aula-08): Control plane + workers em FSN/NBG
  2. Node pool HEL: Worker dedicado em Helsinki (adicionar depois da aula-08)

O Hetzner CCM labela nodes automaticamente:

topology.kubernetes.io/zone=fsn1     # Frankfurt
topology.kubernetes.io/zone=nbg1     # Nuremberg
topology.kubernetes.io/zone=hel1     # Helsinki
topology.kubernetes.io/region=eu-central
topology.kubernetes.io/region=hel-southeast

Sem nodes em HEL: Os 3 pods ficam em eu-central (FSN + NBG). Ainda é HA dentro da região. O soft constraint de zona é ignorado graciosamente.

Com nodes em HEL: O scheduler prefere espalhar — 1 pod por datacenter. Você ganha DR cross-region automaticamente.

Conceitos

Conceito Descrição
Cluster CRD principal do CNPG. Define instâncias, storage, backup, e config PostgreSQL
Primary Instância read-write. Recebe todas as escritas
Replica (sync) Réplica síncrona. Confirma cada transação antes do commit. Zero RPO
Replica (async) Réplica assíncrona. DR cross-region. Pequeno RPO (~30s)
Barman Cloud Tool de backup integrado ao CNPG. WAL archiving + base backups → S3
PITR Point-in-Time Recovery. Restaura o banco para qualquer momento com backup
Switchover Promoção planejada de uma replica a primary (zero downtime)
Failover Promoção automática quando o primary falha
PodMonitor CRD do Victoria Metrics que scrafa métricas dos pods CNPG

Pré-requisitos

  • Cluster Kubernetes na Hetzner (aula-08)
    • Mínimo: 3 nodes em eu-central (FSN + NBG) para HA dentro da região
    • Para DR cross-region: adicionar 1 worker em HEL (hel-southeast)
  • Victoria Metrics (aula-12) — opcional, mas recomendado para métricas
  • Hetzner Object Storage com bucket criado
  • kubectl e helm instalados

Estrutura

aula-17/
├── README.md                  # Esta documentação
├── setup.sh                   # Instalação automatizada
├── cleanup.sh                 # Remoção limpa
├── cnpg/
│   ├── cluster.yaml           # Cluster CNPG (3 instâncias, backup S3)
│   ├── backup-secret.yaml     # Template de credenciais S3
│   └── scheduled-backup.yaml  # Backup diário às 03:00
└── app-demo/
    ├── deployment.yaml         # App demo conectando no CNPG
    └── service.yaml            # Service da app demo

Instalação

cd aula-17
./setup.sh

O script vai:

  1. Verificar pré-requisitos
  2. Pedir as credenciais do Hetzner Object Storage
  3. Instalar o CNPG operator
  4. Criar o cluster PostgreSQL (3 instâncias)
  5. Configurar backup automático
  6. Deployar app demo para validar

Verificação

Após a instalação, verifique se tudo está funcionando:

# Status do cluster
kubectl get cluster -n cnpg

# Deve mostrar:
# NAME              AGE   INSTANCES   READY   STATUS              PRIMARY
# shared-postgres   5m    3           3       Cluster in healthy   shared-postgres-1

# Instâncias (primary vs replicas)
kubectl get pods -n cnpg -l cnpg.io/cluster=shared-postgres -o wide

# Deve mostrar 3 pods em nodes diferentes:
# NAME                 ROLE       STATUS   NODE
# shared-postgres-1    primary    Running  node-fsn-1
# shared-postgres-2    replica    Running  node-nbg-2
# shared-postgres-3    replica    Running  node-hel-3

# Serviços criados pelo CNPG
kubectl get svc -n cnpg -l cnpg.io/cluster=shared-postgres

# Deve mostrar:
# shared-postgres-rw   (points to primary)
# shared-postgres-ro   (points to replicas)
# shared-postgres-r    (any instance)

# Logs do app demo (valida conexão)
kubectl logs deployment/pg-demo -n cnpg

Comandos Essenciais

Status e observação

# Status geral do cluster
kubectl get cluster shared-postgres -n cnpg

# Detalhes completos (instâncias, replication, disk, backups)
kubectl describe cluster shared-postgres -n cnpg

# Ver instâncias com role e node
kubectl get pods -n cnpg -l cnpg.io/cluster=shared-postgres -o wide

# Logs de uma instância específica
kubectl logs shared-postgres-1 -n cnpg -c postgres

Failover / Mudar o leader

O CNPG faz failover automático quando o primary falha. Mas você pode forçar manualmente:

# Ver qual instância é o primary
kubectl get pods -n cnpg -l cnpg.io/cluster=shared-postgres \
  -o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.metadata.annotations.cnpg\.io/instanceRole}{"\n"}{end}'

# Promover uma replica específica a primary (switchover manual)
kubectl cnpg promote shared-postgres shared-postgres-2 -n cnpg

# Acompanhar o switchover
kubectl get pods -n cnpg -l cnpg.io/cluster=shared-postgres -o wide -w

Backup

# Ver backups existentes
kubectl get backups -n cnpg

# Trigger backup manual (além do agendado)
kubectl cnpg backup shared-postgres -n cnpg

# Ver detalhes de um backup
kubectl describe backup <backup-name> -n cnpg

# O backup agendado roda às 03:00 diariamente
# Configurado em: cnpg/scheduled-backup.yaml

Recovery / PITR

Restaurar o cluster a partir de um backup S3 para um ponto específico no tempo:

# 1. Remover cluster existente (CUIDADO: perde dados atuais)
kubectl delete cluster shared-postgres -n cnpg

# 2. Restaurar a partir de backup com PITR
cat <<EOF | kubectl apply -f -
apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
  name: shared-postgres
  namespace: cnpg
spec:
  instances: 3
  imageName: ghcr.io/cloudnative-pg/postgresql:17.2-4

  storage:
    size: 10Gi
    storageClass: hcloud-volumes
  walStorage:
    size: 5Gi
    storageClass: hcloud-volumes

  # ── Recovery a partir de backup S3 ──
  bootstrap:
    recovery:
      source: sourceCluster
      recoveryTarget:
        targetTime: "2026-05-09T14:30:00+00:00"

  externalClusters:
    - name: sourceCluster
      barmanObjectStore:
        destinationPath: s3://SEU_BUCKET/cnpg-backups/shared-postgres
        endpointURL: https://SEU_ENDPOINT
        s3Credentials:
          accessKeyId:
            name: cnpg-backup-credentials
            key: ACCESS_KEY_ID
          secretAccessKey:
            name: cnpg-backup-credentials
            key: ACCESS_SECRET_KEY

  superuserSecret:
    name: shared-postgres-superuser
EOF

# 3. Aguardar recovery
kubectl wait --for=condition=Ready cluster/shared-postgres -n cnpg --timeout=600s

Configuração Production

O diretório /git-ops/base/cnpg/ contém a config production real com:

  • Tenant isolation: pg_hba sameuser — cada user só conecta no seu database
  • WAL storage separado: 40Gi dedicado, sem competir com dados
  • Migration job: migra dados de StatefulSets antigos para o CNPG
  • SealedSecrets: credenciais criptografadas no Git
  • Recursos otimizados: 1Gi RAM, shared_buffers=256MB, max_connections=200

Essa config serve como referência pra quando seu projeto crescer.

Troubleshooting

Pods ficam em "Creating" ou "CrashLoopBackOff"

Verifique se o storage class existe:

kubectl get storageclass
# Deve ter hcloud-volumes

Backup falha com "access denied"

Verifique as credenciais S3:

kubectl get secret cnpg-backup-credentials -n cnpg -o jsonpath='{.data.ACCESS_KEY_ID}' | base64 -d
# Deve mostrar o access key correto

Cluster fica em "Replica cluster not healthy"

Verifique se os nodes têm recursos:

kubectl describe nodes | grep -A5 "Allocated resources"

App demo não conecta

Verifique se o service existe:

kubectl get svc shared-postgres-rw -n cnpg

Cleanup

./cleanup.sh

Referências