bb5e650d7a
A etapa BANCO_CRIADO reinicia o PgBouncer e espera ele voltar. Com 5, o backoff da 155s — menos do que o pod novo levou nas tres medicoes de hoje, e os dois clientes pagos precisaram de retomada a mao. Com 8 da 515s, dentro da janela.
298 lines
16 KiB
YAML
298 lines
16 KiB
YAML
# servico-captacao — recebe o formulario da landing page.
|
|
#
|
|
# SO EXISTE NESTE TENANT, e e por isso que o tenant `plataforma` precisa
|
|
# existir: lead e prospecto DA ATHLETIC MAP, e gravar isso no banco de um
|
|
# cliente seria o oposto do que a Fase 2 inteira fez. Ver P2.8.
|
|
#
|
|
# E o unico servico do repositorio com rota PUBLICA (sem token): quem
|
|
# preenche o formulario ainda nao e cliente. O que substitui a autenticacao
|
|
# esta no proprio servico — CORS restrito, rate limit por IP e consent
|
|
# obrigatorio.
|
|
# Bounded context "financeiro" (2a onda): Cobranca + ApuracaoRateio (centros de custo).
|
|
# Schema proprio "financeiro" (Flyway). Roteado em /api/public/leads no ingress.
|
|
---
|
|
apiVersion: apps/v1
|
|
kind: Deployment
|
|
metadata:
|
|
name: servico-captacao
|
|
namespace: plataforma-prod
|
|
labels:
|
|
app: servico-captacao
|
|
athleticmap.io/contexto: financeiro
|
|
spec:
|
|
replicas: 1
|
|
selector: { matchLabels: { app: servico-captacao } }
|
|
template:
|
|
metadata:
|
|
labels:
|
|
app: servico-captacao
|
|
athleticmap.io/contexto: financeiro
|
|
spec:
|
|
# P5.6/P5.9: ServiceAccount DEDICADA (ver 16-rbac-provisionamento.yaml).
|
|
# Com a `default`, a permissao de ler e escalar Deployment dos tenants
|
|
# valeria para qualquer pod que subisse sem `serviceAccountName`.
|
|
serviceAccountName: atm-provisionamento
|
|
volumes:
|
|
- { name: gitops, emptyDir: {} }
|
|
# P5.3 — a copia de trabalho do GitOps, refeita a cada subida do pod.
|
|
#
|
|
# CLONE RASO E ANONIMO. O Gitea aceita leitura sem credencial neste
|
|
# repositorio (conferido em 2026-09-22), e leitura e tudo o que a etapa 1
|
|
# precisa: ela RENDERIZA o molde para `tenants/<nome>`. Empurrar e a etapa
|
|
# ARGO_SINCRONIZADO, que depende de credencial e continua bloqueada.
|
|
#
|
|
# `emptyDir`, e nao PVC: a copia e descartavel por desenho. Se o pod morrer
|
|
# no meio de um provisionamento, o que ficou pela metade some junto — e isso
|
|
# e melhor do que um clone velho e divergente sobrevivendo entre reinicios.
|
|
#
|
|
# A imagem e a do `act_runner`, que JA esta no no e tem git 2.45.2. Nao ha
|
|
# registry aqui (`imagePullPolicy: Never`), entao construir uma imagem so
|
|
# para ter git custaria mais do que reusar esta.
|
|
#
|
|
# O endereco e o INTERNO, o mesmo que o ApplicationSet do ArgoCD usa — o
|
|
# externo passa por Ingress e TLS, e nao ha motivo para sair do cluster
|
|
# para buscar o que esta dentro dele. Depende do `19-netpol-gitea.yaml`.
|
|
initContainers:
|
|
- name: clonar-gitops
|
|
image: docker.io/gitea/act_runner:0.2.11
|
|
imagePullPolicy: Never
|
|
command: ["/bin/sh", "-c"]
|
|
args:
|
|
- |
|
|
set -e
|
|
git clone --depth 1 http://gitea-http.gitea.svc:3000/atmadmin/athletic-map-deploy.git /gitops
|
|
test -d /gitops/moldes/consolidado || { echo "molde ausente no clone"; exit 1; }
|
|
# O container principal roda como o usuario app (uid 999), e o clone
|
|
# e feito como root. Sem esta linha a copia de trabalho fica
|
|
# so-leitura para ele, e a etapa MANIFESTOS_GERADOS falha ao escrever
|
|
# tenants/<nome> — medido em 2026-09-22.
|
|
#
|
|
# chown, e nao fsGroup: fsGroup ajusta o GRUPO e depende de o
|
|
# diretorio ter permissao de grupo, o que o git nao garante arquivo a
|
|
# arquivo. Entregar a posse e explicito e nao depende de umask.
|
|
chown -R 999:999 /gitops
|
|
echo "clone pronto: $(ls /gitops/moldes/consolidado | wc -l) arquivo(s) de molde"
|
|
volumeMounts:
|
|
- { name: gitops, mountPath: /gitops }
|
|
resources:
|
|
requests: { cpu: 10m, memory: 32Mi }
|
|
limits: { cpu: 200m, memory: 128Mi }
|
|
containers:
|
|
- name: servico-captacao
|
|
image: docker.io/library/servico-captacao:2.9-corrigir-papel
|
|
imagePullPolicy: Never
|
|
ports: [{ containerPort: 8083 }]
|
|
volumeMounts:
|
|
- { name: gitops, mountPath: /gitops }
|
|
env:
|
|
- { name: SPRING_DATASOURCE_URL, value: "jdbc:postgresql://postgres:5432/athleticmap?sslmode=require" }
|
|
- { name: SPRING_DATASOURCE_USERNAME, value: "atm" }
|
|
- name: SPRING_DATASOURCE_PASSWORD
|
|
valueFrom:
|
|
secretKeyRef: { name: db-credentials, key: password }
|
|
- { name: ATM_TENANT, value: "plataforma" }
|
|
# CORS: so a landing page. Formulario publico com CORS aberto
|
|
# vira endpoint de spam de qualquer site. Ver P2.8.
|
|
# A origem do navegador que faz o POST. A landing page e servida
|
|
# pelo proprio nginx da SPA (`frontend/nginx.conf`, `location =
|
|
# /lp-escolinhas`), que neste tenant e o Service `frontend-spa` —
|
|
# logo a origem e o host do tenant, e nao o apex de marketing.
|
|
# `@CrossOrigin` aqui aceita UMA origem: o placeholder vira string
|
|
# literal, entao virgula nao separa em duas. Se a LP for publicada
|
|
# noutro host, este valor muda junto ou o formulario toma CORS.
|
|
- { name: CAPTACAO_ORIGEM, value: "https://plataforma.athleticmap.com" }
|
|
- { name: CAPTACAO_RETENCAO_MESES, value: "24" }
|
|
# P5.1 — o token do webhook de ASSINATURA. Sem ele o endpoint
|
|
# RECUSA TUDO, de proposito, e o log diz exatamente isso. O mesmo
|
|
# valor precisa estar cadastrado no painel do Asaas, na conta da
|
|
# Athletic Map (nao na do cliente).
|
|
- name: PLATAFORMA_WEBHOOK_TOKEN
|
|
valueFrom:
|
|
secretKeyRef: { name: plataforma-webhook-token, key: token }
|
|
# P5.2 — o token de consulta do estado de um provisionamento. E
|
|
# segredo NOSSO, diferente do de cima: o Asaas nao tem o que ler
|
|
# aqui dentro. Interino, no cabecalho `x-atm-admin-token`; o lugar
|
|
# definitivo e uma tela com papel do Keycloak.
|
|
- name: PLATAFORMA_ADMIN_TOKEN
|
|
valueFrom:
|
|
secretKeyRef: { name: plataforma-admin-token, key: token }
|
|
# P5.3 — de onde vem o certificado PUBLICO do sealed-secrets.
|
|
#
|
|
# A URL, e nao um arquivo: o controlador ROTACIONA a chave a cada
|
|
# 30 dias (medido: 16/06, 16/07, 15/08, 14/09), e um certificado
|
|
# versionado nasce certo e fica velho em um mes, EM SILENCIO —
|
|
# selar com o antigo continua funcionando enquanto ele guardar as
|
|
# privadas anteriores. Lido a cada provisionamento, vem sempre o
|
|
# corrente.
|
|
#
|
|
# Depende do `18-netpol-sealed-secrets.yaml`: sem ele a conexao
|
|
# nao sai (o ipBlock da deny-cross-tenant nao vale depois do DNAT).
|
|
- { name: PROVISIONAMENTO_CERT_SELAGEM, value: "http://sealed-secrets-controller.kube-system:8080/v1/cert.pem" }
|
|
# P5.3 — a copia de trabalho do GitOps e o molde, no MESMO volume.
|
|
#
|
|
# O initContainer clona o repositorio em /gitops na subida; dentro
|
|
# dele, `moldes/consolidado` e o molde e `tenants/<nome>` e onde a
|
|
# etapa escreve. Um volume serve aos dois.
|
|
#
|
|
# Vazio falhava FECHADO, nomeando a propriedade — era o estado ate
|
|
# 2026-09-22. Escrever num diretorio temporario que ninguem commita
|
|
# daria a impressao de que a etapa funcionou.
|
|
- { name: PROVISIONAMENTO_GITOPS, value: "/gitops" }
|
|
# P5.3 — o repositorio remoto para onde o commit e EMPURRADO.
|
|
#
|
|
# O nome DE DENTRO do cluster, e nao o `git.<ip>.nip.io` que se usa
|
|
# de fora: o de fora sai do cluster e volta, e a NetworkPolicy de
|
|
# egress (19-netpol-gitea) permite o caminho interno, nao o externo.
|
|
#
|
|
# `gitea-http` e headless (ClusterIP None), entao o DNS devolve o IP
|
|
# do POD. Isso e o que faz a netpol por `podSelector` funcionar aqui
|
|
# — `ipBlock` com ClusterIP nao funciona atravessando namespace,
|
|
# porque o k3s avalia a politica DEPOIS do DNAT.
|
|
- { name: PROVISIONAMENTO_GITOPS_URL,
|
|
value: "http://gitea-http.gitea.svc:3000/atmadmin/athletic-map-deploy.git" }
|
|
# P5.3 — quem empurra. ESTA CREDENCIAL IMPLANTA EM PRODUCAO: o
|
|
# ApplicationSet tem gerador de diretorio sobre `tenants/*` com
|
|
# selfHeal, entao a pasta empurrada vira Application e e aplicada
|
|
# sozinha. Por isso e um usuario proprio do Gitea, com escrita so
|
|
# neste repositorio, e um TOKEN revogavel — nao a conta atmadmin.
|
|
- name: PROVISIONAMENTO_GITOPS_USUARIO
|
|
valueFrom:
|
|
secretKeyRef: { name: provisionamento-credentials, key: gitops-usuario }
|
|
- name: PROVISIONAMENTO_GITOPS_SENHA
|
|
valueFrom:
|
|
secretKeyRef: { name: provisionamento-credentials, key: gitops-senha }
|
|
# P5.4 — a Admin API do Keycloak, para criar o realm do tenant novo.
|
|
#
|
|
# `AdminDoKeycloak` pega token por `grant_type=password` no client
|
|
# `admin-cli` do realm `master`, entao isto e um usuario DE LA, e nao
|
|
# do realm `athleticmap`.
|
|
#
|
|
# Hoje pode ser o admin do master, que pode tudo. O certo e um
|
|
# usuario de servico com `create-realm` e nada mais — esta no diario.
|
|
- name: KEYCLOAK_ADMIN_USUARIO
|
|
valueFrom:
|
|
secretKeyRef: { name: provisionamento-credentials, key: keycloak-admin-usuario }
|
|
- name: KEYCLOAK_ADMIN_SENHA
|
|
valueFrom:
|
|
secretKeyRef: { name: provisionamento-credentials, key: keycloak-admin-senha }
|
|
- { name: PROVISIONAMENTO_MOLDE, value: "/gitops/moldes/consolidado" }
|
|
- { name: PROVISIONAMENTO_PLANO_PADRAO, value: "bronze" }
|
|
# ── PRECO DE TESTE, TEMPORARIO ─────────────────────────
|
|
#
|
|
# A venda de teste de 2026-09-24 exercita o caminho inteiro —
|
|
# link, pagamento, webhook, provisionamento — e nao faz sentido
|
|
# gastar R$ 170 para isso. Sobreposicao por ambiente, e nao
|
|
# mudanca no `application.yml`: assim nao ha nada a reverter no
|
|
# codigo, e tirar e remover este bloco.
|
|
#
|
|
# ENQUANTO ISTO ESTIVER AQUI, o plano bronze custa este valor.
|
|
# Nao venda bronze de verdade sem tirar.
|
|
- { name: PLANO_BRONZE_MENSAL, value: "5.00" }
|
|
# ── fim do preco de teste ──────────────────────────────
|
|
# P5.8 — a chave do Asaas, para CRIAR o link de assinatura.
|
|
#
|
|
# E o mesmo Secret que o `servico-financeiro` usa, e por isso
|
|
# nao ha credencial nova a selar: muda so quem a le. Os dois
|
|
# fluxos continuam distintos — o financeiro cobra o ATLETA, este
|
|
# cobra o TENANT.
|
|
#
|
|
# `optional: true` de proposito: um ambiente sem Asaas sobe do
|
|
# mesmo jeito, e a venda que precisar da chave recusa nomeando a
|
|
# propriedade. Derrubar o pod tiraria do ar o provisionamento e a
|
|
# captura de lead, que nao dependem disso.
|
|
- name: ASAAS_API_KEY
|
|
valueFrom:
|
|
secretKeyRef:
|
|
name: asaas-credentials
|
|
key: api-key
|
|
optional: true
|
|
- { name: CAPTACAO_LIMITE_POR_IP, value: "5" }
|
|
# Destino do aviso ao comercial. Vazio = lead e gravado e o
|
|
# aviso vira log sem PII. Preencher quando houver SMTP.
|
|
- { name: CAPTACAO_EMAIL_COMERCIAL, value: "" }
|
|
# ── SMTP ─────────────────────────────────
|
|
#
|
|
# HOSTINGER, e nao Google. O dominio `athleticmap.com` tem MX em
|
|
# mx1/mx2.hostinger.com e SPF `include:_spf.mail.hostinger.com`:
|
|
# mandar pelo Gmail falharia SPF justamente no e-mail que leva o
|
|
# link de senha de quem acabou de pagar. Medido em 2026-09-24 — a
|
|
# credencial e ACEITA em smtp.hostinger.com:587 e recusada (535)
|
|
# em smtp.gmail.com e em smtp.titan.email.
|
|
- { name: SMTP_HOST, value: "smtp.hostinger.com" }
|
|
- { name: SMTP_PORTA, value: "587" }
|
|
- { name: SMTP_USUARIO, value: "nao-responda@athleticmap.com" }
|
|
# `nao-responda@` de proposito: a senha dela vive no cluster, e uma
|
|
# caixa que nao guarda nada limita o estrago se o segredo vazar. Sem
|
|
# o RESPONDER_PARA, quem acabou de pagar e responde fala com o vazio.
|
|
- { name: SMTP_RESPONDER_PARA, value: "contato@athleticmap.com" }
|
|
- { name: SMTP_STARTTLS, value: "true" }
|
|
- { name: SMTP_AUTENTICAR, value: "true" }
|
|
# Vazia, o realm nasceria com um SMTP que so falha na hora de ENVIAR
|
|
# — depois de o cliente ter conta. A etapa recusa antes disso.
|
|
- name: SMTP_SENHA
|
|
valueFrom:
|
|
secretKeyRef: { name: smtp-credentials, key: senha }
|
|
# O molde do realm. Sem ele a etapa recusa nomeando a propriedade; o
|
|
# arquivo vive no mesmo clone do GitOps que o /gitops monta.
|
|
- { name: PROVISIONAMENTO_REALM_TEMPLATE, value: "/gitops/moldes/realm-do-tenant.json" }
|
|
# ── o banco do cliente novo (etapa BANCO_CRIADO) ──────────────
|
|
#
|
|
# DIRETO no pg-compartilhado, e nao pelo PgBouncer: o pool so serve
|
|
# databases que ja estao na lista dele, e aqui o database ainda NAO
|
|
# existe. E a netpol egress-banco-compartilhado ja permite os dois.
|
|
# 8, e nao as 5 do padrao.
|
|
#
|
|
# A etapa BANCO_CRIADO reinicia o PgBouncer e espera ele voltar servindo o
|
|
# tenant novo. Com 5 tentativas o backoff da 5+10+20+40+80 = 155s, e nas
|
|
# tres medicoes de 2026-09-24 o pod novo demorou mais que isso — os dois
|
|
# clientes pagos precisaram de uma retomada a mao CADA.
|
|
#
|
|
# Com 8 e o teto de 120s: 515s. A janela de 20 minutos da execucao continua
|
|
# cabendo, entao isto nao troca uma espera por um incidente silencioso.
|
|
- { name: PROVISIONAMENTO_TENTATIVAS, value: "8" }
|
|
- { name: PROVISIONAMENTO_PG_ADMIN_URL, value: "jdbc:postgresql://pg-compartilhado:5432/{database}" }
|
|
- { name: PROVISIONAMENTO_PG_ADMIN_USUARIO, value: "postgres" }
|
|
- name: PROVISIONAMENTO_PG_ADMIN_SENHA
|
|
valueFrom:
|
|
secretKeyRef: { name: pg-compartilhado-admin, key: password }
|
|
# As etapas de migracao e de catalogos, que conectam num banco que JA
|
|
# existe. Tambem direto: elas rodam com a conta de administracao, e
|
|
# passar pelo pool so acrescentaria uma peca ao caminho.
|
|
- { name: PROVISIONAMENTO_BANCO_URL, value: "jdbc:postgresql://pg-compartilhado:5432/athleticmap_{tenant}" }
|
|
- { name: PROVISIONAMENTO_BANCO_USUARIO, value: "postgres" }
|
|
- name: PROVISIONAMENTO_BANCO_SENHA
|
|
valueFrom:
|
|
secretKeyRef: { name: pg-compartilhado-admin, key: password }
|
|
resources:
|
|
requests: { cpu: 150m, memory: 320Mi }
|
|
limits: { cpu: "1", memory: 768Mi }
|
|
# `/actuator/health/readiness`, e nao um health de dominio.
|
|
#
|
|
# A primeira versao apontava para `/api/public/leads/health`, copiado
|
|
# do manifesto do financeiro por analogia. Esse caminho NAO EXISTE:
|
|
# o `LeadController` tem um unico `@PostMapping`, e por desenho — a
|
|
# superficie deste servico publico e exatamente um POST. O pod ficou
|
|
# 0/1 enquanto a aplicacao dizia "Started" no log, que e o sintoma
|
|
# mais confuso possivel.
|
|
readinessProbe:
|
|
httpGet: { path: /actuator/health/readiness, port: 8083 }
|
|
initialDelaySeconds: 25
|
|
periodSeconds: 10
|
|
failureThreshold: 30
|
|
livenessProbe:
|
|
httpGet: { path: /actuator/health/liveness, port: 8083 }
|
|
initialDelaySeconds: 60
|
|
periodSeconds: 20
|
|
failureThreshold: 6
|
|
---
|
|
apiVersion: v1
|
|
kind: Service
|
|
metadata:
|
|
name: servico-captacao
|
|
namespace: plataforma-prod
|
|
labels:
|
|
app: servico-captacao
|
|
spec:
|
|
selector: { app: servico-captacao }
|
|
ports: [{ name: http, port: 80, targetPort: 8083 }]
|