From f6aeb233a7a3351a0fd58940d96fbc5e24a5d4fb Mon Sep 17 00:00:00 2001 From: deploy Date: Tue, 22 Sep 2026 22:07:34 +0000 Subject: [PATCH] feat(moldes): o molde do tenant novo entra no repositorio GitOps MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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/). 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. --- .../00-namespace-quota-netpol.yaml | 144 ++++++++++++++++++ .../16-rolebinding-provisionamento.yaml | 22 +++ moldes/consolidado/30-ingress.yaml | 74 +++++++++ moldes/consolidado/60-monitoring-netpol.yaml | 40 +++++ moldes/consolidado/70-runtime-core.yaml | 63 ++++++++ moldes/consolidado/71-runtime-operacao.yaml | 82 ++++++++++ moldes/consolidado/72-runtime-saude.yaml | 66 ++++++++ moldes/consolidado/95-frontend-spa.yaml | 85 +++++++++++ moldes/consolidado/LEIA-ME.md | 51 +++++++ 9 files changed, 627 insertions(+) create mode 100644 moldes/consolidado/00-namespace-quota-netpol.yaml create mode 100644 moldes/consolidado/16-rolebinding-provisionamento.yaml create mode 100644 moldes/consolidado/30-ingress.yaml create mode 100644 moldes/consolidado/60-monitoring-netpol.yaml create mode 100644 moldes/consolidado/70-runtime-core.yaml create mode 100644 moldes/consolidado/71-runtime-operacao.yaml create mode 100644 moldes/consolidado/72-runtime-saude.yaml create mode 100644 moldes/consolidado/95-frontend-spa.yaml create mode 100644 moldes/consolidado/LEIA-ME.md diff --git a/moldes/consolidado/00-namespace-quota-netpol.yaml b/moldes/consolidado/00-namespace-quota-netpol.yaml new file mode 100644 index 0000000..5dae64e --- /dev/null +++ b/moldes/consolidado/00-namespace-quota-netpol.yaml @@ -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/`: 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 } diff --git a/moldes/consolidado/16-rolebinding-provisionamento.yaml b/moldes/consolidado/16-rolebinding-provisionamento.yaml new file mode 100644 index 0000000..9552494 --- /dev/null +++ b/moldes/consolidado/16-rolebinding-provisionamento.yaml @@ -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 diff --git a/moldes/consolidado/30-ingress.yaml b/moldes/consolidado/30-ingress.yaml new file mode 100644 index 0000000..7411d91 --- /dev/null +++ b/moldes/consolidado/30-ingress.yaml @@ -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: `-@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 } } } } diff --git a/moldes/consolidado/60-monitoring-netpol.yaml b/moldes/consolidado/60-monitoring-netpol.yaml new file mode 100644 index 0000000..e6aa244 --- /dev/null +++ b/moldes/consolidado/60-monitoring-netpol.yaml @@ -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 diff --git a/moldes/consolidado/70-runtime-core.yaml b/moldes/consolidado/70-runtime-core.yaml new file mode 100644 index 0000000..c72b3cd --- /dev/null +++ b/moldes/consolidado/70-runtime-core.yaml @@ -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 }] diff --git a/moldes/consolidado/71-runtime-operacao.yaml b/moldes/consolidado/71-runtime-operacao.yaml new file mode 100644 index 0000000..cba5d9c --- /dev/null +++ b/moldes/consolidado/71-runtime-operacao.yaml @@ -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 }] diff --git a/moldes/consolidado/72-runtime-saude.yaml b/moldes/consolidado/72-runtime-saude.yaml new file mode 100644 index 0000000..798f3a6 --- /dev/null +++ b/moldes/consolidado/72-runtime-saude.yaml @@ -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 }] diff --git a/moldes/consolidado/95-frontend-spa.yaml b/moldes/consolidado/95-frontend-spa.yaml new file mode 100644 index 0000000..bf694b5 --- /dev/null +++ b/moldes/consolidado/95-frontend-spa.yaml @@ -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 }] diff --git a/moldes/consolidado/LEIA-ME.md b/moldes/consolidado/LEIA-ME.md new file mode 100644 index 0000000..877bf49 --- /dev/null +++ b/moldes/consolidado/LEIA-ME.md @@ -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-`, 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/`**, 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.