feat(moldes): o molde do tenant novo entra no repositorio GitOps

Ate aqui ele vivia SO no monorepo, e o provisionamento roda DENTRO do cluster —
o pod do servico-captacao nao tinha como enxerga-lo. A etapa MANIFESTOS_GERADOS
falhava por configuracao vazia, e falhar fechado era o comportamento certo; o
que faltava era o molde chegar.

Pondo-o aqui, o pod precisa de UM volume em vez de dois: o mesmo clone do GitOps
serve de molde (moldes/consolidado) e de copia de trabalho (tenants/<nome>).

Os tres marcadores — TENANT, REALM e PLANO — batem com o RenderizadorDeMolde. Os
arquivos NAO sao YAML valido antes de renderizados, e isso e de proposito: um
`{{PLANO}}` literal aplicaria sem reclamar e criaria um tenant cujo plano se
chama "{{PLANO}}", com a barreira negando tudo depois.
This commit is contained in:
deploy
2026-09-22 22:07:34 +00:00
parent 713db06a7b
commit f6aeb233a7
9 changed files with 627 additions and 0 deletions
@@ -0,0 +1,144 @@
# =============================================================================
# P4.9 — o molde consolidado: 4 Deployments no lugar de 15.
#
# ── POR QUE FORA DE `tenants/` ───────────────────────────────────────────────
#
# O ApplicationSet do ArgoCD tem um gerador de diretorio sobre `tenants/*`. Uma
# pasta de MOLDE ali dentro seria tratada como tenant e implantada — com nome de
# namespace de mentira e tudo. Por isso os moldes vivem em `moldes/`, que o
# gerador nao enxerga.
#
# `scripts/provisionar_tenant.py` procura o molde em `tenants/<nome>`: ele
# precisa aprender este caminho novo. Esta no diario.
#
# ── A COTA, COM OS NUMEROS MEDIDOS ───────────────────────────────────────────
#
# Hoje, um tenant (`escolinha`, medido em 2026-09-18): **5253 Mi em 18 pods**.
#
# Com a consolidacao, medido container a container no mesmo dia:
#
# runtime-core 384 MiB
# runtime-operacao 403 MiB
# runtime-saude 366 MiB
# frontend-spa 24 MiB
# ----------------------------
# soma 1.177 MiB
#
# `requests.memory: 1.5Gi` cobre a soma com ~300 MiB de folga.
# `limits.memory: 2.5Gi` da a cada runtime espaco para picos sem que a soma dos
# limites vire uma promessa que o no nao pode cumprir.
#
# `pods: 8` — sao 4 Deployments; os outros 4 sao para o pod novo conviver com o
# velho durante um rollout, sem travar o deploy por cota. Foi exatamente isso que
# derrubou o provisionamento do `plataforma` em 2026-09-17: a cota do molde nao
# cabia e keycloak, bff e captacao nao conseguiam criar pod.
#
# NAO ha Postgres nem Keycloak aqui: os dois passam a viver em `plataforma-prod`
# (P4.7 e P4.8).
# =============================================================================
apiVersion: v1
kind: Namespace
metadata:
name: {{TENANT}}-prod
labels:
kubernetes.io/metadata.name: {{TENANT}}-prod
athleticmap.io/tenant: {{TENANT}}
---
apiVersion: v1
kind: ResourceQuota
metadata:
name: cota
namespace: {{TENANT}}-prod
spec:
hard:
requests.cpu: "1"
requests.memory: 1.5Gi
limits.cpu: "4"
limits.memory: 2.5Gi
pods: "8"
---
# =============================================================================
# A politica de rede.
#
# ── O QUE MUDOU EM RELACAO AO MOLDE ANTIGO ──────────────────────────────────
#
# O `deny-cross-tenant` antigo liberava egress para `ipBlock: 10.43.0.0/16` — a
# CIDR de Services do cluster INTEIRO. Era mais largo do que o nome dizia: um pod
# de qualquer tenant alcancava, pela rede, o ClusterIP de qualquer outro.
#
# Aqui o egress interno e nominal: DNS, os pods do proprio namespace, e o
# namespace `plataforma-prod` nas portas que importam.
#
# ── O QUE NAO FOI POSSIVEL, E O MOTIVO E O CNI ──────────────────────────────
#
# O card pede "egress so ... os destinos externos reais (Asaas, provedor fiscal,
# Garmin) — nada de egress irrestrito".
#
# **Nao da com o CNI deste cluster.** Conferido em 2026-09-19: k3s com flannel
# embutido, sem Cilium e sem Calico (nenhum CRD dos dois). NetworkPolicy padrao
# do Kubernetes so aceita `ipBlock` — CIDR, nao nome. E Asaas, provedor fiscal e
# Garmin sao SaaS atras de CDN: os IPs mudam, e uma lista de CIDR quebraria a
# cobranca sem aviso, num dia qualquer.
#
# Entao a regra de 443 continua, e continua sendo egress irrestrito para a
# internet. O `except` preserva o que a politica de fato protege: um tenant nao
# alcanca a rede interna do outro.
#
# **O caminho para fechar isso de verdade** esta no diario: um proxy de saida no
# `plataforma-prod`, com lista de hosts permitidos, e os tenants alcancando so
# ele. Funciona com flannel, e nao exige trocar o CNI.
# =============================================================================
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: deny-cross-tenant
namespace: {{TENANT}}-prod
spec:
podSelector: {}
policyTypes: [Ingress, Egress]
ingress:
- from:
- podSelector: {} # mesma namespace
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: kube-system # Traefik (ingress)
egress:
# 1. Dentro do proprio namespace.
- to:
- podSelector: {}
# 2. DNS.
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: kube-system
ports:
- { protocol: UDP, port: 53 }
- { protocol: TCP, port: 53 }
# 3. A plataforma: banco e identidade, so nas portas que importam.
#
# 5432 direto no Postgres e para o `pg_dump` do backup: ele abre uma
# transacao unica e longa, o oposto do que transaction pooling serve.
# 6432 e o PgBouncer, por onde os runtimes falam.
# 8080 e o Keycloak (P4.8).
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: plataforma-prod
ports:
- { protocol: TCP, port: 5432 }
- { protocol: TCP, port: 6432 }
- { protocol: TCP, port: 8080 }
# 4. HTTPS para a internet publica. Ver o cabecalho: nao da para estreitar
# com flannel, e uma lista de CIDR de SaaS quebraria a cobranca sem aviso.
#
# O `except` e o que a politica de fato garante: nenhum tenant alcanca a
# rede interna de outro por este caminho.
- to:
- ipBlock:
cidr: 0.0.0.0/0
except: [10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16]
ports:
- { protocol: TCP, port: 443 }
@@ -0,0 +1,22 @@
# =============================================================================
# P5.6/P5.9 — a plataforma pode esperar rollout e suspender ESTE tenant.
#
# O ClusterRole (`atm-provisionamento-tenants`, em
# `tenants/plataforma/16-rbac-provisionamento.yaml`) e so a lista de verbos;
# quem concede e este RoleBinding, e ele vale SO neste namespace. Um tenant sem
# este arquivo simplesmente nao e alcancado pelo provisionamento — a etapa falha
# com 403 dizendo o caminho que tentou, que e melhor do que um acesso amplo.
# =============================================================================
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: atm-provisionamento
namespace: {{TENANT}}-prod
subjects:
- kind: ServiceAccount
name: atm-provisionamento
namespace: plataforma-prod
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: atm-provisionamento-tenants
+74
View File
@@ -0,0 +1,74 @@
# =============================================================================
# P5.5 — a entrada do tenant, com certificado automatico.
#
# ── O MOLDE NAO TINHA INGRESS NENHUM ────────────────────────────────────────
#
# Achado em 2026-09-19, ao comecar o card 165: os cinco arquivos do molde
# consolidado criavam namespace, tres runtimes e a SPA — e nenhuma entrada. Um
# tenant provisionado a partir dele subiria inteiro e **nao seria alcancavel de
# fora**, com todos os pods saudaveis.
#
# ── UMA REGRA SO, E ISSO E O PONTO DE P4.6 ──────────────────────────────────
#
# O Ingress do `plataforma` tem SETE caminhos: um por contexto, mais o BFF, mais a
# SPA. Aqui ha um. O nginx da imagem do frontend roteia `/bff` e `/api` por conta
# propria (P4.6), entao a topologia interna deixou de ser problema do Ingress — e
# mudar de cinco pods para tres runtimes nao obriga mais a editar o Ingress de
# todos os tenants.
#
# ── SEM HOST DE AUTENTICACAO ────────────────────────────────────────────────
#
# O `plataforma` tem um segundo host, `auth-plataforma...`, porque tinha Keycloak
# proprio. Com o Keycloak compartilhado (P4.8) o tenant nao tem: a autenticacao
# acontece no host da plataforma, e o realm e que separa um cliente do outro.
#
# ── O CERTIFICADO, E POR QUE HTTP-01 ────────────────────────────────────────
#
# `cert-manager.io/cluster-issuer: letsencrypt-prod` — o mesmo que o `plataforma`
# ja usa, com desafio HTTP-01.
#
# A alternativa seria um certificado CURINGA, e ele exigiria desafio DNS-01, e
# DNS-01 exige um solver do provedor de DNS. **O provedor e a Hostinger, e o
# cert-manager nao tem solver nativo para ela** — seria preciso escrever um
# webhook. Conferido em 2026-09-19.
#
# Com HTTP-01 cada tenant tem o proprio certificado e nao ha solver nenhum a
# escrever. O que se precisa do provedor e UMA entrada de DNS curinga
# (`*.athleticmap.com` apontando para o cluster), feita uma vez a mao.
#
# **A ORDEM E OBRIGATORIA:** o host precisa resolver para o cluster ANTES de o
# cert-manager pedir o certificado, porque o desafio HTTP-01 e uma requisicao que
# a Let's Encrypt faz ao proprio host. Sem o curinga no ar, o certificado fica em
# `False` para sempre e o tenant responde so em HTTP.
# =============================================================================
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: redirect-https
namespace: {{TENANT}}-prod
spec:
redirectScheme:
scheme: https
permanent: true
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: {{TENANT}}
namespace: {{TENANT}}-prod
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
# O middleware e por namespace: `<namespace>-<nome>@kubernetescrd`.
traefik.ingress.kubernetes.io/router.middlewares: {{TENANT}}-prod-redirect-https@kubernetescrd
spec:
ingressClassName: traefik
tls:
- hosts:
- {{TENANT}}.athleticmap.com
secretName: {{TENANT}}-tls
rules:
- host: {{TENANT}}.athleticmap.com
http:
paths:
# UMA rota. O nginx da SPA resolve /bff e /api internamente (P4.6).
- { path: /, pathType: Prefix, backend: { service: { name: frontend-spa, port: { number: 80 } } } }
@@ -0,0 +1,40 @@
# =============================================================================
# Deixa o Prometheus raspar as metricas deste tenant.
#
# Aditivo a `deny-cross-tenant` (em `00-namespace-quota-netpol.yaml`), que fecha
# ingresso de fora do namespace. Sem esta politica o tenant sobe, funciona e fica
# INVISIVEL no Grafana — e um tenant sem metrica e um tenant em que ninguem
# percebe consumo subindo nem pod reiniciando.
#
# ── POR QUE `podSelector: {}` (2026-09-20) ──────────────────────────────────
#
# Antes a politica citava pods por NOME (`app: backend`, ou uma lista). Resultado:
# quando o `ServiceMonitor` passou a raspar os runtimes e os 13 servicos, TODOS os
# alvos novos ficaram `up=0` e o cluster acendeu 48 alertas de `TargetDown` — a
# coleta chegava ao Service e morria na rede.
#
# Lista de nomes aqui e a mesma lista do `ServiceMonitor` escrita duas vezes, em
# lugares diferentes, com o erro aparecendo so em producao. Agora vale para
# qualquer pod DESTE namespace, e a fronteira e a mesma de antes:
#
# - SO a porta 8083, que e a das metricas (`/actuator/prometheus`);
# - SO do namespace `monitoring`.
#
# O `frontend-spa` escuta na 80 e continua fora, sem precisar ser citado.
# =============================================================================
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-monitoring
namespace: {{TENANT}}-prod
spec:
podSelector: {}
policyTypes: [Ingress]
ingress:
- from:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: monitoring
ports:
- protocol: TCP
port: 8083
+63
View File
@@ -0,0 +1,63 @@
# =============================================================================
# P4.9 — runtime-core — configuracao, cadastro, organizacao, documentos, agenda
#
# `{{TENANT}}` e substituido pelo nome do tenant ao provisionar. O molde inteiro usa
# o mesmo marcador, para `sed`/script nao precisarem conhecer cada arquivo.
# =============================================================================
apiVersion: apps/v1
kind: Deployment
metadata:
name: runtime-core
namespace: {{TENANT}}-prod
spec:
replicas: 1
selector:
matchLabels: { app: runtime-core }
template:
metadata:
labels:
app: runtime-core
athleticmap.io/contexto: runtime-core
spec:
containers:
- name: runtime-core
image: docker.io/library/runtime-core:1.1-demo
# `Never`: a imagem tem de estar no containerd do no ANTES do deploy.
# "Presente no no" tambem nao e "atual" — conferir a TAG, e nao o nome.
imagePullPolicy: Never
ports: [{ containerPort: 8083 }]
env:
# O banco e o PgBouncer do `plataforma-prod` (P4.7), e nao um
# Postgres deste namespace: ele nao existe mais aqui.
- { name: SPRING_DATASOURCE_URL, value: "jdbc:postgresql://pgbouncer.plataforma-prod.svc:6432/athleticmap_{{TENANT}}" }
- { name: SPRING_DATASOURCE_USERNAME, value: "r_{{TENANT}}" }
- name: SPRING_DATASOURCE_PASSWORD
valueFrom:
secretKeyRef: { name: db-credentials, key: password }
# O Keycloak tambem e da plataforma (P4.8). O REALM passa a ser o
# nome do tenant: uma instancia so nao comporta quatro realms
# chamados `athleticmap`.
- { name: ATM_JWK_SET_URI, value: "http://keycloak.plataforma-prod.svc:8080/realms/{{REALM}}/protocol/openid-connect/certs" }
- { name: ATM_ISSUER, value: "https://auth.athleticmap.influxdigital.com.br/realms/{{REALM}}" }
- { name: ATM_TENANT, value: "{{TENANT}}" }
- { name: ATM_PLANO, value: "{{PLANO}}" }
resources:
requests: { cpu: 100m, memory: 384Mi }
limits: { cpu: "1", memory: 768Mi }
readinessProbe:
httpGet: { path: /actuator/health/readiness, port: 8083 }
initialDelaySeconds: 20
periodSeconds: 10
livenessProbe:
httpGet: { path: /actuator/health/liveness, port: 8083 }
initialDelaySeconds: 60
periodSeconds: 30
---
apiVersion: v1
kind: Service
metadata:
name: runtime-core
namespace: {{TENANT}}-prod
spec:
selector: { app: runtime-core }
ports: [{ port: 80, targetPort: 8083 }]
@@ -0,0 +1,82 @@
# =============================================================================
# P4.9 — runtime-operacao — planejamento, campeonato, administrativo, financeiro
#
# `{{TENANT}}` e substituido pelo nome do tenant ao provisionar. O molde inteiro usa
# o mesmo marcador, para `sed`/script nao precisarem conhecer cada arquivo.
# =============================================================================
apiVersion: apps/v1
kind: Deployment
metadata:
name: runtime-operacao
namespace: {{TENANT}}-prod
spec:
replicas: 1
selector:
matchLabels: { app: runtime-operacao }
template:
metadata:
labels:
app: runtime-operacao
athleticmap.io/contexto: runtime-operacao
spec:
containers:
- name: runtime-operacao
image: docker.io/library/runtime-operacao:1.1-demo
# `Never`: a imagem tem de estar no containerd do no ANTES do deploy.
# "Presente no no" tambem nao e "atual" — conferir a TAG, e nao o nome.
imagePullPolicy: Never
ports: [{ containerPort: 8083 }]
env:
# O banco e o PgBouncer do `plataforma-prod` (P4.7), e nao um
# Postgres deste namespace: ele nao existe mais aqui.
- { name: SPRING_DATASOURCE_URL, value: "jdbc:postgresql://pgbouncer.plataforma-prod.svc:6432/athleticmap_{{TENANT}}" }
- { name: SPRING_DATASOURCE_USERNAME, value: "r_{{TENANT}}" }
- name: SPRING_DATASOURCE_PASSWORD
valueFrom:
secretKeyRef: { name: db-credentials, key: password }
# O Keycloak tambem e da plataforma (P4.8). O REALM passa a ser o
# nome do tenant: uma instancia so nao comporta quatro realms
# chamados `athleticmap`.
- { name: ATM_JWK_SET_URI, value: "http://keycloak.plataforma-prod.svc:8080/realms/{{REALM}}/protocol/openid-connect/certs" }
- { name: ATM_ISSUER, value: "https://auth.athleticmap.influxdigital.com.br/realms/{{REALM}}" }
- { name: ATM_TENANT, value: "{{TENANT}}" }
- { name: ATM_PLANO, value: "{{PLANO}}" }
# O provedor de pagamento vive neste runtime.
#
# `optional: true` NAO e descuido, e virou obrigatorio com a decisao de
# 2026-09-19: a chave do Asaas e DO CLIENTE, e ele a fornece depois de o
# tenant existir — precisa entrar no painel dele, gerar a chave e colar.
#
# Sem `optional`, um Secret ausente IMPEDE O POD DE INICIAR. O tenant
# novo nao subiria ate o cliente mandar a chave, e o sintoma chegaria
# como "o sistema nao abriu" em vez de "falta configurar cobranca".
#
# O runtime ja sabe conviver com isso: `AsaasPropriedades` trata chave
# vazia como INTEGRACAO DESLIGADA, e o resto do sistema funciona.
- name: ASAAS_API_KEY
valueFrom:
secretKeyRef: { name: asaas-credentials, key: api-key, optional: true }
- name: ASAAS_WEBHOOK_TOKEN
valueFrom:
secretKeyRef: { name: asaas-credentials, key: webhook-token, optional: true }
- { name: ASAAS_AMBIENTE, value: "sandbox" }
resources:
requests: { cpu: 100m, memory: 400Mi }
limits: { cpu: "1", memory: 768Mi }
readinessProbe:
httpGet: { path: /actuator/health/readiness, port: 8083 }
initialDelaySeconds: 20
periodSeconds: 10
livenessProbe:
httpGet: { path: /actuator/health/liveness, port: 8083 }
initialDelaySeconds: 60
periodSeconds: 30
---
apiVersion: v1
kind: Service
metadata:
name: runtime-operacao
namespace: {{TENANT}}-prod
spec:
selector: { app: runtime-operacao }
ports: [{ port: 80, targetPort: 8083 }]
+66
View File
@@ -0,0 +1,66 @@
# =============================================================================
# P4.9 — runtime-saude — saude, nutricao, assistencia-social, pedagogico
#
# `{{TENANT}}` e substituido pelo nome do tenant ao provisionar. O molde inteiro usa
# o mesmo marcador, para `sed`/script nao precisarem conhecer cada arquivo.
# =============================================================================
apiVersion: apps/v1
kind: Deployment
metadata:
name: runtime-saude
namespace: {{TENANT}}-prod
spec:
replicas: 1
selector:
matchLabels: { app: runtime-saude }
template:
metadata:
labels:
app: runtime-saude
athleticmap.io/contexto: runtime-saude
spec:
containers:
- name: runtime-saude
image: docker.io/library/runtime-saude:1.2-demo
# `Never`: a imagem tem de estar no containerd do no ANTES do deploy.
# "Presente no no" tambem nao e "atual" — conferir a TAG, e nao o nome.
imagePullPolicy: Never
ports: [{ containerPort: 8083 }]
env:
# O banco e o PgBouncer do `plataforma-prod` (P4.7), e nao um
# Postgres deste namespace: ele nao existe mais aqui.
- { name: SPRING_DATASOURCE_URL, value: "jdbc:postgresql://pgbouncer.plataforma-prod.svc:6432/athleticmap_{{TENANT}}" }
- { name: SPRING_DATASOURCE_USERNAME, value: "r_{{TENANT}}" }
- name: SPRING_DATASOURCE_PASSWORD
valueFrom:
secretKeyRef: { name: db-credentials, key: password }
# O Keycloak tambem e da plataforma (P4.8). O REALM passa a ser o
# nome do tenant: uma instancia so nao comporta quatro realms
# chamados `athleticmap`.
- { name: ATM_JWK_SET_URI, value: "http://keycloak.plataforma-prod.svc:8080/realms/{{REALM}}/protocol/openid-connect/certs" }
- { name: ATM_ISSUER, value: "https://auth.athleticmap.influxdigital.com.br/realms/{{REALM}}" }
- { name: ATM_TENANT, value: "{{TENANT}}" }
- { name: ATM_PLANO, value: "{{PLANO}}" }
# Retencao da trilha de auditoria. O expurgo roda agendado DENTRO
# deste runtime — ver RuntimeSaude.
- { name: ATM_AUDITORIA_RETENCAO_DIAS, value: "730" }
resources:
requests: { cpu: 100m, memory: 384Mi }
limits: { cpu: "1", memory: 768Mi }
readinessProbe:
httpGet: { path: /actuator/health/readiness, port: 8083 }
initialDelaySeconds: 20
periodSeconds: 10
livenessProbe:
httpGet: { path: /actuator/health/liveness, port: 8083 }
initialDelaySeconds: 60
periodSeconds: 30
---
apiVersion: v1
kind: Service
metadata:
name: runtime-saude
namespace: {{TENANT}}-prod
spec:
selector: { app: runtime-saude }
ports: [{ port: 80, targetPort: 8083 }]
+85
View File
@@ -0,0 +1,85 @@
# =============================================================================
# P4.9 — a SPA, e com ela o roteamento interno de /bff e /api (P4.6).
#
# `{{TENANT}}` e substituido ao provisionar.
#
# ── O QUE MUDOU EM RELACAO AO MOLDE ANTIGO ──────────────────────────────────
#
# O nginx desta imagem roteia `/bff` e `/api` por conta propria, a partir de
# `ATM_BFF_URL` e `ATM_API_URL`. Por isso o Ingress deste molde tem UMA rota — e
# nao uma linha por contexto, que era o que obrigava a editar o Ingress de todos
# os tenants a cada mudanca de topologia.
#
# O `/api` aponta para o `runtime-operacao` porque e ele quem recebe o webhook do
# provedor de pagamento em `/api/public/**`. Os outros caminhos de `/api` sao
# resolvidos pelo proprio nginx? NAO — e por isso esta linha e provisoria.
#
# **PENDENCIA CONHECIDA:** `/api/cadastro`, `/api/saude` e os demais precisam ir
# para runtimes DIFERENTES, e uma variavel so nao faz isso. O nginx precisa de
# uma regra por grupo de contexto. Esta no diario; ate resolver, use o Ingress
# para separar, como hoje.
# =============================================================================
apiVersion: apps/v1
kind: Deployment
metadata:
name: frontend-spa
namespace: {{TENANT}}-prod
spec:
replicas: 1
selector:
matchLabels: { app: frontend-spa }
template:
metadata:
labels: { app: frontend-spa }
spec:
containers:
- name: frontend-spa
image: docker.io/library/frontend-spa:2.65-csp
imagePullPolicy: Never
ports: [{ containerPort: 80 }]
env:
- { name: ATM_KC_URL, value: "https://auth.athleticmap.influxdigital.com.br" }
- { name: ATM_REALM, value: "{{REALM}}" }
- { name: ATM_CLIENT, value: "spa" }
- { name: ATM_TENANT, value: "{{TENANT}}" }
- { name: ATM_PLANO, value: "{{PLANO}}" }
# ATENCAO: nao ha Deployment de `servico-bff` neste molde — sao
# quatro, e nenhum e o BFF. Este endereco NAO RESOLVE, e todo /bff
# devolve 502. Ver a pendencia no diario; o molde ainda nao pode ser
# aplicado a um tenant por causa disto. Deixado apontando para o nome
# antigo de proposito: apagar a linha esconderia o buraco.
- { name: ATM_BFF_URL, value: "http://servico-bff" }
# /api por GRUPO DE CONTEXTO, e nao um destino so.
#
# A versao anterior mandava TUDO para o `runtime-operacao`, porque e
# ele quem recebe o webhook do Asaas. Isso estava certo para quatro
# contextos e errado para os outros nove: `/api/cadastro` tem de ir
# para o core e `/api/saude` para o saude.
#
# O sintoma seria 404 do Spring, e nao 502 — o runtime responde, so
# nao conhece o caminho. Na tela, uma lista vazia.
#
# `ATM_API_URL` continua, e nao e redundante: e o destino do que nao
# pertence a grupo nenhum, hoje `/api/public/**`.
- { name: ATM_API_URL, value: "http://runtime-operacao" }
- { name: ATM_API_CORE_URL, value: "http://runtime-core" }
- { name: ATM_API_OPERACAO_URL, value: "http://runtime-operacao" }
- { name: ATM_API_SAUDE_URL, value: "http://runtime-saude" }
resources:
# 24 MiB medidos em 2026-09-18. 64Mi de request da folga de 2,5x.
requests: { cpu: 50m, memory: 64Mi }
limits: { cpu: 200m, memory: 128Mi }
readinessProbe:
httpGet: { path: /, port: 80 }
initialDelaySeconds: 5
periodSeconds: 10
---
apiVersion: v1
kind: Service
metadata:
name: frontend-spa
namespace: {{TENANT}}-prod
spec:
selector: { app: frontend-spa }
ports: [{ port: 80, targetPort: 80 }]
+51
View File
@@ -0,0 +1,51 @@
# Molde consolidado de tenant — P4.9
Quatro Deployments no lugar de quinze.
## Os numeros, medidos e nao estimados
| | |
|---|---:|
| um tenant hoje (`escolinha`, 2026-09-18) | **5.253 Mi em 18 pods** |
| runtime-core | 384 MiB |
| runtime-operacao | 403 MiB |
| runtime-saude | 366 MiB |
| frontend-spa | 24 MiB |
| **soma do molde novo** | **1.177 MiB em 4 pods** |
A cota (`requests 1.5Gi` / `limits 2.5Gi` / `pods 8`) sai desses numeros, com
folga para o pod novo conviver com o velho durante um rollout — foi a falta
disso que travou o provisionamento do `plataforma` em 2026-09-17.
## O que este molde NAO tem
**Postgres e Keycloak.** Os dois passam a viver em `plataforma-prod` (P4.7 e
P4.8). E por isso que o tenant caiu de 18 pods para 4.
## O que falta antes de usar
1. **A renomeacao dos quatro realms que ja existem.** O molde usa agora um
marcador PROPRIO, `{{REALM}}`, e nao mais `{{TENANT}}` — decidido em
2026-09-19: o realm passa a se chamar `athleticmap-<N>`, com N na ordem de
criacao, e o tenant continua com o nome comercial.
**Sao coisas diferentes agora**, e confundi-las gera um `ATM_JWK_SET_URI`
apontando para um realm inexistente: o pod fica saudavel e ninguem consegue
entrar.
Os quatro tenants atuais ainda tem realm `athleticmap`, e a renomeacao deles e
o card 156. **A ordem de criacao nao foi conferida** — esta no diario.
2. **Nao ha Deployment do `servico-bff` neste molde**, e o `ATM_BFF_URL` aponta
para um Service que nao existe no namespace. Todo `/bff` daria 502. Ver o
cabecalho do `95-frontend-spa.yaml` e o diario: bloqueia os cards 158 e 159.
3. **O `asaas-credentials` pode nao existir quando o pod subir.** Com a decisao de
2026-09-19 (a plataforma abre subconta por cliente), a aprovacao passa por
analise de documentos e nao cabe na janela de provisionamento. O
`secretKeyRef` precisa virar `optional: true`, senao o `runtime-operacao` nao
inicia.
4. **`scripts/provisionar_tenant.py` procura o molde em `tenants/<nome>`**, e
este vive em `moldes/`. Precisa aprender o caminho.
## O molde antigo continua onde estava
`tenants/piloto` e os outros nao foram tocados: **eles sao o rollback**. O card
pede isso explicitamente, e nao e formalidade — enquanto os runtimes nao
estiverem em producao, o que roda e o desenho antigo.