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.