feat(tenant): provisiona teste5

Cliente: Teste5
Assinatura: sub_zx7fdtz40b8is2gk
Plano: (nao informado)
Realm: athleticmap-9

Gerado pelo provisionamento automatico (P5.2/P5.3) a partir do molde
consolidado de P4.9. O ApplicationSet cria a Application sozinho pelo
gerador de diretorio sobre tenants/*.
This commit is contained in:
provisionamento Athletic Map
2026-09-25 15:26:54 +00:00
committed by app
parent 7b67bf810e
commit 706a6e67ab
10 changed files with 768 additions and 0 deletions
@@ -0,0 +1,176 @@
# =============================================================================
# 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
#
# OS NUMEROS DA TABELA ACIMA SAO DE QUANDO ERAM QUATRO DEPLOYMENTS. Hoje sao
# cinco (o `servico-bff` entrou em 2026-09-24), e os tetos ja viveram duas
# correcoes: requests.memory 1.5Gi -> 2Gi, limits.memory 2.5Gi -> 3.5Gi ->
# 4352Mi, pods 8 -> 10. A razao de cada valor esta ao lado dele, la embaixo.
#
# A REGRA QUE VALE PARA TODOS: a cota tem de caber o REPOUSO MAIS UMA TROCA.
# Um Deployment sobe o pod novo ANTES de derrubar o velho, entao no pico existe
# um pod a mais do que o desenho sugere -- e o `maxSurge: 25%` sobre uma unica
# replica arredonda para cima, ou seja, sempre um inteiro.
#
# Ja custou duas vezes, pela mesma causa em dimensoes diferentes: derrubou o
# provisionamento do `plataforma` em 2026-09-17 (faltou `pods`), e prendeu os
# dois tenants pagos por 12h em 2026-09-24 (faltou `limits.memory`), com o
# ArgoCD dizendo "Synced" enquanto o pod novo era recusado pela cota.
#
#
# 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: teste5-prod
labels:
kubernetes.io/metadata.name: teste5-prod
athleticmap.io/tenant: teste5
---
apiVersion: v1
kind: ResourceQuota
metadata:
name: cota
namespace: teste5-prod
spec:
hard:
# SUBIU em 2026-09-24 para caber o BFF, o quinto Deployment.
#
# Nao e folga generosa: com quatro Deployments o tenant ja usava 1232Mi dos
# 1536Mi, e o BFF pede 256Mi. Caberia no limite antigo em repouso, e
# estouraria no primeiro rollout — que e quando um Deployment sobe o pod
# novo antes de derrubar o velho. Foi essa conta que derrubou o
# provisionamento do `plataforma` em 17/09, e o registro dela esta ali.
#
# `pods` vai a 10 pelo mesmo motivo: cinco Deployments, um surge cada.
requests.cpu: "1.5"
requests.memory: 2Gi
limits.cpu: "5"
# SUBIU de 3.5Gi em 2026-09-25: faltava por 128Mi na hora da troca.
#
# A conta que o comentario acima comecou, agora terminada. Em repouso os
# cinco Deployments somam 2944Mi (128 + 768*3 + 512). O `maxSurge: 25%` sobre
# UMA replica arredonda para cima e pede um pod inteiro a mais: 768Mi, se o
# que esta trocando e um runtime. 2944 + 768 = 3712, contra um teto de 3584.
#
# Nao foi hipotese: os dois tenants pagos ficaram 12h presos em `1.1-demo`
# sem conseguir ir para `1.2-auditoria`, com o ArgoCD dizendo "Synced" e o
# pod novo recusado pela cota a cada ~17min.
#
# 4352Mi = 2944 de repouso + 768 da troca + 640 de sobra.
#
# So `limits.memory` travava. Medido em 2026-09-25: requests.memory tinha
# 560Mi livres (a troca pede 400), requests.cpu 1100m (pede 100), limits.cpu
# 1300m (pede 1000) e pods 5 de 10. Subir a memoria basta.
limits.memory: 4352Mi
pods: "10"
---
# =============================================================================
# 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: teste5-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,23 @@
# =============================================================================
# P5.3 — gerado pelo provisionamento. NAO edite a mao.
#
# A senha em claro NUNCA existiu em disco: foi gerada em memoria, selada com o
# certificado PUBLICO do controlador e zerada. Ninguem a conhece, e ninguem
# precisa: quem a le e o proprio Postgres, pelo Secret que este arquivo cria.
#
# Escopo ESTRITO: este cifrado so abre para o Secret `db-credentials` no namespace `teste5-prod`.
# Copiar o bloco para outro tenant NAO funciona, e e assim que tem de ser.
# =============================================================================
apiVersion: bitnami.com/v1alpha1
kind: SealedSecret
metadata:
name: db-credentials
namespace: teste5-prod
spec:
encryptedData:
password: AgAf6mGxLgKonu6p/AT3mNIhBILBQP0mhecqKyhI9wSe+tRWsFK1jiXTZ6DIW//xcPVAaI1k6B9Hgrukd+sDZIMpd5wwidz+lGB4V5/qIWTORQYfQFB/9wIyA2Xk8TK7+bVBYWc7rMEWdtsKUhfuTgZOMVEj89ltMxJslj6E0DrGeg1GyPP2Byd7v5g71LnqqEDemn0KnVb8RooZOIfqv8RqNduqmLXCBJ8UlTu05lSdOS15zd5FrAl7NZIsYwfYpsZvYjR3tHDZlUIPmhH1GzTjSDS8AC4mYeblHRpDOibbT1iEBs/grCD6Ml7WRk6smbSkWF5hOf6olPu9yyca2VOIHgC6oVxp70Rh5R2DgLIJM1X7ssnxHe/rLE+JLOWM8eUJHfxjKMIsIlWVXVBoG+ucbgD6gTXhxYtIzR3qiU0VxGQq2Ay6ZZAuwQZav43Ya+DAcclTWPZe4cn27botfCnc2abSENc/AtlsEQRfrHx7fB70ZbTBcmFD5Ua0L8SBp/V6zDOLHcrNv20yuTDaqx0LOkb3FjoKq4PuCBmCGiQL9HWmF247YIFikon7tfzTpnC3MYlDw7EKVcwXOMyA0+/JtMUbScO9wYjdwbpPfZi3o1cMOhFGb1VaEO5VjOoPcfNkZfzVPIwoXLu8uP49xArfzdibfJvPLlMucIiFJnzZvKNGAlj4rHa7LAifOQr2Id+Y/6EgVR2Q486G5pdnCctYa53rAooqYIeQbs3L+kXpY0BcKNDoQSjQo/R/
template:
metadata:
name: db-credentials
namespace: teste5-prod
type: Opaque
@@ -0,0 +1,59 @@
# =============================================================================
# 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: teste5-prod
subjects:
- kind: ServiceAccount
name: atm-provisionamento
namespace: plataforma-prod
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: atm-provisionamento-tenants
---
# Os DOIS Secrets que o provisionamento le neste namespace, pelo nome.
#
# Role por tenant, e nao regra no ClusterRole, porque os nomes dependem do
# tenant e `resourceNames` nao aceita curinga. A alternativa seria dar `get` em
# QUALQUER Secret do namespace — e ai o provisionamento passaria a poder ler a
# chave Asaas DO CLIENTE, que e dele e nao nossa.
#
# db-credentials a senha do papel no Postgres, que a etapa BANCO_CRIADO
# precisa para dar a senha ao papel e ao pool
# teste5-tls so a EXISTENCIA, que e como a etapa DNS_PUBLICADO sabe que
# o cert-manager terminou de emitir
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: atm-provisionamento-segredos
namespace: teste5-prod
rules:
- apiGroups: [""]
resources: ["secrets"]
verbs: ["get"]
resourceNames: ["db-credentials", "teste5-tls"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: atm-provisionamento-segredos
namespace: teste5-prod
subjects:
- kind: ServiceAccount
name: atm-provisionamento
namespace: plataforma-prod
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: atm-provisionamento-segredos
+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: teste5-prod
spec:
redirectScheme:
scheme: https
permanent: true
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: teste5
namespace: teste5-prod
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
# O middleware e por namespace: `<namespace>-<nome>@kubernetescrd`.
traefik.ingress.kubernetes.io/router.middlewares: teste5-prod-redirect-https@kubernetescrd
spec:
ingressClassName: traefik
tls:
- hosts:
- teste5.athleticmap.com
secretName: teste5-tls
rules:
- host: teste5.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 } } } }
+40
View File
@@ -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: teste5-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
#
# `teste5` 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: teste5-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.2-auditoria
# `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_teste5" }
- { name: SPRING_DATASOURCE_USERNAME, value: "r_teste5" }
- 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/athleticmap-9/protocol/openid-connect/certs" }
- { name: ATM_ISSUER, value: "https://auth.athleticmap.com/realms/athleticmap-9" }
- { name: ATM_TENANT, value: "teste5" }
- { name: ATM_PLANO, value: "bronze" }
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: teste5-prod
spec:
selector: { app: runtime-core }
ports: [{ port: 80, targetPort: 8083 }]
+82
View File
@@ -0,0 +1,82 @@
# =============================================================================
# P4.9 — runtime-operacao — planejamento, campeonato, administrativo, financeiro
#
# `teste5` 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: teste5-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.2-auditoria
# `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_teste5" }
- { name: SPRING_DATASOURCE_USERNAME, value: "r_teste5" }
- 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/athleticmap-9/protocol/openid-connect/certs" }
- { name: ATM_ISSUER, value: "https://auth.athleticmap.com/realms/athleticmap-9" }
- { name: ATM_TENANT, value: "teste5" }
- { name: ATM_PLANO, value: "bronze" }
# 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: teste5-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
#
# `teste5` 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: teste5-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_teste5" }
- { name: SPRING_DATASOURCE_USERNAME, value: "r_teste5" }
- 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/athleticmap-9/protocol/openid-connect/certs" }
- { name: ATM_ISSUER, value: "https://auth.athleticmap.com/realms/athleticmap-9" }
- { name: ATM_TENANT, value: "teste5" }
- { name: ATM_PLANO, value: "bronze" }
# 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: teste5-prod
spec:
selector: { app: runtime-saude }
ports: [{ port: 80, targetPort: 8083 }]
+89
View File
@@ -0,0 +1,89 @@
# =============================================================================
# O BFF do tenant.
#
# POR QUE ELE EXISTE AQUI, E NAO COMO BIBLIOTECA
#
# A SPA chama o BFF para quase tudo. Sem ele, `/bff` devolve 502 e o sistema
# abre vazio — foi o que aconteceu nos dois primeiros tenants provisionados, em
# 2026-09-24. As tres saidas estavam registradas desde 2026-09-18; o dono
# escolheu o quinto Deployment, que e o unico que entrega sem refatorar um
# componente que ja roda em producao em dois lugares.
#
# O PRECO ESTA NA COTA, e nao aqui: cinco Deployments precisam de mais que oito
# pods durante um rollout. Ver 00-namespace-quota-netpol.yaml.
#
# AS URLS APONTAM PARA OS RUNTIMES, e nao para os treze servicos: e esta a
# diferenca entre este molde e o `escolinha`. O mapa e o mesmo dos poms:
# core configuracao, cadastro, organizacao, documentos, agenda
# operacao planejamento, campeonato, administrativo, financeiro
# saude saude, nutricao, assistencia-social, pedagogico
# =============================================================================
apiVersion: apps/v1
kind: Deployment
metadata:
name: servico-bff
namespace: teste5-prod
labels:
app: servico-bff
athleticmap.io/contexto: bff
spec:
replicas: 1
selector: { matchLabels: { app: servico-bff } }
template:
metadata:
labels:
app: servico-bff
athleticmap.io/contexto: bff
spec:
containers:
- name: servico-bff
image: docker.io/library/servico-bff:2.0-exportacao
imagePullPolicy: Never
ports: [{ containerPort: 8083 }]
env:
- { name: ATM_CONFIG_URL, value: "http://runtime-core" }
- { name: ATM_CADASTRO_URL, value: "http://runtime-core" }
- { name: ATM_ORGANIZACAO_URL, value: "http://runtime-core" }
- { name: ATM_DOCUMENTOS_URL, value: "http://runtime-core" }
- { name: ATM_AGENDA_URL, value: "http://runtime-core" }
- { name: ATM_PLANEJAMENTO_URL, value: "http://runtime-operacao" }
- { name: ATM_CAMPEONATO_URL, value: "http://runtime-operacao" }
- { name: ATM_ADMINISTRATIVO_URL, value: "http://runtime-operacao" }
- { name: ATM_FINANCEIRO_URL, value: "http://runtime-operacao" }
- { name: ATM_SAUDE_URL, value: "http://runtime-saude" }
- { name: ATM_NUTRICAO_URL, value: "http://runtime-saude" }
- { name: ATM_ASSISTENCIA_SOCIAL_URL, value: "http://runtime-saude" }
- { name: ATM_PEDAGOGICO_URL, value: "http://runtime-saude" }
- { name: ATM_JWK_SET_URI, value: "http://keycloak.plataforma-prod.svc:8080/realms/athleticmap-9/protocol/openid-connect/certs" }
- { name: ATM_ISSUER, value: "https://auth.athleticmap.com/realms/athleticmap-9" }
- { name: ATM_TENANT, value: "teste5" }
- { name: ATM_PLANO, value: "bronze" }
# `optional` porque o molde ainda NAO cria este Secret. Sem ele o
# selo de consentimento simplesmente nao e emitido — o BFF sobe e
# funciona (ver SeloDeConsentimento, que trata segredo vazio). No dia
# em que o Secret existir, isto passa a valer sozinho.
- name: ATM_CONSENTIMENTO_SEGREDO
valueFrom:
secretKeyRef: { name: consentimento-selo, key: segredo, optional: true }
resources:
requests: { cpu: 50m, memory: 256Mi }
limits: { cpu: 500m, memory: 512Mi }
readinessProbe:
httpGet: { path: /bff/health, port: 8083 }
initialDelaySeconds: 20
periodSeconds: 10
failureThreshold: 30
livenessProbe:
httpGet: { path: /actuator/health/liveness, port: 8083 }
initialDelaySeconds: 50
periodSeconds: 20
failureThreshold: 6
---
apiVersion: v1
kind: Service
metadata:
name: servico-bff
namespace: teste5-prod
spec:
selector: { app: servico-bff }
ports: [{ port: 80, targetPort: 8083 }]
+96
View File
@@ -0,0 +1,96 @@
# =============================================================================
# P4.9 — a SPA, e com ela o roteamento interno de /bff e /api (P4.6).
#
# `teste5` 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: teste5-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.67-csp-script
imagePullPolicy: Never
ports: [{ containerPort: 80 }]
env:
- { name: ATM_KC_URL, value: "https://auth.athleticmap.com" }
- { name: ATM_REALM, value: "athleticmap-9" }
- { name: ATM_CLIENT, value: "spa" }
- { name: ATM_TENANT, value: "teste5" }
- { name: ATM_PLANO, value: "bronze" }
# 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.
# NOME COMPLETO, e nao o curto. A diferenca nao e estetica.
#
# O nginx roteia /bff e /api com proxy_pass de VARIAVEL, e ai ele
# resolve por requisicao usando a diretiva resolver. E o resolver do
# nginx NAO le os dominios de busca do /etc/resolv.conf: ele pergunta
# o nome cru ao CoreDNS, que responde NXDOMAIN para servico-bff.
#
# O sintoma e 502 com could not be resolved (3: Host not found) no log
# do nginx — enquanto getent hosts servico-bff DENTRO do mesmo pod
# resolve, porque o getent usa o search. Medido em 2026-09-24; e por
# isso que o escolinha sempre usou FQDN aqui, e este molde nao usava.
- { name: ATM_BFF_URL, value: "http://servico-bff.teste5-prod.svc.cluster.local" }
# /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.teste5-prod.svc.cluster.local" }
- { name: ATM_API_CORE_URL, value: "http://runtime-core.teste5-prod.svc.cluster.local" }
- { name: ATM_API_OPERACAO_URL, value: "http://runtime-operacao.teste5-prod.svc.cluster.local" }
- { name: ATM_API_SAUDE_URL, value: "http://runtime-saude.teste5-prod.svc.cluster.local" }
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: teste5-prod
spec:
selector: { app: frontend-spa }
ports: [{ port: 80, targetPort: 80 }]