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)
This commit is contained in:
@@ -27,6 +27,7 @@ App de demonstração: `node-bugado` - trava após N requests para demonstrar he
|
|||||||
| 14 | Istio Traffic Splitting | Hetzner |
|
| 14 | Istio Traffic Splitting | Hetzner |
|
||||||
| 15 | APM: Grafana Tempo + OpenTelemetry | Hetzner |
|
| 15 | APM: Grafana Tempo + OpenTelemetry | Hetzner |
|
||||||
| 16 | Canary Automatizado com Flagger | Hetzner |
|
| 16 | Canary Automatizado com Flagger | Hetzner |
|
||||||
|
| 17 | Cloud Native PostgreSQL (CNPG) | Hetzner |
|
||||||
|
|
||||||
## Comandos Rápidos
|
## Comandos Rápidos
|
||||||
|
|
||||||
@@ -34,7 +35,7 @@ App de demonstração: `node-bugado` - trava após N requests para demonstrar he
|
|||||||
# Aulas 01-06 (Local)
|
# Aulas 01-06 (Local)
|
||||||
cd aula-XX && ./setup.sh # ou kubectl apply -f .
|
cd aula-XX && ./setup.sh # ou kubectl apply -f .
|
||||||
|
|
||||||
# Aulas 07-15 (Hetzner)
|
# Aulas 07-17 (Hetzner)
|
||||||
cd aula-08 && ./setup.sh # Cluster base
|
cd aula-08 && ./setup.sh # Cluster base
|
||||||
cd aula-09 && ./setup.sh # n8n
|
cd aula-09 && ./setup.sh # n8n
|
||||||
cd aula-10 && ./setup.sh # Gitea + Registry + Runner
|
cd aula-10 && ./setup.sh # Gitea + Registry + Runner
|
||||||
@@ -44,6 +45,7 @@ cd aula-13 && ./setup.sh # Container Factory
|
|||||||
cd aula-14 && ./setup.sh # Istio Traffic Splitting
|
cd aula-14 && ./setup.sh # Istio Traffic Splitting
|
||||||
cd aula-15 && ./setup.sh # APM: Tempo + OpenTelemetry
|
cd aula-15 && ./setup.sh # APM: Tempo + OpenTelemetry
|
||||||
cd aula-16 && ./setup.sh # Canary Automatizado com Flagger
|
cd aula-16 && ./setup.sh # Canary Automatizado com Flagger
|
||||||
|
cd aula-17 && ./setup.sh # CNPG - PostgreSQL HA
|
||||||
```
|
```
|
||||||
|
|
||||||
## App node-bugado
|
## App node-bugado
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
.env
|
||||||
@@ -0,0 +1,379 @@
|
|||||||
|
# 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/)
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: pg-demo
|
||||||
|
namespace: cnpg
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: pg-demo
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: pg-demo
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: pg-demo
|
||||||
|
image: postgres:17
|
||||||
|
env:
|
||||||
|
- name: PGHOST
|
||||||
|
value: shared-postgres-rw
|
||||||
|
- name: PGPORT
|
||||||
|
value: "5432"
|
||||||
|
- name: PGUSER
|
||||||
|
value: app
|
||||||
|
- name: PGDATABASE
|
||||||
|
value: app
|
||||||
|
- name: PGPASSWORD
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: shared-postgres-superuser
|
||||||
|
key: password
|
||||||
|
command:
|
||||||
|
- /bin/bash
|
||||||
|
- -c
|
||||||
|
- |
|
||||||
|
echo "=== CNPG Demo App ==="
|
||||||
|
echo "Connecting to $PGHOST:$PGPORT/$PGDATABASE as $PGUSER..."
|
||||||
|
|
||||||
|
until pg_isready -h "$PGHOST" -p "$PGPORT" -U app -q; do
|
||||||
|
echo "Waiting for PostgreSQL..."
|
||||||
|
sleep 2
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "Connected! Creating test table..."
|
||||||
|
psql -c "
|
||||||
|
CREATE TABLE IF NOT EXISTS visits (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
visited_at TIMESTAMP DEFAULT NOW(),
|
||||||
|
source TEXT DEFAULT 'pg-demo'
|
||||||
|
);
|
||||||
|
"
|
||||||
|
|
||||||
|
echo "Inserting visit..."
|
||||||
|
psql -c "INSERT INTO visits (source) VALUES ('pg-demo');"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Recent visits:"
|
||||||
|
psql -c "SELECT * FROM visits ORDER BY id DESC LIMIT 5;"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=== Keeping alive. Press Ctrl+C to stop. ==="
|
||||||
|
echo "Connection is working! Check with: kubectl logs -f deployment/pg-demo -n cnpg"
|
||||||
|
sleep infinity
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: pg-demo
|
||||||
|
namespace: cnpg
|
||||||
|
spec:
|
||||||
|
selector:
|
||||||
|
app: pg-demo
|
||||||
|
ports:
|
||||||
|
- port: 80
|
||||||
|
targetPort: 80
|
||||||
|
type: ClusterIP
|
||||||
Executable
+102
@@ -0,0 +1,102 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# ============================================================================
|
||||||
|
# Aula 17 - Cleanup do CNPG
|
||||||
|
# ============================================================================
|
||||||
|
# Remove o cluster PostgreSQL, app demo, e o CNPG operator.
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
BLUE='\033[0;34m'
|
||||||
|
CYAN='\033[0;36m'
|
||||||
|
NC='\033[0m'
|
||||||
|
|
||||||
|
log_info() { echo -e "${BLUE}[INFO]${NC} $1"; }
|
||||||
|
log_success() { echo -e "${GREEN}[OK]${NC} $1"; }
|
||||||
|
log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
|
||||||
|
log_error() { echo -e "${RED}[ERRO]${NC} $1"; }
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo -e "${CYAN}╔═══════════════════════════════════════════════════════════╗${NC}"
|
||||||
|
echo -e "${CYAN}║ Cleanup - Aula 17 (CNPG) ║${NC}"
|
||||||
|
echo -e "${CYAN}╚═══════════════════════════════════════════════════════════╝${NC}"
|
||||||
|
echo ""
|
||||||
|
echo -e "${YELLOW}Isso vai remover:${NC}"
|
||||||
|
echo " - App demo (cnpg namespace)"
|
||||||
|
echo " - Cluster PostgreSQL shared-postgres"
|
||||||
|
echo " - Secrets (credenciais S3, superuser)"
|
||||||
|
echo " - ScheduledBackup"
|
||||||
|
echo " - Namespace cnpg"
|
||||||
|
echo " - CNPG Operator (cnpg-system namespace)"
|
||||||
|
echo ""
|
||||||
|
echo -e "${YELLOW}NÃO vai remover:${NC}"
|
||||||
|
echo " - Backups no Hetzner Object Storage (permanecem no S3)"
|
||||||
|
echo " - Victoria Metrics (aula-12)"
|
||||||
|
echo " - PVCs (removidos junto com o cluster)"
|
||||||
|
echo ""
|
||||||
|
read -p "Confirmar remoção? (digite 'sim'): " confirm
|
||||||
|
if [[ "$confirm" != "sim" ]]; then
|
||||||
|
echo "Cancelado."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# 1. Remover app demo
|
||||||
|
log_info "Removendo app demo..."
|
||||||
|
kubectl delete deployment pg-demo -n cnpg --ignore-not-found=true 2>/dev/null || true
|
||||||
|
kubectl delete service pg-demo -n cnpg --ignore-not-found=true 2>/dev/null || true
|
||||||
|
log_success "App demo removido"
|
||||||
|
|
||||||
|
# 2. Remover ScheduledBackup
|
||||||
|
log_info "Removendo backup agendado..."
|
||||||
|
kubectl delete scheduledbackup shared-postgres-backup -n cnpg --ignore-not-found=true 2>/dev/null || true
|
||||||
|
log_success "Backup agendado removido"
|
||||||
|
|
||||||
|
# 3. Remover Cluster PostgreSQL (isso também remove PVCs)
|
||||||
|
log_info "Removendo cluster PostgreSQL..."
|
||||||
|
kubectl delete cluster shared-postgres -n cnpg --ignore-not-found=true 2>/dev/null || true
|
||||||
|
log_info "Aguardando pods serem removidos..."
|
||||||
|
kubectl wait --for=delete cluster/shared-postgres -n cnpg --timeout=300s 2>/dev/null || true
|
||||||
|
log_success "Cluster removido"
|
||||||
|
|
||||||
|
# 4. Remover secrets
|
||||||
|
log_info "Removendo secrets..."
|
||||||
|
kubectl delete secret cnpg-backup-credentials -n cnpg --ignore-not-found=true 2>/dev/null || true
|
||||||
|
kubectl delete secret shared-postgres-superuser -n cnpg --ignore-not-found=true 2>/dev/null || true
|
||||||
|
log_success "Secrets removidos"
|
||||||
|
|
||||||
|
# 5. Remover namespace
|
||||||
|
log_info "Removendo namespace cnpg..."
|
||||||
|
kubectl delete namespace cnpg --ignore-not-found=true 2>/dev/null || true
|
||||||
|
log_success "Namespace cnpg removido"
|
||||||
|
|
||||||
|
# 6. Remover CNPG Operator
|
||||||
|
log_info "Removendo CNPG operator..."
|
||||||
|
helm uninstall cnpg -n cnpg-system --wait 2>/dev/null || true
|
||||||
|
kubectl delete namespace cnpg-system --ignore-not-found=true 2>/dev/null || true
|
||||||
|
log_success "CNPG operator removido"
|
||||||
|
|
||||||
|
# 7. Remover CRDs
|
||||||
|
log_info "Removendo CNPG CRDs..."
|
||||||
|
kubectl delete crd clusters.postgresql.cnpg.io backups.postgresql.cnpg.io scheduledbackups.postgresql.cnpg.io poolers.postgresql.cnpg.io imagecatalogs.postgresql.cnpg.io 2>/dev/null || true
|
||||||
|
log_success "CRDs removidos"
|
||||||
|
|
||||||
|
# 8. Remover .env
|
||||||
|
read -p "Remover arquivo .env? [s/N]: " remove_env
|
||||||
|
if [[ "$remove_env" == "s" || "$remove_env" == "S" ]]; then
|
||||||
|
rm -f "${SCRIPT_DIR}/.env"
|
||||||
|
log_success ".env removido"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo -e "${GREEN}Cleanup concluído!${NC}"
|
||||||
|
echo ""
|
||||||
|
echo -e "${YELLOW}Lembrete:${NC} Backups no Hetzner Object Storage NÃO foram removidos."
|
||||||
|
echo " Para remover: acesse o console Hetzner > Object Storage"
|
||||||
|
echo ""
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Secret
|
||||||
|
metadata:
|
||||||
|
name: cnpg-backup-credentials
|
||||||
|
namespace: cnpg
|
||||||
|
type: Opaque
|
||||||
|
stringData:
|
||||||
|
ACCESS_KEY_ID: CHANGE_ACCESS_KEY
|
||||||
|
ACCESS_SECRET_KEY: CHANGE_SECRET_KEY
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
apiVersion: postgresql.cnpg.io/v1
|
||||||
|
kind: Cluster
|
||||||
|
metadata:
|
||||||
|
name: shared-postgres
|
||||||
|
namespace: cnpg
|
||||||
|
spec:
|
||||||
|
imageName: ghcr.io/cloudnative-pg/postgresql:17.2-4
|
||||||
|
instances: 3
|
||||||
|
enableSuperuserAccess: true
|
||||||
|
# ── Topology-aware placement ──
|
||||||
|
# Hetzner CCM labels nodes automatically:
|
||||||
|
# topology.kubernetes.io/zone = fsn1 | nbg1 | hel1 (datacenter)
|
||||||
|
# topology.kubernetes.io/region = eu-central | hel-southeast
|
||||||
|
#
|
||||||
|
# Layer 1: podAntiAffinity on hostname (hard constraint)
|
||||||
|
# → No 2 PG pods on the same node. Ever.
|
||||||
|
#
|
||||||
|
# Layer 2: podAntiAffinity on zone (soft constraint)
|
||||||
|
# → Prefer spreading across datacenters. With nodes in fsn1, nbg1, hel1,
|
||||||
|
# you get 1 pod per DC. With only eu-central nodes, they still spread
|
||||||
|
# across FSN and NBG (both in eu-central, ~1-2ms apart).
|
||||||
|
#
|
||||||
|
# Prerequisite for cross-region (eu-central + HEL):
|
||||||
|
# - Node pool in eu-central (FSN + NBG) → aula-08
|
||||||
|
# - Node pool in hel-southeast (HEL) → add worker after aula-08
|
||||||
|
# - Without HEL nodes, all 3 pods stay in eu-central (still HA within region)
|
||||||
|
affinity:
|
||||||
|
topologyKey: kubernetes.io/hostname
|
||||||
|
podAntiAffinityType: required
|
||||||
|
podAntiAffinity:
|
||||||
|
preferredDuringSchedulingIgnoredDuringExecution:
|
||||||
|
- weight: 100
|
||||||
|
podAffinityTerm:
|
||||||
|
topologyKey: topology.kubernetes.io/zone
|
||||||
|
labelSelector:
|
||||||
|
matchLabels:
|
||||||
|
cnpg.io/cluster: shared-postgres
|
||||||
|
storage:
|
||||||
|
size: 10Gi
|
||||||
|
storageClass: hcloud-volumes
|
||||||
|
walStorage:
|
||||||
|
size: 5Gi
|
||||||
|
storageClass: hcloud-volumes
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: 500m
|
||||||
|
memory: 1Gi
|
||||||
|
limits:
|
||||||
|
cpu: "1"
|
||||||
|
memory: 1Gi
|
||||||
|
postgresql:
|
||||||
|
parameters:
|
||||||
|
max_connections: "100"
|
||||||
|
shared_buffers: "256MB"
|
||||||
|
effective_cache_size: "512MB"
|
||||||
|
work_mem: "4MB"
|
||||||
|
maintenance_work_mem: "128MB"
|
||||||
|
log_min_duration_statement: "1000"
|
||||||
|
bootstrap:
|
||||||
|
initdb:
|
||||||
|
database: app
|
||||||
|
owner: app
|
||||||
|
secret:
|
||||||
|
name: shared-postgres-superuser
|
||||||
|
superuserSecret:
|
||||||
|
name: shared-postgres-superuser
|
||||||
|
backup:
|
||||||
|
barmanObjectStore:
|
||||||
|
destinationPath: s3://CHANGE_BUCKET_NAME/cnpg-backups/shared-postgres
|
||||||
|
endpointURL: https://CHANGE_ENDPOINT
|
||||||
|
s3Credentials:
|
||||||
|
accessKeyId:
|
||||||
|
name: cnpg-backup-credentials
|
||||||
|
key: ACCESS_KEY_ID
|
||||||
|
secretAccessKey:
|
||||||
|
name: cnpg-backup-credentials
|
||||||
|
key: ACCESS_SECRET_KEY
|
||||||
|
wal:
|
||||||
|
compression: gzip
|
||||||
|
data:
|
||||||
|
compression: gzip
|
||||||
|
jobs: 2
|
||||||
|
retentionPolicy: "30d"
|
||||||
|
monitoring:
|
||||||
|
enablePodMonitor: true
|
||||||
|
startDelay: 3600
|
||||||
|
livenessProbeTimeout: 90
|
||||||
|
primaryUpdateStrategy: unsupervised
|
||||||
|
primaryUpdateMethod: switchover
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
apiVersion: postgresql.cnpg.io/v1
|
||||||
|
kind: ScheduledBackup
|
||||||
|
metadata:
|
||||||
|
name: shared-postgres-backup
|
||||||
|
namespace: cnpg
|
||||||
|
spec:
|
||||||
|
cluster:
|
||||||
|
name: shared-postgres
|
||||||
|
schedule: "0 3 * * *"
|
||||||
|
backupOwnerReference: cluster
|
||||||
|
method: barmanObjectStore
|
||||||
Executable
+346
@@ -0,0 +1,346 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# ============================================================================
|
||||||
|
# Aula 17 - Cloud Native PostgreSQL (CNPG)
|
||||||
|
# ============================================================================
|
||||||
|
# Instala o CloudNativePG operator e cria um cluster PostgreSQL HA
|
||||||
|
# com backup automático para Hetzner Object Storage (S3).
|
||||||
|
#
|
||||||
|
# Componentes:
|
||||||
|
# - CNPG Operator (gerenciamento do ciclo de vida do PostgreSQL)
|
||||||
|
# - Cluster PostgreSQL (3 instâncias: 1 primary + 1 sync + 1 async)
|
||||||
|
# - Backup S3 (WAL archiving + base backup diário)
|
||||||
|
# - App demo (valida conexão com o banco)
|
||||||
|
#
|
||||||
|
# Pré-requisitos:
|
||||||
|
# - Cluster Kubernetes na Hetzner (aula-08)
|
||||||
|
# - Victoria Metrics (aula-12) para métricas
|
||||||
|
# - Hetzner Object Storage com bucket criado
|
||||||
|
# - kubectl e helm instalados
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
BLUE='\033[0;34m'
|
||||||
|
CYAN='\033[0;36m'
|
||||||
|
NC='\033[0m'
|
||||||
|
|
||||||
|
log_info() { echo -e "${BLUE}[INFO]${NC} $1"; }
|
||||||
|
log_success() { echo -e "${GREEN}[OK]${NC} $1"; }
|
||||||
|
log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
|
||||||
|
log_error() { echo -e "${RED}[ERRO]${NC} $1"; }
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
ENV_FILE="${SCRIPT_DIR}/.env"
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Gerenciamento de Configuração
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
load_config() {
|
||||||
|
if [[ -f "$ENV_FILE" ]]; then
|
||||||
|
source "$ENV_FILE"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
save_config() {
|
||||||
|
cat > "$ENV_FILE" << EOF
|
||||||
|
# Configuração da Aula 17 - CNPG
|
||||||
|
# Gerado em: $(date)
|
||||||
|
|
||||||
|
S3_ACCESS_KEY=${S3_ACCESS_KEY}
|
||||||
|
S3_SECRET_KEY=${S3_SECRET_KEY}
|
||||||
|
S3_ENDPOINT=${S3_ENDPOINT}
|
||||||
|
S3_BUCKET=${S3_BUCKET}
|
||||||
|
EOF
|
||||||
|
log_success "Configuração salva em .env"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Verificação de Pré-requisitos
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
check_prerequisites() {
|
||||||
|
echo ""
|
||||||
|
log_info "Verificando pré-requisitos..."
|
||||||
|
|
||||||
|
local failed=false
|
||||||
|
|
||||||
|
for cmd in kubectl helm; do
|
||||||
|
if command -v "$cmd" &> /dev/null; then
|
||||||
|
log_success "$cmd encontrado"
|
||||||
|
else
|
||||||
|
log_error "$cmd não encontrado"
|
||||||
|
failed=true
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if ! kubectl cluster-info &> /dev/null; then
|
||||||
|
log_error "Cluster Kubernetes não acessível. Execute a aula-08 primeiro."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
log_success "Cluster Kubernetes acessível"
|
||||||
|
|
||||||
|
# Verificar Victoria Metrics
|
||||||
|
if kubectl get svc -n monitoring vmsingle-monitoring-victoria-metrics-k8s-stack &> /dev/null; then
|
||||||
|
log_success "Victoria Metrics encontrado"
|
||||||
|
else
|
||||||
|
log_warn "Victoria Metrics não encontrado (aula-12). PodMonitor não terá scrape target."
|
||||||
|
log_warn "O CNPG funcionará sem métricas, mas recomenda-se instalar a aula-12."
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$failed" == "true" ]]; then
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Coleta de Configuração
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
collect_config() {
|
||||||
|
echo ""
|
||||||
|
echo -e "${CYAN}═══════════════════════════════════════════════════════════${NC}"
|
||||||
|
echo -e "${CYAN} Configuração do CNPG - Hetzner Object Storage${NC}"
|
||||||
|
echo -e "${CYAN}═══════════════════════════════════════════════════════════${NC}"
|
||||||
|
echo ""
|
||||||
|
echo -e "Você precisa de um bucket no Hetzner Object Storage."
|
||||||
|
echo -e "Crie em: ${GREEN}https://console.hetzner.cloud/storage/object${NC}"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
if load_config; then
|
||||||
|
echo -e "Configuração existente encontrada:"
|
||||||
|
echo -e " Endpoint: ${GREEN}${S3_ENDPOINT}${NC}"
|
||||||
|
echo -e " Bucket: ${GREEN}${S3_BUCKET}${NC}"
|
||||||
|
echo ""
|
||||||
|
echo -e "[1] Usar configuração existente"
|
||||||
|
echo -e "[2] Inserir nova configuração"
|
||||||
|
read -p "Escolha [1/2]: " choice
|
||||||
|
if [[ "$choice" == "1" ]]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo -e "${YELLOW}Endpoints disponíveis:${NC}"
|
||||||
|
echo " fsn1.your-objectstorage.com (Frankfurt)"
|
||||||
|
echo " nbg1.your-objectstorage.com (Nuremberg)"
|
||||||
|
echo " hel1.your-objectstorage.com (Helsinki)"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
if [[ -z "$S3_ENDPOINT" ]]; then
|
||||||
|
S3_ENDPOINT="nbg1.your-objectstorage.com"
|
||||||
|
fi
|
||||||
|
echo -ne "Endpoint (${GREEN}${S3_ENDPOINT}${NC}): "
|
||||||
|
read -r input
|
||||||
|
[[ -n "$input" ]] && S3_ENDPOINT="$input"
|
||||||
|
|
||||||
|
if [[ -z "$S3_BUCKET" ]]; then
|
||||||
|
S3_BUCKET="cnpg-backups"
|
||||||
|
fi
|
||||||
|
echo -ne "Bucket name (${GREEN}${S3_BUCKET}${NC}): "
|
||||||
|
read -r input
|
||||||
|
[[ -n "$input" ]] && S3_BUCKET="$input"
|
||||||
|
|
||||||
|
echo -ne "Access Key ID: "
|
||||||
|
read -r S3_ACCESS_KEY
|
||||||
|
if [[ -z "$S3_ACCESS_KEY" ]]; then
|
||||||
|
log_error "Access Key é obrigatória"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo -ne "Secret Access Key: "
|
||||||
|
read -rs S3_SECRET_KEY
|
||||||
|
echo ""
|
||||||
|
if [[ -z "$S3_SECRET_KEY" ]]; then
|
||||||
|
log_error "Secret Key é obrigatória"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
save_config
|
||||||
|
}
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Instalação do CNPG Operator
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
install_cnpg_operator() {
|
||||||
|
echo ""
|
||||||
|
echo -e "${CYAN}═══════════════════════════════════════════════════════════${NC}"
|
||||||
|
echo -e "${CYAN} Instalando CNPG Operator${NC}"
|
||||||
|
echo -e "${CYAN}═══════════════════════════════════════════════════════════${NC}"
|
||||||
|
|
||||||
|
helm repo add cnpg https://cloudnative-pg.github.io/charts 2>/dev/null || true
|
||||||
|
helm repo update cnpg
|
||||||
|
|
||||||
|
log_info "Instalando CNPG operator no namespace cnpg-system..."
|
||||||
|
if helm status cnpg -n cnpg-system &> /dev/null; then
|
||||||
|
helm upgrade cnpg cnpg/cloudnative-pg \
|
||||||
|
-n cnpg-system \
|
||||||
|
--wait
|
||||||
|
else
|
||||||
|
helm install cnpg cnpg/cloudnative-pg \
|
||||||
|
-n cnpg-system \
|
||||||
|
--create-namespace \
|
||||||
|
--wait
|
||||||
|
fi
|
||||||
|
log_success "CNPG operator instalado"
|
||||||
|
|
||||||
|
log_info "Aguardando operator ficar ready..."
|
||||||
|
kubectl wait --for=condition=available deployment/cnpg-controller-manager -n cnpg-system --timeout=120s
|
||||||
|
log_success "CNPG operator pronto"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Criar Namespace e Secrets
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
setup_namespace_and_secrets() {
|
||||||
|
echo ""
|
||||||
|
echo -e "${CYAN}═══════════════════════════════════════════════════════════${NC}"
|
||||||
|
echo -e "${CYAN} Criando namespace e secrets${NC}"
|
||||||
|
echo -e "${CYAN}═══════════════════════════════════════════════════════════${NC}"
|
||||||
|
|
||||||
|
kubectl create namespace cnpg --dry-run=client -o yaml | kubectl apply -f -
|
||||||
|
|
||||||
|
log_info "Criando secret de credenciais S3..."
|
||||||
|
kubectl create secret generic cnpg-backup-credentials \
|
||||||
|
-n cnpg \
|
||||||
|
--from-literal=ACCESS_KEY_ID="$S3_ACCESS_KEY" \
|
||||||
|
--from-literal=ACCESS_SECRET_KEY="$S3_SECRET_KEY" \
|
||||||
|
--dry-run=client -o yaml | kubectl apply -f -
|
||||||
|
log_success "Secret criado"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Criar Cluster PostgreSQL
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
create_cluster() {
|
||||||
|
echo ""
|
||||||
|
echo -e "${CYAN}═══════════════════════════════════════════════════════════${NC}"
|
||||||
|
echo -e "${CYAN} Criando Cluster PostgreSQL${NC}"
|
||||||
|
echo -e "${CYAN}═══════════════════════════════════════════════════════════${NC}"
|
||||||
|
|
||||||
|
log_info "Gerando cluster.yaml com suas configurações..."
|
||||||
|
sed -e "s|CHANGE_BUCKET_NAME|${S3_BUCKET}|g" \
|
||||||
|
-e "s|CHANGE_ENDPOINT|https://${S3_ENDPOINT}|g" \
|
||||||
|
"${SCRIPT_DIR}/cnpg/cluster.yaml" | kubectl apply -f -
|
||||||
|
|
||||||
|
log_success "Cluster aplicado"
|
||||||
|
|
||||||
|
log_info "Aplicando backup agendado..."
|
||||||
|
kubectl apply -f "${SCRIPT_DIR}/cnpg/scheduled-backup.yaml"
|
||||||
|
log_success "Backup agendado aplicado"
|
||||||
|
|
||||||
|
log_info "Aguardando cluster ficar ready (pode levar 2-5 minutos)..."
|
||||||
|
kubectl wait --for=condition=Ready cluster/shared-postgres -n cnpg --timeout=600s
|
||||||
|
log_success "Cluster PostgreSQL pronto!"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Deploy App Demo
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
deploy_demo() {
|
||||||
|
echo ""
|
||||||
|
echo -e "${CYAN}═══════════════════════════════════════════════════════════${NC}"
|
||||||
|
echo -e "${CYAN} Deploy App Demo${NC}"
|
||||||
|
echo -e "${CYAN}═══════════════════════════════════════════════════════════${NC}"
|
||||||
|
|
||||||
|
log_info "Deployando app de demonstração..."
|
||||||
|
kubectl apply -f "${SCRIPT_DIR}/app-demo/deployment.yaml"
|
||||||
|
kubectl apply -f "${SCRIPT_DIR}/app-demo/service.yaml"
|
||||||
|
|
||||||
|
log_info "Aguardando app demo..."
|
||||||
|
kubectl wait --for=condition=available deployment/pg-demo -n cnpg --timeout=120s 2>/dev/null || true
|
||||||
|
|
||||||
|
# Mostrar resultado
|
||||||
|
sleep 3
|
||||||
|
echo ""
|
||||||
|
log_info "Resultado da conexão do app demo:"
|
||||||
|
kubectl logs deployment/pg-demo -n cnpg --tail=20 2>/dev/null || log_warn "Aguardando logs..."
|
||||||
|
log_success "App demo deployado"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Resumo
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
show_summary() {
|
||||||
|
echo ""
|
||||||
|
echo -e "${CYAN}═══════════════════════════════════════════════════════════${NC}"
|
||||||
|
echo -e "${CYAN} Instalação Concluída${NC}"
|
||||||
|
echo -e "${CYAN}═══════════════════════════════════════════════════════════${NC}"
|
||||||
|
echo ""
|
||||||
|
echo -e "${GREEN}Componentes instalados:${NC}"
|
||||||
|
echo " - CNPG Operator (namespace: cnpg-system)"
|
||||||
|
echo " - Cluster PostgreSQL 3 instâncias (namespace: cnpg)"
|
||||||
|
echo " - Backup diário às 03:00 → Hetzner Object Storage"
|
||||||
|
echo " - App demo (valida conexão)"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Status do cluster
|
||||||
|
echo -e "${GREEN}Status do cluster:${NC}"
|
||||||
|
kubectl get cluster -n cnpg
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo -e "${GREEN}Instâncias:${NC}"
|
||||||
|
kubectl get pods -n cnpg -l cnpg.io/cluster=shared-postgres
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo -e "${GREEN}Serviços criados pelo CNPG:${NC}"
|
||||||
|
kubectl get svc -n cnpg -l cnpg.io/cluster=shared-postgres
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo -e "${GREEN}Comandos essenciais:${NC}"
|
||||||
|
echo ""
|
||||||
|
echo " # Status do cluster"
|
||||||
|
echo " kubectl get cluster -n cnpg"
|
||||||
|
echo ""
|
||||||
|
echo " # Ver instâncias (primary vs replica)"
|
||||||
|
echo " kubectl get pods -n cnpg -l cnpg.io/cluster=shared-postgres -o wide"
|
||||||
|
echo ""
|
||||||
|
echo " # Failover manual (promover replica)"
|
||||||
|
echo " kubectl cnpg promote shared-postgres <instance-name> -n cnpg"
|
||||||
|
echo ""
|
||||||
|
echo " # Trigger backup manual"
|
||||||
|
echo " kubectl cnpg backup shared-postgres -n cnpg"
|
||||||
|
echo ""
|
||||||
|
echo " # Listar backups"
|
||||||
|
echo " kubectl get backups -n cnpg"
|
||||||
|
echo ""
|
||||||
|
echo " # Logs do app demo"
|
||||||
|
echo " kubectl logs -f deployment/pg-demo -n cnpg"
|
||||||
|
echo ""
|
||||||
|
echo -e "${YELLOW}Próximos passos:${NC}"
|
||||||
|
echo " - Conecte sua app ao service: shared-postgres-rw.cnpg.svc:5432"
|
||||||
|
echo " - O backup automático roda às 03:00 diariamente"
|
||||||
|
echo " - Para recovery/PITR, consulte o README"
|
||||||
|
echo ""
|
||||||
|
}
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Execução
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
main() {
|
||||||
|
echo ""
|
||||||
|
echo -e "${CYAN}╔═══════════════════════════════════════════════════════════╗${NC}"
|
||||||
|
echo -e "${CYAN}║ Aula 17 - Cloud Native PostgreSQL (CNPG) ║${NC}"
|
||||||
|
echo -e "${CYAN}╚═══════════════════════════════════════════════════════════╝${NC}"
|
||||||
|
|
||||||
|
check_prerequisites
|
||||||
|
collect_config
|
||||||
|
install_cnpg_operator
|
||||||
|
setup_namespace_and_secrets
|
||||||
|
create_cluster
|
||||||
|
deploy_demo
|
||||||
|
show_summary
|
||||||
|
}
|
||||||
|
|
||||||
|
main "$@"
|
||||||
Reference in New Issue
Block a user