Files
workshop/aula-17/README.md
T
ArgoCD Setup 17a2891329 feat(aula-17): adicionar Cloud Native PostgreSQL (CNPG)
- 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)
2026-05-09 02:14:22 -03:00

380 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 ~€36/mês (~$40) |
| 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?
- 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?
1. **Menos recursos = custo menor**. StackGres consome 3-5× mais RAM em sidecars. Num CPX31 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/)