From cfa3bbe8f6db12a38b37631f119767737009d315 Mon Sep 17 00:00:00 2001 From: deploy Date: Sat, 19 Sep 2026 16:19:14 +0000 Subject: [PATCH] feat(plataforma): Postgres compartilhado, cota e rede (P4.7) --- .../plataforma/00-namespace-quota-netpol.yaml | 10 +- .../09-sealed-pg-compartilhado-admin.yaml | 15 ++ .../plataforma/12-postgres-compartilhado.yaml | 146 ++++++++++++++++++ .../14-netpol-banco-compartilhado.yaml | 71 +++++++++ .../15-netpol-tenants-no-pgbouncer.yaml | 31 ++++ 5 files changed, 268 insertions(+), 5 deletions(-) create mode 100644 tenants/plataforma/09-sealed-pg-compartilhado-admin.yaml create mode 100644 tenants/plataforma/12-postgres-compartilhado.yaml create mode 100644 tenants/plataforma/14-netpol-banco-compartilhado.yaml create mode 100644 tenants/plataforma/15-netpol-tenants-no-pgbouncer.yaml diff --git a/tenants/plataforma/00-namespace-quota-netpol.yaml b/tenants/plataforma/00-namespace-quota-netpol.yaml index 2ac78f4..4d562ad 100644 --- a/tenants/plataforma/00-namespace-quota-netpol.yaml +++ b/tenants/plataforma/00-namespace-quota-netpol.yaml @@ -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 diff --git a/tenants/plataforma/09-sealed-pg-compartilhado-admin.yaml b/tenants/plataforma/09-sealed-pg-compartilhado-admin.yaml new file mode 100644 index 0000000..548f954 --- /dev/null +++ b/tenants/plataforma/09-sealed-pg-compartilhado-admin.yaml @@ -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 diff --git a/tenants/plataforma/12-postgres-compartilhado.yaml b/tenants/plataforma/12-postgres-compartilhado.yaml new file mode 100644 index 0000000..4cfb23e --- /dev/null +++ b/tenants/plataforma/12-postgres-compartilhado.yaml @@ -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_`, `r_`), 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 }] diff --git a/tenants/plataforma/14-netpol-banco-compartilhado.yaml b/tenants/plataforma/14-netpol-banco-compartilhado.yaml new file mode 100644 index 0000000..3d3f201 --- /dev/null +++ b/tenants/plataforma/14-netpol-banco-compartilhado.yaml @@ -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 } diff --git a/tenants/plataforma/15-netpol-tenants-no-pgbouncer.yaml b/tenants/plataforma/15-netpol-tenants-no-pgbouncer.yaml new file mode 100644 index 0000000..365515e --- /dev/null +++ b/tenants/plataforma/15-netpol-tenants-no-pgbouncer.yaml @@ -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 }