diff --git a/tenants/escolinha-de-teste-2/00-namespace-quota-netpol.yaml b/tenants/escolinha-de-teste-2/00-namespace-quota-netpol.yaml new file mode 100644 index 0000000..308a68e --- /dev/null +++ b/tenants/escolinha-de-teste-2/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: escolinha-de-teste-2-prod + labels: + kubernetes.io/metadata.name: escolinha-de-teste-2-prod + athleticmap.io/tenant: escolinha-de-teste-2 +--- +apiVersion: v1 +kind: ResourceQuota +metadata: + name: cota + namespace: escolinha-de-teste-2-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: escolinha-de-teste-2-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/tenants/escolinha-de-teste-2/05-sealed-db-credentials.yaml b/tenants/escolinha-de-teste-2/05-sealed-db-credentials.yaml new file mode 100644 index 0000000..3018cfa --- /dev/null +++ b/tenants/escolinha-de-teste-2/05-sealed-db-credentials.yaml @@ -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 `escolinha-de-teste-2-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: escolinha-de-teste-2-prod +spec: + encryptedData: + password: AgBitfhOYwTir8dKqEa/x61WKyFMpXvWClqovaFZOCJH3BtJZKh57U0RSy2Uso5lgupRZyrC0BnqVZMsjCwHJRX+5WRaci3VopST4RlxLA/ITXP6ElHkj+u48BlY0dMkygB2ksRPt6tTXuXlCqp7X0x4TKBYOuTOZ7G4SG72XgzOQnkp9OB8RTC/hu3VQ98g3STF2dyroEHiMrBskLnY14iqYsydRkkHecKqI24lV9qXmvNBrol+ikX9+bscwMwwMvZb2Mjpta4sPA7dqDwI1F/iVJc2rFWe9He8hZXcxWEIaYp7NzBrx5Ow6AkowNbH9ENmT3LU72bRXoygxKZcn5eLGBBuWLO92RUIoUCYMYfz5NAOAy8oeV5f7xRYuPElmKaL1mrgb9SqdyGWF0nnXJvJJ+X8niMG4ikGhzoAzWyaPXky4SbsBKKXbZHaXgFJKUQXFY7yzZzkLW/kYFTQT+OYWjGvCp0mn3bAJG4p7If8v4Uk75loBaWUV38tle7cvdKpuB3VdZDdUqTGI4nGcK9NdFDU53G9vWz35d4oMq++1S8LI9CENIfbcajGRHGFfTcsq4bjEU/0ocenk0kX8nbAj2fWGpeXwXnL7AIw35FVRxGIQ4Rxv91ivFFejtkIoOlWolQBlQAiWjAIjEq/9ZatwvQkPW/oTdH6WsLEUKrLfJ4rU5r6wjyGoMD7WcpE+jVQcZkIztv6U4hFwABQIeIFlEtoGDBapTjLhl8StgilgNm9ojgu3CF55rD3 + template: + metadata: + name: db-credentials + namespace: escolinha-de-teste-2-prod + type: Opaque diff --git a/tenants/escolinha-de-teste-2/16-rolebinding-provisionamento.yaml b/tenants/escolinha-de-teste-2/16-rolebinding-provisionamento.yaml new file mode 100644 index 0000000..27b6b19 --- /dev/null +++ b/tenants/escolinha-de-teste-2/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: escolinha-de-teste-2-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/tenants/escolinha-de-teste-2/30-ingress.yaml b/tenants/escolinha-de-teste-2/30-ingress.yaml new file mode 100644 index 0000000..17259c5 --- /dev/null +++ b/tenants/escolinha-de-teste-2/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: escolinha-de-teste-2-prod +spec: + redirectScheme: + scheme: https + permanent: true +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: escolinha-de-teste-2 + namespace: escolinha-de-teste-2-prod + annotations: + cert-manager.io/cluster-issuer: letsencrypt-prod + # O middleware e por namespace: `-@kubernetescrd`. + traefik.ingress.kubernetes.io/router.middlewares: escolinha-de-teste-2-prod-redirect-https@kubernetescrd +spec: + ingressClassName: traefik + tls: + - hosts: + - escolinha-de-teste-2.athleticmap.com + secretName: escolinha-de-teste-2-tls + rules: + - host: escolinha-de-teste-2.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/tenants/escolinha-de-teste-2/60-monitoring-netpol.yaml b/tenants/escolinha-de-teste-2/60-monitoring-netpol.yaml new file mode 100644 index 0000000..99436fd --- /dev/null +++ b/tenants/escolinha-de-teste-2/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: escolinha-de-teste-2-prod +spec: + podSelector: {} + policyTypes: [Ingress] + ingress: + - from: + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: monitoring + ports: + - protocol: TCP + port: 8083 diff --git a/tenants/escolinha-de-teste-2/70-runtime-core.yaml b/tenants/escolinha-de-teste-2/70-runtime-core.yaml new file mode 100644 index 0000000..0917c20 --- /dev/null +++ b/tenants/escolinha-de-teste-2/70-runtime-core.yaml @@ -0,0 +1,63 @@ +# ============================================================================= +# P4.9 — runtime-core — configuracao, cadastro, organizacao, documentos, agenda +# +# `escolinha-de-teste-2` 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: escolinha-de-teste-2-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_escolinha-de-teste-2" } + - { name: SPRING_DATASOURCE_USERNAME, value: "r_escolinha-de-teste-2" } + - 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-7/protocol/openid-connect/certs" } + - { name: ATM_ISSUER, value: "https://auth.athleticmap.com/realms/athleticmap-7" } + - { name: ATM_TENANT, value: "escolinha-de-teste-2" } + - { 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: escolinha-de-teste-2-prod +spec: + selector: { app: runtime-core } + ports: [{ port: 80, targetPort: 8083 }] diff --git a/tenants/escolinha-de-teste-2/71-runtime-operacao.yaml b/tenants/escolinha-de-teste-2/71-runtime-operacao.yaml new file mode 100644 index 0000000..f54c6bb --- /dev/null +++ b/tenants/escolinha-de-teste-2/71-runtime-operacao.yaml @@ -0,0 +1,82 @@ +# ============================================================================= +# P4.9 — runtime-operacao — planejamento, campeonato, administrativo, financeiro +# +# `escolinha-de-teste-2` 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: escolinha-de-teste-2-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_escolinha-de-teste-2" } + - { name: SPRING_DATASOURCE_USERNAME, value: "r_escolinha-de-teste-2" } + - 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-7/protocol/openid-connect/certs" } + - { name: ATM_ISSUER, value: "https://auth.athleticmap.com/realms/athleticmap-7" } + - { name: ATM_TENANT, value: "escolinha-de-teste-2" } + - { 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: escolinha-de-teste-2-prod +spec: + selector: { app: runtime-operacao } + ports: [{ port: 80, targetPort: 8083 }] diff --git a/tenants/escolinha-de-teste-2/72-runtime-saude.yaml b/tenants/escolinha-de-teste-2/72-runtime-saude.yaml new file mode 100644 index 0000000..8498b8c --- /dev/null +++ b/tenants/escolinha-de-teste-2/72-runtime-saude.yaml @@ -0,0 +1,66 @@ +# ============================================================================= +# P4.9 — runtime-saude — saude, nutricao, assistencia-social, pedagogico +# +# `escolinha-de-teste-2` 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: escolinha-de-teste-2-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_escolinha-de-teste-2" } + - { name: SPRING_DATASOURCE_USERNAME, value: "r_escolinha-de-teste-2" } + - 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-7/protocol/openid-connect/certs" } + - { name: ATM_ISSUER, value: "https://auth.athleticmap.com/realms/athleticmap-7" } + - { name: ATM_TENANT, value: "escolinha-de-teste-2" } + - { 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: escolinha-de-teste-2-prod +spec: + selector: { app: runtime-saude } + ports: [{ port: 80, targetPort: 8083 }] diff --git a/tenants/escolinha-de-teste-2/95-frontend-spa.yaml b/tenants/escolinha-de-teste-2/95-frontend-spa.yaml new file mode 100644 index 0000000..05246a8 --- /dev/null +++ b/tenants/escolinha-de-teste-2/95-frontend-spa.yaml @@ -0,0 +1,85 @@ +# ============================================================================= +# P4.9 — a SPA, e com ela o roteamento interno de /bff e /api (P4.6). +# +# `escolinha-de-teste-2` 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: escolinha-de-teste-2-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.com" } + - { name: ATM_REALM, value: "athleticmap-7" } + - { name: ATM_CLIENT, value: "spa" } + - { name: ATM_TENANT, value: "escolinha-de-teste-2" } + - { 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. + - { 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: escolinha-de-teste-2-prod +spec: + selector: { app: frontend-spa } + ports: [{ port: 80, targetPort: 80 }]