Skip to content
Share

Por que migramos o Viajaflux do Laravel Cloud pro AWS Fargate (e o gotcha de CSS que quase me pegou)

O Viajaflux rodava em Laravel Cloud/Vapor desde sempre. Funcionava bem pra web, mas tinha um limite que foi ficando incômodo conforme o produto cresceu: Vapor é Lambda por baixo, e Lambda não foi feito pra processo longo. Horizon (nossas filas) e Reverb (WebSocket) são exatamente isso — processos que precisam ficar vivos, não uma função que responde e morre. Migramos produção pra AWS Fargate, com mais controle sobre containers de verdade. Registro aqui a arquitetura que ficou, as decisões que tomamos de propósito (e as que evitamos por excesso de engenharia), e um gotcha de build que me pegou no meio do caminho.

Por que Fargate e não só “mais Lambda”

A resposta curta já está acima: Horizon e Reverb são processos de vida longa, e forçar isso em Lambda é nadar contra a maré da própria plataforma. Fargate roda containers de verdade, então a mesma imagem Docker vira quatro processos diferentes rodando em paralelo:

ServiçoO que roda
webRequisições HTTP normais
horizonFilas (Consolidadoras, Operadoras, Gateways, etc.)
schedulerCron do Laravel
reverbWebSocket

Cada um escala de forma independente — hoje web vai de 2 a 6 tasks por CPU, reverb de 1 a 3. Antes, no Vapor, essa separação de processo longo vs. função efêmera era sempre um ponto de fricção.

O que ficou de pé, peça por peça

Cliente → Cloudflare → ALB → task web (Fargate) → RDS + Redis
                                 ↑
                    horizon / scheduler / reverb rodam em paralelo

VPC com subnets públicas (ALB, NAT) e privadas (app, banco, Redis). RDS MySQL 8.4 gerenciado, ElastiCache Redis 7 fazendo cache + filas do Horizon + sessão ao mesmo tempo, Secrets Manager guardando APP_KEY e senha do banco fora da imagem, IAM Roles separadas pra Fargate (baixar imagem, ler segredos) e pra app (acessar S3). Tudo em arm64/Graviton — mais barato, e já era o padrão que usávamos no mail server.

As decisões que tomamos por exclusão, não por padrão

O que não entrou na arquitetura foi tão deliberado quanto o que entrou:

  • CloudFront: não. É o CDN da própria AWS, mas a gente já usa Cloudflare (CDN + cache + WAF + TLS). Rodar os dois ao mesmo tempo é custo duplicado sem ganho — clássico caso de “porque dá pra usar” não é motivo pra usar.
  • Route 53: não. Cloudflare já resolve DNS. A topologia final ficou Cloudflare (DNS + proxy + WAF + TLS) → ALB → Fargate, com um CNAME simples apontando pro ALB. Com Cloudflare em modo proxy, nem precisei correr atrás do certificado ACM de imediato.
  • Multi-AZ no RDS: desligado, por ora. É economia consciente, não descuido — sei exatamente que estou trocando alta disponibilidade automática por custo menor enquanto o volume não justifica o gasto extra. Fica documentado como pendência pra revisitar, não como esquecimento.
  • Read replica: não incluída ainda. O banco é leitura e escrita — o “somente leitura” que às vezes aparece nas minhas ferramentas de análise é restrição da minha conexão de consulta (MCP), não da arquitetura em si. Ligar uma réplica de leitura fica pra quando o volume de relatório pedir.

O custo total ficou entre US$ 130 e 180/mês (ALB + NAT + RDS t4g.small + Redis t4g.micro + 4 tasks Fargate arm64) — redutível desligando NAT em alta disponibilidade e ajustando tamanho de instância, se precisar cortar mais.

Um detalhe que não é opcional: o IP de saída

Toda integração externa do Viajaflux (Wooba, Pagar.me, Asaas, Infotravel) enxerga o tráfego saindo de um único IP fixo — o Elastic IP do NAT Gateway de produção (NAT único, single-AZ). Isso importa na prática porque é literalmente o dado que eu preciso mandar pra cada fornecedor whitelistar.

O gotcha: MaryUI perdendo estilo só no build Docker

Essa foi a parte que mais me ensinou. Depois de subir a imagem, os componentes MaryUI apareciam sem estilo nenhum em produção — mas local, rodando npm run dev, tudo funcionava normal. Comportamento clássico de “funciona na minha máquina”, só que dessa vez com uma causa bem específica.

O Tailwind v4 do projeto está configurado pra escanear vendor/robsontenorio/mary/src/View/Components/**/*.php — porque o MaryUI guarda o Blade inline dentro dos próprios arquivos PHP dos componentes, não em .blade.php separados. O problema é que o .dockerignore exclui /vendor do contexto de build. No stage de assets da imagem Docker, esses arquivos simplesmente não existiam — então o Tailwind fazia o purge de qualquer classe que só aparecia ali dentro. E php artisan view:cache não resolve, porque esse Blade inline só compila em runtime, não em build time.

O fix foi trazer explicitamente esses arquivos pro stage de assets, antes do build do CSS rodar:

# Stage 2 — assets
COPY --from=vendor /app/vendor/robsontenorio/mary/src ./vendor/robsontenorio/mary/src

Confirmei o fix de um jeito bem direto: escolhi classes canário que só existem em componentes MaryUI (avatar-placeholder, file-input, decoration-wavy) e chequei o CSS final. Sem o fix, 448KB e nenhuma dessas classes. Com o fix, 470KB e as três presentes.

Minha conclusão

O gotcha do MaryUI resume bem o motivo de eu documentar decisão de infra em vez de só “fazer funcionar”: não foi um bug de código, foi uma interação entre duas otimizações legítimas (.dockerignore enxuto, Tailwind fazendo purge agressivo) que ninguém planejou colidir. O mesmo vale pra arquitetura inteira — cada “não” que tomei (CloudFront, Route 53, Multi-AZ por ora) é uma aposta consciente, não esquecimento, e só continua sendo uma boa aposta se eu lembrar o motivo depois. É por isso que essa doc de infra vale tanto quanto o Terraform em si.