- Cluster PostgreSQL 3 instâncias com anti-affinity por hostname (hard) e por zona (soft) para spread entre datacenters Hetzner - Backup automático via Barman Cloud para Hetzner Object Storage (S3) - ScheduledBackup diário às 03:00 - App demo que valida conexão com o banco - setup.sh: instala operator, cria cluster, deploya demo - cleanup.sh: remoção limpa (backups S3 preservados)
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× CPX31 |
| 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 | |
| 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?
- CPX31: 4 vCPU AMD, 8GB RAM, 160GB NVMe → ~€12/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)
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?
- Menos recursos = custo menor. StackGres consome 3-5× mais RAM em sidecars. Num CPX31 com 8GB, isso faz diferença.
- Setup mais simples. Um Cluster CRD resolve. StackGres precisa de SGCluster, SGPostgresConfig, SGPgBouncerConfig, SGBackupConfig, SGPoolingConfig...
- CNCF Sandbox. Governança aberta, roadmap transparente, não depende de uma empresa.
- 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:
- Node pool eu-central (aula-08): Control plane + workers em FSN/NBG
- 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:
- Verificar pré-requisitos
- Pedir as credenciais do Hetzner Object Storage
- Instalar o CNPG operator
- Criar o cluster PostgreSQL (3 instâncias)
- Configurar backup automático
- 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