384 lines
16 KiB
Markdown
384 lines
16 KiB
Markdown
# 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
|
||
|
||
```bash
|
||
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:
|
||
|
||
```bash
|
||
# 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
|
||
|
||
```bash
|
||
# 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:
|
||
|
||
```bash
|
||
# 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
|
||
|
||
```bash
|
||
# 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:
|
||
|
||
```bash
|
||
# 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:
|
||
```bash
|
||
kubectl get storageclass
|
||
# Deve ter hcloud-volumes
|
||
```
|
||
|
||
### Backup falha com "access denied"
|
||
|
||
Verifique as credenciais S3:
|
||
```bash
|
||
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:
|
||
```bash
|
||
kubectl describe nodes | grep -A5 "Allocated resources"
|
||
```
|
||
|
||
### App demo não conecta
|
||
|
||
Verifique se o service existe:
|
||
```bash
|
||
kubectl get svc shared-postgres-rw -n cnpg
|
||
```
|
||
|
||
## Cleanup
|
||
|
||
```bash
|
||
./cleanup.sh
|
||
```
|
||
|
||
## Referências
|
||
|
||
- [CloudNativePG Documentation](https://cloudnative-pg.io/docs/)
|
||
- [CNPG Backup & Recovery](https://cloudnative-pg.io/docs/backup_recovery/)
|
||
- [Hetzner Object Storage](https://www.hetzner.com/storage/object-storage/)
|
||
- [CNPG CNCF Sandbox Announcement](https://www.cncf.io/projects/cloudnativepg/)
|
||
- [Comparing Kubernetes Operators for PostgreSQL (Palark)](https://palark.com/blog/comparing-kubernetes-operators-for-postgresql/)
|