Arquitetura e Kubernetes
Tudo no cluster `ecomsmart-hostinger`. Imagens no registry próprio. Deploy por pipeline do GitLab self-hosted. Nenhum `kubectl apply` manual fora de teste.
Restrição de capacidade do cluster — ler antes de planejar deploy
Levantamento de 17/09/2026 (registrado na skill /firecrawl-ecomsmart): os quatro nós do cluster estavam com 98 a 99% da CPU alocada em requests. Isso é anterior a este projeto e não foi causado por ele.
Consequência prática e imediata para o SmartHub Apps:
- Este site (
apps-site) cabe: são 2 réplicas de 20m, 40m de CPU no total. - A aplicação Django completa não cabe hoje. O desenho abaixo pede cerca de 1100m de CPU em requests entre
radar-web,radar-workereradar-render. - Qualquer deploy novo pode ficar
PendingcomFailedSchedulingaté o cluster ganhar capacidade ou algum serviço reduzir requests.
Decisão: publicar o site agora (cabe), e antes do prompt P0.1 rodar uma auditoria de requests do cluster. Ou some um worker, ou reduzimos os requests dos serviços existentes que estão superdimensionados. Está no P0.0.
Vale também investigar o Longhorn: vários volumes apareceram faulted com ReplicaSchedulingFailure: insufficient storage mesmo com mais de 100 GB livres em todos os nós. É problema pré-existente e afeta qualquer PVC novo.
Domínios
| Host | O que serve | Proxy Cloudflare |
|---|---|---|
apps.ecomsmart.com.br | Este site — planejamento e execução | DNS only |
radar.ecomsmart.com.br | A aplicação: Raio-X, Radar de Preço, Radar de Anúncios | DNS only |
roi.ecomsmart.com.br | Painel de investido contra faturado | DNS only |
api.radar.ecomsmart.com.br | Webhooks de Stripe, Nuvemshop, Meta | DNS only |
Proxy desligado porque o cert-manager valida por HTTP-01 e o proxy da Cloudflare quebra o desafio. Para ligar o proxy depois, migrar para DNS-01.
Fluxo da máquina
Meta Ads (frio) Google Ads (demanda) App Nuvemshop / Tray
| | |
+------------------------+--------------------------+
|
Landing por produto (radar.ecomsmart.com.br)
captura utm_* + gclid + fbclid + fbp/fbc
|
+---------------------------------+
| CONECTAR LOJA (OAuth) | gratis, sem cartao
| Raio-X gerado em ~3 min |
+---------------------------------+
|
LEAD SCORE automatico
faturamento · pedidos/mes · % compra unica
investe em trafego? · tem WhatsApp no site?
|
+---------------------+---------------------+
| |
fatura < R$ 30k fatura >= R$ 30k
nutricao por e-mail FILA DO VENDEDOR em 24h
oferta Radar R$ 97/ano |
| |
v v
CHECKOUT STRIPE -------------------> Ligacao com o Raio-X na tela
R$ 97/ano · CARTAO ARQUIVADO Upgrade = 1 clique
| |
v v
Meta CAPI + Google offline conv. HUB R$ 299/mes
(algoritmo aprende com COMPRA REAL) trial 7d · R$ 99 x3
|
v
+-------------------------------+
| PAINEL ROI |
| Meta + Google spend |
| x Stripe (MRR, receita, LTV) |
| = CAC, ROAS, payback |
+-------------------------------+
Serviços no cluster
Namespace producao, o mesmo do Hub.
| Deployment | Réplicas | Requests | Limits | Papel |
|---|---|---|---|---|
apps-site | 2 | 20m / 32Mi | 100m / 64Mi | Este site, nginx estático |
radar-web | 2 | 200m / 384Mi | 1000m / 1Gi | Django, gunicorn |
radar-worker | 2 | 200m / 512Mi | 1000m / 1Gi | Celery: sync, Raio-X, alertas |
radar-beat | 1 | 50m / 128Mi | 200m / 256Mi | Celery beat, strategy: Recreate |
radar-render | 1 | 300m / 768Mi | 1500m / 1,5Gi | Chromium: PDF do Raio-X e criativos de anúncio |
Postgres e Redis: reaproveitar as instâncias que já existem no cluster, criando apenas banco e usuário novos. Não subir outro Postgres.
Firecrawl — o crawler já existe
O cluster já tem Firecrawl self-hosted no namespace firecrawl, em firecrawl.ecomsmart.com.br, com Basic Auth, deployado por Helm e pipeline próprio (skill /firecrawl-ecomsmart, repo ecomsmart/infrastructure/firecrawl).
Isso muda o desenho do Radar de Preço de forma relevante: não construímos motor de crawl. O radar-worker chama POST /v2/scrape do Firecrawl e recebe o HTML ou o markdown já renderizado, inclusive de páginas que dependem de JavaScript. O que fica do nosso lado é o que é específico do produto:
- Descoberta e persistência do seletor de preço
- Conversão da string para
Decimalcomprice-parser - Agendamento, teto de orçamento por plano e antiflood
- Extratores dedicados por marketplace
Limitações a respeitar, já conhecidas:
- Sem
OPENAI_API_KEYconfigurada, os formatos que dependem de LLM (extract) não funcionam. Para extração de preço isso não faz falta: usamos JSON-LD e seletor. - A persistência do Postgres interno do NuQ está em
emptyDir. Reiniciar o pod perde a fila em andamento. Nosso agendamento precisa ser idempotente e reagendar o que não voltou — não confiar na fila do Firecrawl como fonte de verdade. - Proxies residenciais continuam sendo nossos. O Firecrawl resolve renderização, não bloqueio por IP.
Orçamento de recursos
| Componente | RAM |
|---|---|
apps-site | 128 Mi |
radar-web | 2 Gi |
radar-worker | 2 Gi |
radar-beat | 256 Mi |
radar-render | 1,5 Gi |
| Postgres (fatia deste projeto) | 2 Gi |
| Redis (fatia) | 512 Mi |
| Total | cerca de 8,4 Gi |
Se o cluster não tiver essa folga, a ordem de corte é radar-render (gera PDF sob demanda em vez de em fila) e depois reduzir radar-worker para uma réplica.
Estrutura do repositório
smarthub/apps
README.md
docs/ planejamento em markdown (vira este site)
prompts/ os 20 prompts de execução
brand/ tokens.css, simbolo-e.svg, base.html, manual v2.4
build/
build.py gerador do site estático
md.py renderizador markdown sem dependência
site/
assets/app.css identidade v2.4, claro e escuro
assets/app.js tema, menu, copiar código
*.html gerado pelo build
k8s/
base/ deployment, service, ingress, configmap
overlays/producao/ kustomization
Dockerfile multi-stage: python constrói, nginx serve
nginx.conf
.gitlab-ci.yml
Pipeline
Estágios build → deploy.
build — imagem multi-stage. Estágio 1 em python:3.12-slim roda python3 build/build.py. Estágio 2 em nginx:alpine copia site/. Envia para registry.ecomsmart.com.br/smarthub/apps:$CI_COMMIT_SHORT_SHA e :latest.
deploy — só em main. export KUBECONFIG=$KUBECONFIG_ECOMSMART no before_script, aplica com kubectl apply -k k8s/overlays/apps, atualiza a imagem com kubectl set image e espera com kubectl rollout status --timeout=300s.
Runner com tags self-hosted,kubernetes,deploy.
Pull secret
O kubelet precisa puxar a imagem do registry privado muito depois do pipeline terminar. Usar Deploy Token do projeto com escopo apenas read_registry, nome k8s-registry-pull.
Nunca usar $CI_REGISTRY_PASSWORD para isso: esse token só vale durante o job, e o pod vai falhar em todo restart depois.
Segredos
Nada de chave em repositório.
| Credencial | Onde fica | Como chega no pod |
|---|---|---|
| Kubeconfig | Variável de grupo KUBECONFIG_ECOMSMART (tipo file, protegida) | Só no pipeline |
| Registry pull | Deploy Token k8s-registry-pull | imagePullSecrets |
| Stripe | Secret radar-stripe | envFrom |
| Meta (app, token de sistema, ad account, dataset) | Secret radar-meta | envFrom |
| Google Ads | Reaproveitar o Secret do mcp-google-ads | envFrom |
| Nuvemshop, Tray, Shopify | Secret radar-conectores | envFrom |
| SMTP (SMTP2GO) | Secret ecomsmart-hub-app já existente | envFrom |
| Cloudflare | Ainda não cadastrado no GitLab — precisa ser feito manualmente | Variável CLOUDFLARE_API_TOKEN |
Gotchas conhecidos do cluster
Registrados na skill /gitlab-ecomsmart e válidos aqui:
- Regra de host duplicada em Ingress. Já apareceu Ingress ganhando uma segunda regra de host por conta própria, apontando para a porta interna errada, dando 503 com todo o resto certo. Depois de aplicar, conferir que nenhum host aparece em dois Ingress.
GITLAB_OMNIBUS_CONFIGsó é escrito no primeiro boot com PVC vazio. Não vale para este projeto, mas vale lembrar ao mexer no GitLab.monitoring_whitelistprecisa incluir a subnet dos nodes (2.25.167.0/24), porque os probes do kubelet chegam com o IP público do node.
Observabilidade
O cluster já tem Grafana, Prometheus e a stack ELK no namespace logging.
Métricas a expor:
radar_coletas_total{status}
radar_coleta_duracao_segundos
radar_proxy_custo_estimado_centavos
radar_monitoramentos_ativos
radar_raiox_duracao_segundos
radar_leads_total{fila}
radar_capi_envios_total{status}
Logs em JSON com structlog, para cair estruturados no Elasticsearch.