Desacoplando um monolito na AWS 

Existe um tipo de projeto de cloud que eu chamo de migração de mudança de endereço. O cliente sai de uma VPS, sobe tudo numa EC2 maior, aponta o DNS e comemora. Tecnicamente migrou. Na prática, trocou de endereço e levou o mesmo problema na mudança.

Sei disso porque já peguei ambiente assim depois de pronto, e o sintoma é sempre o mesmo: seis meses depois, alguém pergunta por que a nuvem não trouxe nenhum dos benefícios prometidos, e a resposta é que ninguém desacoplou nada.

Este artigo é sobre o outro caminho, a partir de um projeto real. Desacoplar frontend, backend, dados e observabilidade em camadas independentes, cada uma no serviço certo para o trabalho que faz. É mais longo que os meus artigos habituais porque a profundidade técnica pede, e porque as decisões só fazem sentido quando você vê o encadeamento entre elas.

Um aviso de rota: acesso sem SSH e a discussão de banco em EC2 eu já cobri em SSH do desespero? Não com AWS SSM. Aqui eu cito e sigo em frente, sem repetir.

Meu papel neste projeto

Vale deixar claro o escopo antes, porque isso muda a leitura de tudo que vem depois.

Meu papel como arquiteta foi desenhar a arquitetura alvo, justificar cada escolha de serviço por trade-off explícito, definir o modelo de segurança e acesso, estabelecer a estratégia de observabilidade e planejar o cutover com rollback testado.

Não foi escolher a stack mais moderna. Foi escolher a stack que o cliente consegue operar depois que eu sair. São coisas diferentes, e a segunda é bem mais difícil.

Por que a VPS única cobra caro depois

Numa VPS típica, o mesmo servidor entrega o site estático, roda a aplicação, mantém o processo de tempo real, executa os workers e às vezes ainda hospeda o banco.

Funciona. O problema não é funcionar, é o que acontece quando algo dá errado.

Escala em bloco. Falha em bloco. Manutenção derruba o conjunto. E como tudo divide o mesmo disco, a mesma memória e o mesmo IP, o diagnóstico fica ambíguo: você nunca sabe se o problema é da aplicação, do banco ou do vizinho de processo.

Desacoplar resolve isso por separação de responsabilidade. Cada camada escala e falha sozinha, então pico no frontend não derruba o backend. Cada componente vai para o serviço mais barato e adequado, porque conteúdo estático não precisa de servidor e banco precisa de disco e memória. O que é privado fica privado, e só a borda é pública.

Mas o ganho que realmente paga o projeto é o último, e é o que ninguém enxerga no dia da entrega: a próxima aplicação entra como um serviço novo, sem encostar no que já roda.

O desenho alvo tem quatro camadas, com observabilidade atravessando todas:

Usuários → Borda (CDN + WAF) → Frontend (estático)
                              → Backend (containers) → Dados (banco)
                     Observabilidade e Backup cobrindo tudo

As escolhas, e o que cada uma custa

Desacoplar só compensa se cada camada for para o serviço certo. Aqui é onde o projeto se define, e onde eu passei mais tempo.

Frontend estático: S3 com CloudFront

Se o frontend é uma SPA, ou seja, um build estático, ele não precisa de servidor nenhum. Publicar no S3 e servir pelo CloudFront entrega cache na borda, HTTPS gerenciado e um custo irrisório perto de manter um servidor web ligado.

Dois detalhes decidem se você vai sofrer ou não.

O bucket fica privado, acessível somente pelo CloudFront via Origin Access Control. Bucket público para servir frontend não tem mais justificativa, e o OAC substituiu a Origin Access Identity justamente para cobrir os casos que a OAI não cobria, como SSE-KMS. A referência está em Restringir acesso a origem no Amazon S3.

Para SPA com roteamento no cliente, configure o fallback de erro mandando 403 e 404 para o index. Sem isso, a home funciona, a navegação interna funciona, e o refresh numa rota interna quebra. É um bug que aparece só em produção e some no ambiente local, o que faz perder uma tarde inteira.

O trade-off honesto: nada disso se aplica se a aplicação for SSR, com renderização no servidor. Aí você precisa de runtime, e o caminho é container ou serviço gerenciado de SSR. Não force estático onde não cabe só porque é mais barato.

Backend: ECS Fargate

Para aplicação com carga previsível e contínua, container gerenciado é o melhor equilíbrio entre controle e operação. Você não gerencia servidor, escala horizontalmente e paga por vCPU e memória da task.

Contra EC2, o ganho é eliminar patch de sistema operacional e gestão de cluster. Contra Lambda, o ponto decisivo foi conexão longa: a aplicação tem WebSocket, e Lambda continua sendo melhor resposta para pico esporádico, não para processo sempre ativo com conexão persistente.

Dados: o mesmo debate do artigo anterior

Banco em EC2 versus RDS eu já defendi em detalhe no artigo de SSM, e a posição continua: a menos que exista requisito específico que impeça o gerenciado, o RDS sai mais barato no médio prazo, porque o custo real não está na fatura, está nas horas de quem opera.

Quando a escolha por EC2 é consciente e documentada, como foi lá, o papel da arquitetura é garantir que ela seja implementada com segurança. Aqui isso significou instância própria para o banco, disco de dados dedicado e separado do disco raiz, e nenhuma porta de entrada aberta.

Borda: CloudFront com WAF, roteamento no ALB

O CloudFront é o ponto de entrada público, cuidando de CDN, TLS e cache, com o WAF anexado. Atrás dele, o Application Load Balancer faz roteamento por caminho: a raiz para o frontend, a rota de API para o backend, a rota de WebSocket para o processo de tempo real.

O cuidado que muita gente pula: restrinja o ALB para aceitar tráfego somente do CloudFront. Isso se faz com a prefix list gerenciada de origem do CloudFront somada a um header secreto validado no WAF. Sem esses dois, o ALB continua alcançável direto pela internet, e quem descobrir o DNS dele passa por cima do seu WAF.

Eu chamo isso de WAF de decoração: existe, custa, aparece bonito no diagrama e não protege nada, porque o tráfego tem um caminho alternativo que ninguém fechou.

Acesso e pipeline: sem credencial de longa duração

Acesso administrativo por SSM Session Manager, sem porta 22, sem chave, sem bastion. O detalhamento completo está no artigo anterior, inclusive o port forwarding para alcançar o banco com cliente local.

No pipeline, o equivalente: federação OIDC entre o CI e a AWS, em vez de access key guardada como segredo do repositório. O pipeline assume uma role de curta duração por token, e deixa de existir credencial permanente que alguém precise rotacionar, e que ninguém rotaciona.

O padrão está documentado em Provedores de identidade OIDC no IAM.

Somando as duas coisas, o ambiente terminou sem nenhum segredo de longa duração: nem chave SSH, nem access key. É uma propriedade extremamente importante em auditoria, e bem mais cara de implantar depois do que desde o início.

Rede: Security Group referenciando origem

VPC própria, subnet pública para ALB e NAT, subnet privada para aplicação e dados.

O ponto que mais economiza dor no futuro: Security Group referenciando origem por outro Security Group, não por faixa de IP. A regra do banco libera a porta a partir do SG do backend. Quando o IP mudar, e ele vai mudar, nada quebra e ninguém precisa lembrar de atualizar CIDR em três lugares.

Os comandos que sustentam isso

Todos genéricos, com placeholder. A ideia é o esqueleto, não o copiar e colar cego.

OAC e bucket privado do frontend

aws cloudfront create-origin-access-control \
  --origin-access-control-config '{"Name":"app-oac","OriginAccessControlOriginType":"s3","SigningBehavior":"always","SigningProtocol":"sigv4"}'

aws s3api put-bucket-policy --bucket <BUCKET> --policy file://bucket-policy.json

ECS: task definition e serviço atrás do ALB

aws ecs register-task-definition --cli-input-json file://taskdef.json

aws ecs create-service \
  --cluster <CLUSTER> --service-name app-backend \
  --task-definition app-backend --desired-count 2 --launch-type FARGATE \
  --network-configuration "awsvpcConfiguration={subnets=[<SUBNET>],securityGroups=[<SG_ECS>],assignPublicIp=DISABLED}" \
  --load-balancers "targetGroupArn=<TG_ARN>,containerName=app,containerPort=<PORTA>"

ALB: target group e listener

aws elbv2 create-target-group --name app-tg --protocol HTTP --port <PORTA> \
  --vpc-id <VPC_ID> --target-type ip --health-check-path /health

aws elbv2 create-listener --load-balancer-arn <ALB_ARN> --protocol HTTPS --port 443 \
  --certificates CertificateArn=<ACM_ARN> \
  --default-actions Type=forward,TargetGroupArn=<TG_ARN>

Agente SSM em AMI Debian via user-data

Em AMI Debian o agente não vem instalado, diferente das AMIs Amazon Linux e Ubuntu fornecidas pela AWS. Esquecer isso significa subir a instância e descobrir que não consegue entrar nela.

#!/bin/bash
set -e
cd /tmp
curl -fsSL -o amazon-ssm-agent.deb \
  https://s3.<REGIAO>.amazonaws.com/amazon-ssm-<REGIAO>/latest/debian_amd64/amazon-ssm-agent.deb
dpkg -i amazon-ssm-agent.deb
systemctl enable amazon-ssm-agent && systemctl start amazon-ssm-agent

Alarme essencial no CloudWatch

aws cloudwatch put-metric-alarm --alarm-name alb-alvos-unhealthy \
  --namespace AWS/ApplicationELB --metric-name UnHealthyHostCount --statistic Maximum \
  --dimensions Name=LoadBalancer,Value=<ALB_DIM> Name=TargetGroup,Value=<TG_DIM> \
  --period 60 --evaluation-periods 3 --threshold 1 \
  --comparison-operator GreaterThanOrEqualToThreshold --alarm-actions <SNS_TOPIC_ARN>

Aumentar disco a quente

aws ec2 modify-volume --volume-id <VOL_ID> --size 200
sudo resize2fs /dev/nvme1n1

Observabilidade não é item de fim de lista

Monitoramento é o que te dá confiança para virar a chave e conseguir dormir depois. Deixar para o final significa virar a chave sem saber o que está acontecendo do outro lado.

O que funcionou foi trabalhar com duas camadas complementares, em vez de tentar cobrir tudo com uma só.

A ferramenta do time, seja Zabbix, Datadog ou o que já estiver em uso, cobre sistema operacional e aplicação com agente. É o monitoramento principal e detalhado, e o time já sabe operar.

O CloudWatch cobre a camada gerenciada da AWS, que o agente simplesmente não enxerga: saúde de alvo no ALB, erro 5xx, serviço sem task rodando, uso do ECS.

O erro comum aqui é duplicar. Se o agente já mede CPU e memória da instância, não replique no CloudWatch. Você paga duas vezes pela mesma informação e, pior, cria dois lugares para olhar durante um incidente, que é exatamente quando ninguém tem paciência para decidir em qual acreditar.

Alarmes mínimos para o primeiro mês de produção: alvo não saudável no ALB, erro 5xx no backend, serviço sem task rodando, CPU e latência acima do esperado.

E dois comandos que resolvem a maior parte dos diagnósticos:

aws elbv2 describe-target-health --target-group-arn <TG_ARN>
aws logs tail /ecs/app-backend --since 1h --filter-pattern "ERROR"

O que doeu, e como você evita

Essa é a parte que eu mais queria escrever, porque é a que não está na documentação. Nenhum desses pontos é difícil. Todos são fáceis de esquecer, e cada um custou tempo.

Porta coerente de ponta a ponta. A porta do container na task definition, a porta do target group e a regra do Security Group precisam ser a mesma. Um valor provisório que sobrou em um dos três derruba o health check e faz o serviço reciclar em loop, com aparência perfeita de problema de rede. Confirme a porta real que a aplicação escuta antes de subir qualquer coisa.

Egress de Security Group não é dado. Quando alguém edita o SG manualmente, é comum a saída padrão virar uma regra quebrada. Sem egress, a task não puxa a imagem do ECR e devolve timeout, e o agente SSM não conecta. Confira a regra de saída mesmo quando ninguém mexeu nela conscientemente.

NAT tem dois lados. Não basta a subnet privada rotear tudo para o NAT. A subnet pública onde o NAT está precisa rotear para o Internet Gateway. Esquecer o segundo lado é clássico, e o sintoma é idêntico ao do item anterior, o que torna o diagnóstico confuso justamente porque você acabou de conferir o SG e ele estava certo.

Confirme o nome do device antes de formatar. Em instância Nitro, o volume que você anexou como sdb aparece como nvme1n1, e a numeração não segue a ordem de anexação. Confira sempre com lsblk antes de rodar mkfs ou resize2fs. Errar o device aqui significa formatar o disco errado, e não tem desfazer.

Right-size por métrica, não por espelho. Não copie o tamanho da VPS antiga. Ela rodava tudo junto e com folga, e esse número não significa nada no ambiente novo. Suba conservador, meça em produção e ajuste. No Fargate isso é uma nova revisão da task mais um deploy, sem downtime, então o custo de errar para baixo é pequeno.

Erro de aplicação se disfarça de erro de infraestrutura. Chave de criptografia ausente ou inconsistente, variável de ambiente faltando, tudo isso vira 500 no ALB e parece problema de rede. Olhe o log da aplicação antes de culpar a infra. E em migração, reutilize as chaves originais: chave nova quebra dado já criptografado e derruba sessão de usuário logado.

IMDSv2 obrigatório. Deixe o Instance Metadata Service como required em toda EC2. É um endurecimento simples que fecha uma porta clássica de roubo de credencial de role via SSRF. Detalhes em Usar IMDSv2.

Backup não existe só porque o cofre existe. Criar o vault e o plano não protege nada até você associar os recursos, por tag ou por seleção. Marque toda instância nova com a tag de backup e confirme que a associação pegou. Um cofre sem recurso associado aparece configurado no console e não restaura nada, e você só descobre no dia em que precisar.

Cutover é ensaio, não improviso. Rollback testado, janela de menor uso, comunicação combinada com o time e critério claro do que significa deu certo. Virada sem incidente é resultado de preparação, não de sorte.

O que separa mudar de endereço de mudar de patamar

Desacoplar é o que transforma migração em modernização.

Frontend estático no S3 com CloudFront, usando OAC e fallback de SPA. Backend em container no Fargate, atrás do ALB. Banco isolado, com disco dedicado e backup associado de fato. Acesso por SSM, sem SSH, com IMDSv2 obrigatório. Pipeline por OIDC, sem chave fixa no repositório. Portas coerentes entre task, target group e Security Group. Egress liberado no SG e NAT com rota nos dois lados. Alarme essencial no ALB e no ECS, somado ao monitoramento de sistema e aplicação. Cutover com rollback testado e janela combinada.

O resultado não é estar na nuvem. É ter uma base onde a próxima aplicação entra como um serviço novo, sem encostar no que já roda, e onde ninguém precisa acordar de madrugada para descobrir qual dos cinco processos do mesmo servidor causou o incidente.

Essa é a diferença entre ter mudado de endereço e ter mudado de patamar.


Tem dúvida sobre alguma dessas decisões no seu ambiente, ou discorda de alguma delas? Me chama no LinkedIn ou deixa nos comentários. Se ainda não leu, o artigo de acesso sem SSH está em SSH do desespero? Não com AWS SSM.