feat(plataforma): Postgres compartilhado, cota e rede (P4.7)

This commit is contained in:
deploy
2026-09-19 16:19:14 +00:00
parent 37eb1a5cf9
commit cfa3bbe8f6
5 changed files with 268 additions and 5 deletions
@@ -33,12 +33,12 @@ spec:
# comprometidos, o que e pratica normal. O que reserva de verdade e
# `requests`, e ai a conta importa: o no tem 8 CPU e 32Gi, com 74% da CPU
# e 72% da memoria ja reservados. Os 8 pods pedem ~2,1Gi.
requests.cpu: "2"
requests.memory: 3Gi
limits.cpu: "8"
limits.memory: 8Gi
requests.cpu: "3"
requests.memory: 4Gi
limits.cpu: "10"
limits.memory: 11Gi
pods: "20"
persistentvolumeclaims: "4"
persistentvolumeclaims: "6"
---
# Defaults de recursos por container (para a cota ser satisfeita sem declarar em cada pod)
apiVersion: v1
@@ -0,0 +1,15 @@
---
apiVersion: bitnami.com/v1alpha1
kind: SealedSecret
metadata:
creationTimestamp: null
name: pg-compartilhado-admin
namespace: plataforma-prod
spec:
encryptedData:
password: AgCFUw1peC9QxgbJfFVVS03DrHRLGPzT7yykUyIZX2JAesslRJEkOHxMPKyYfF2xIC17lkg6VnkfAtKcWhbNLrprDeJCVcYIx4xlpxaMyGqzQrq10Y1lL61xcdRIAtDUDg2peUrdO6geX4iyGbDeTcHe58TYHb5zQvREY/hyR2fdlJ9rjgdT+5x8X0a+Tyn0G3KuQfKAS0PocC88pGvwKGCLC7ZJXlTS3DdWD7hWKDpB6nXJ8RA44Vc8cy4JhrLqiX0jI/16fPg3BaVP4pmzF1DJbaBHh6ttcvYYfgcOYHFQWjJj7GCa6OBDc/DX3P4mdwfkNRaff0Ai+8Flr12zwBBOK77134I73+ojHemOVXQSgIl+6Ba3qeEt3dbCsOsitDoToicA6Cq0Bhplrc2i57wxwhbiH4VxUX/llODMZJ5AcOhz7lAsMPapjAAcxZsM/KWLpCVuU+iTkh/WGEhK1keC/PAeEt1EFYlhWvPKnPx3KclJRF4zTTg4I3THOCgsecdJ8ad0NO7qU83KBPGlxZGdbEDxrSzC5YAOUJqRSgTmVnut2wlgd0ifBvpXoJ3Uvr53o3vTfA+YPhgVMhiOqvoD/JYdW1Q7JO+CGZywJl7zMheL12eZWulWAbFYDRttg2he0V6AkxowoYzYT5yBl253F5m2MhQzknrPQ4K+X5A2kQePG8zfSDzkwC35OB+4w2nXbGN/BOBORKiP6mpHtu1/d22Q88xmch1a
template:
metadata:
creationTimestamp: null
name: pg-compartilhado-admin
namespace: plataforma-prod
@@ -0,0 +1,146 @@
# =============================================================================
# P4.7 — UMA instancia de Postgres para todos os tenants, com PgBouncer na
# frente.
#
# Hoje cada tenant tem o proprio pod de Postgres. Sao cinco instancias servindo
# bancos de poucos megabytes cada, e cada uma paga um processo, um volume e um
# ciclo de backup proprio.
#
# O modelo alvo ja estava decidido e preparado em P2.1: um DATABASE e um ROLE por
# tenant na mesma instancia (`athleticmap_<tenant>`, `r_<tenant>`), com schema por
# contexto DENTRO do database. Sao duas fronteiras diferentes — schema separa
# CONTEXTO, database separa CLIENTE (ADR-0002 §2).
#
# ── O QUE FOI VERIFICADO LOCALMENTE, E O QUE NAO FOI ─────────────────────────
#
# Verificado em 2026-09-18, com Postgres 16 + PgBouncer 1.25 em containers, dois
# tenants provisionados pelos scripts de P2.1:
#
# 1. role de um tenant no database de outro -> "permission denied" ✔
# 2. role certo com a senha do vizinho -> "SASL authentication failed" ✔
# 3. role certo no proprio database -> conecta ✔
# 4. enumeracao de tenants pelo catalogo -> enxerga 1, o proprio ✔
# 5. pools do PgBouncer -> um por (database, role),
# SEM mistura de credencial ✔
#
# TUDO ISSO ATRAVES DO PGBOUNCER, que e o ponto: transaction pooling reaproveita
# conexao de servidor entre clientes, e um pool compartilhado entre roles faria
# um tenant herdar a sessao do outro.
#
# NAO verificado: comportamento sob carga real, e a migracao dos dados dos
# tenants que ja existem. Os dois dependem de deploy.
#
# ── O DIMENSIONAMENTO, QUE E CONTA E NAO CHUTE ───────────────────────────────
#
# Alvo do card: ~18 tenants.
#
# por tenant: 1 database, 1 role, 1 pool
# por pool: default_pool_size 10 + reserve_pool_size 5 = ate 15
# 18 tenants: 18 x 15 = 270 conexoes de servidor no pior caso
# + superusuario: 3 reservadas (superuser_reserved_connections)
# + folga: ~27
# -----------------------------------------------------------------
# max_connections: 300
#
# `shared_buffers` em 512Mi = 25% do limite de 2Gi, que e a regra de bolso do
# proprio Postgres. `effective_cache_size` em 1536Mi (75%) diz ao planejador
# quanta pagina ele pode esperar encontrar em cache — nao reserva memoria, so
# muda a escolha entre varredura sequencial e indice.
#
# `work_mem` em 4MB, e nao mais: ele e POR OPERACAO de ordenacao, nao por
# conexao. Com 300 conexoes e consultas com dois sorts, 16MB viraria 9,6 GB no
# pior caso — e o limite do pod e 2Gi.
# =============================================================================
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: pg-compartilhado
namespace: plataforma-prod
spec:
accessModes: [ReadWriteOnce]
storageClassName: local-path
resources:
requests:
storage: 50Gi
---
apiVersion: v1
kind: ConfigMap
metadata:
name: pg-compartilhado-conf
namespace: plataforma-prod
data:
# Passados por `-c` no comando do postgres, e nao por arquivo: o entrypoint da
# imagem oficial reescreve o postgresql.conf na primeira subida, e um arquivo
# montado seria ignorado ou sobrescrito, sem aviso.
parametros: >-
-c max_connections=300
-c superuser_reserved_connections=3
-c shared_buffers=512MB
-c effective_cache_size=1536MB
-c work_mem=4MB
-c maintenance_work_mem=128MB
-c wal_compression=on
-c log_min_duration_statement=1000
-c log_connections=off
-c log_disconnections=off
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: pg-compartilhado
namespace: plataforma-prod
spec:
replicas: 1
strategy:
# `Recreate`, e nao RollingUpdate: sao dois processos disputando o MESMO
# PersistentVolume. O segundo nao sobe — o Postgres recusa iniciar sobre um
# diretorio ja travado — e o rollout fica preso com o pod novo em erro e o
# velho ainda de pe, ate alguem olhar.
type: Recreate
selector:
matchLabels: { app: pg-compartilhado }
template:
metadata:
labels: { app: pg-compartilhado }
spec:
containers:
- name: postgres
image: postgres:16
args: ["postgres"]
command: ["sh", "-c", "exec docker-entrypoint.sh postgres $PARAMETROS"]
env:
- name: PARAMETROS
valueFrom:
configMapKeyRef: { name: pg-compartilhado-conf, key: parametros }
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef: { name: pg-compartilhado-admin, key: password }
# `PGDATA` num subdiretorio: o local-path monta o volume na raiz, e o
# Postgres recusa inicializar num diretorio que ja tem `lost+found`.
- { name: PGDATA, value: /var/lib/postgresql/data/pgdata }
ports: [{ containerPort: 5432 }]
volumeMounts:
- { name: dados, mountPath: /var/lib/postgresql/data }
resources:
requests: { cpu: 500m, memory: 1Gi }
limits: { cpu: "2", memory: 2Gi }
readinessProbe:
exec: { command: ["pg_isready", "-U", "postgres"] }
initialDelaySeconds: 10
periodSeconds: 10
livenessProbe:
exec: { command: ["pg_isready", "-U", "postgres"] }
initialDelaySeconds: 60
periodSeconds: 30
volumes:
- name: dados
persistentVolumeClaim: { claimName: pg-compartilhado }
---
apiVersion: v1
kind: Service
metadata:
name: pg-compartilhado
namespace: plataforma-prod
spec:
selector: { app: pg-compartilhado }
ports: [{ port: 5432, targetPort: 5432 }]
@@ -0,0 +1,71 @@
# =============================================================================
# P4.7 — egress do tenant para o banco compartilhado.
#
# Com o Postgres saindo do namespace do tenant e indo para o `plataforma-prod`,
# os runtimes passam a falar com um endereco de OUTRO namespace. Sem esta
# politica, a conexao morre — e o sintoma e o pod ficando em CrashLoopBackOff
# com "connection refused", que parece problema de banco e e de rede.
#
# ── ESTE ARQUIVO E O MOLDE, E PRECISA SER COPIADO PARA CADA TENANT ───────────
#
# Ele vive aqui porque `plataforma` e o primeiro a usar o banco compartilhado.
# Cada tenant que migrar (cards 158 e 159) precisa da SUA copia, com o proprio
# `namespace:` — NetworkPolicy nao atravessa namespace.
#
# ── UMA COISA QUE ESTA POLITICA NAO CONSERTA, E DEVIA ────────────────────────
#
# O `deny-cross-tenant` de `00-namespace-quota-netpol.yaml` libera egress para
# `ipBlock: 10.43.0.0/16` — a CIDR de Services do cluster INTEIRO.
#
# Isso e mais largo do que o nome da politica sugere: hoje um pod de qualquer
# tenant alcanca, pela rede, o ClusterIP do Postgres de qualquer outro. O que
# impede o acesso e a autenticacao do Postgres (P2.1), e nao a rede.
#
# Com banco compartilhado isso passa a importar mais, porque o alvo deixa de ser
# "o postgres do vizinho" e vira "a instancia de todo mundo".
#
# **Nao estreitei aquela regra aqui**, e a razao e honesta: o comentario original
# diz que ela existe para alcançar "ClusterIPs (VIP de service, pre-DNAT)", e
# retira-la pode quebrar o acesso a Services DENTRO do proprio namespace,
# dependendo de como o CNI avalia a politica em relacao ao DNAT do kube-proxy.
# Isso precisa ser testado no cluster, e um erro aqui derruba o tenant inteiro.
# Esta registrado no diario como pendencia propria.
# =============================================================================
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: egress-banco-compartilhado
namespace: plataforma-prod
spec:
# Vale para todos os pods do tenant. Poderia ser so os runtimes, mas o backup
# e os CronJobs tambem falam com o banco, e uma lista de `app:` daria manutencao
# a cada pod novo — com a falha aparecendo so quando o pod novo tentasse
# conectar.
podSelector: {}
policyTypes: [Egress]
egress:
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: plataforma-prod
podSelector:
matchLabels:
app: pgbouncer
ports:
- { protocol: TCP, port: 6432 }
# 5432 direto no Postgres, para o CronJob de backup.
#
# `pg_dump` abre uma conexao longa e faz uma transacao unica — exatamente o
# que o transaction pooling NAO serve. Pelo bouncer, um dump grande seguraria
# uma conexao de servidor do pool inteiro pelo tempo do backup, e os outros
# clientes daquele tenant esperariam.
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: plataforma-prod
podSelector:
matchLabels:
app: pg-compartilhado
ports:
- { protocol: TCP, port: 5432 }
@@ -0,0 +1,31 @@
# =============================================================================
# P4.7 — os tenants CHEGAM ao PgBouncer.
#
# A `deny-cross-tenant` deste namespace (00-namespace-quota-netpol.yaml) so
# aceita entrada do proprio namespace e do kube-system. Sem esta politica, um
# servico do `demo-prod` apontado para `pgbouncer.plataforma-prod:6432` teria a
# conexao recusada na rede — e o sintoma seria o pod em CrashLoop com
# "connection refused", sem nada dizendo que foi a NetworkPolicy.
#
# Politicas de rede SOMAM: esta abre so a porta 6432, so no pod do PgBouncer, e
# so para os namespaces listados. O Postgres (5432) continua fechado para fora:
# os tenants passam SEMPRE pelo bouncer, onde cada um tem pool e role proprios.
#
# Um tenant por linha, como a lista de databases do 13-pgbouncer.yaml. Tenant
# migrado e nao listado aqui simplesmente nao conecta.
# =============================================================================
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: tenants-no-pgbouncer
namespace: plataforma-prod
spec:
podSelector:
matchLabels: { app: pgbouncer }
policyTypes: [Ingress]
ingress:
- from:
- namespaceSelector:
matchLabels: { kubernetes.io/metadata.name: demo-prod }
ports:
- { protocol: TCP, port: 6432 }