Migrando uma API de ECS para Docker + Traefik sem Big Bang
Como migrar uma API de ECS para Docker e Traefik em paralelo, validando container, proxy, origin e CloudFront antes de tocar na origem antiga.
Migrar uma API não precisa começar desligando a versão antiga.
Em uma migração real de serviços que rodavam no ECS para hosts Docker, a regra operacional foi justamente o contrário:
o ambiente novo precisava provar que funcionava antes de a produção antiga ser tocada.
Isso mudou completamente o formato da migração.
Em vez de tratar a tarefa como “mover a API da AWS para outro servidor”, tratamos como uma sequência de validações independentes:
| |
Nada de Big Bang.
O ponto de partida: produção antiga continuava sendo a referência #
Durante a etapa de homologação, a orientação era explícita:
- não alterar o serviço ECS antigo;
- não alterar o CloudFront de produção;
- não subir o Compose inteiro do host;
- adicionar o novo serviço em paralelo;
- validar em camadas;
- só discutir a virada depois da validação funcional.
Isso é importante porque migração e remoção são operações diferentes.
Se eu removo a origem antiga antes de provar a nova, transformo qualquer erro de configuração em indisponibilidade.
Se as duas existem em paralelo, o rollback da fase de validação é quase banal: simplesmente não faço a virada.
Primeiro inventário: o que exatamente estamos migrando? #
Antes de escrever o Compose novo, a coleta precisa responder pelo menos:
| |
Essa etapa evita um erro comum: copiar a ideia do serviço sem copiar as dependências operacionais dele.
Uma task definition pode parecer simples, mas o comportamento público depende também de load balancer, path, headers, certificado, DNS e camada de CDN.
O Compose novo entra como adição, não como substituição #
No host Docker, o procedimento usado foi adicionar apenas o bloco do serviço novo ao Compose existente.
Antes de subir qualquer container:
| |
Depois, revisar o diff do arquivo versionado.
Essa ordem parece conservadora porque é conservadora mesmo.
O objetivo não é descobrir erro de YAML depois que o Compose já tentou reconciliar metade do host.
--no-deps foi um guard-rail importante #
Para subir a API nova, a regra era não executar:
| |
no projeto inteiro.
A forma controlada era algo como:
| |
Isso limita a mudança ao serviço que está entrando.
Em um host compartilhado com aplicações já ativas, esse detalhe reduz bastante o blast radius da operação.
Não significa que --no-deps é obrigatório em toda implantação. Significa que, naquele cenário, não havia motivo para pedir ao Compose que reconciliasse serviços que não faziam parte da mudança.
A validação foi feita em camadas #
Uma resposta HTTP 200 no domínio de homologação não me diz em qual camada o serviço está certo.
Por isso a validação seguia uma progressão.
1. Container #
Primeiro confirmar que o processo existe e permanece estável:
| |
Depois, descobrir o IP interno e testar diretamente a aplicação:
| |
Se isso falha, não adianta discutir CloudFront.
2. Path interno da API #
Quando a aplicação também responde por um path específico:
| |
Isso valida o comportamento do próprio container antes do reverse proxy.
3. Traefik local #
Depois entra a regra de roteamento:
| |
Agora estamos testando router, rule, rede Docker e service port do Traefik.
4. Origin externo #
O mesmo Host é enviado para o endereço externo do host:
| |
Se container e Traefik local funcionam, mas essa etapa não, a investigação muda para firewall, publicação, TLS ou caminho externo.
5. CloudFront de homologação #
Só depois chegamos à URL pública de HMG:
| |
Quando essa resposta passa, sabemos que o caminho inteiro de homologação está funcional.
Por que usar um CloudFront separado na homologação #
A migração tinha endpoints públicos que posteriormente seriam atendidos pelo CloudFront já existente em produção.
Mexer nesse CloudFront para “testar” a origem nova seria colocar a validação dentro da própria produção.
Então foi usada uma distribuição de homologação com behaviors apontando para os novos Docker hosts.
Isso permitiu reproduzir uma parte importante da arquitetura pública:
| |
sem alterar o tráfego real.
O health check validou vários serviços antes da virada #
A fonte operacional dessa migração registra múltiplas APIs novas respondendo HTTP 200 Healthy pela camada de homologação.
Ao mesmo tempo, os containers antigos e o ECS de produção foram preservados durante essa fase.
Isso é exatamente o estado que eu queria antes de uma virada:
novo ambiente tecnicamente validado + origem antiga ainda disponível.
Ainda não é “migração concluída”.
É a condição necessária para planejar a mudança com risco muito menor.
Um detalhe que complicou: a imagem podia estar em outra conta AWS #
Alguns Docker hosts já tinham uma conta AWS padrão configurada, enquanto as imagens novas estavam em outro ECR.
Isso cria uma armadilha porque duas perguntas diferentes passam a existir:
| |
A migração precisou manter essa diferença documentada e, quando necessário, usar autenticação isolada para o registry da imagem nova.
Esse assunto merece um diagnóstico próprio, porque problema de ECR pode parecer problema do container quando na verdade a aplicação nem chegou a ser baixada.
HTTPS no origin também precisa ser validado #
Outro aprendizado apareceu nos hosts publicados via Traefik: quando CloudFront conversa com origin HTTPS, o certificado apresentado pelo origin precisa fazer sentido para o hostname usado nessa conexão.
Usar um domínio técnico com certificado válido evita transformar a migração em uma caça ao TRAEFIK DEFAULT CERT ou a falhas de handshake.
É um daqueles detalhes que não aparecem no desenho simples “CDN → servidor”, mas aparecem na hora em que o tráfego real passa por ali.
O que não foi feito durante a homologação #
A fonte é clara sobre os limites dessa fase.
Não foi:
- removido o ECS antigo;
- alterado o CloudFront de produção para concluir a virada;
- feito
docker compose up -dgeral nos hosts; - assumido que todos os hosts usavam o mesmo padrão de proxy;
- considerado HMG saudável como autorização automática para desligar a origem anterior.
Alguns hosts usavam Traefik. Outros tinham Nginx local e Certbot e precisavam de coleta específica.
Padronizar mentalmente ambientes diferentes é uma ótima forma de aplicar a configuração certa no lugar errado.
O rollback mais barato acontece antes da virada #
Em uma migração desse tipo, eu prefiro gastar mais tempo provando o ambiente novo enquanto ninguém depende dele.
Porque, antes da virada:
| |
Depois da virada, cada uma dessas falhas já concorre com o relógio da indisponibilidade.
Checklist que ficou desse caso #
Antes de migrar o tráfego de uma API do ECS para Docker:
- inventariar task/service/imagem/porta/path;
- versionar a configuração do host de destino;
- validar
docker compose config; - puxar somente a imagem necessária;
- subir somente o serviço novo;
- validar container diretamente;
- validar o path esperado;
- validar reverse proxy local;
- validar origin externo;
- validar CDN/HMG;
- obter validação funcional;
- manter a origem antiga intacta até a virada;
- planejar a mudança de produção e o rollback separadamente;
- só remover o legado depois do aceite.
A ideia principal #
“Sem Big Bang” não é só ter rollback.
É desenhar a migração de forma que a maior parte dos erros seja descoberta antes de existir qualquer necessidade de rollback.
Nesse caso, colocar Docker + Traefik em paralelo com o ECS e testar cada camada por uma distribuição de homologação permitiu validar várias APIs sem mexer na origem que ainda atendia produção.
A virada final continuava sendo uma mudança importante e precisava ser coordenada.
Mas ela deixou de carregar junto a dúvida básica de saber se o container novo sequer funcionava.
Primeiro provar. Depois virar.
Me chama.
Manda o contexto, os sintomas e o que já foi testado. Bora organizar as evidências antes de sair mexendo.