↓Pular para o conteúdo principal
  1. blog/
ARTIGO TÉCNICO

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.

·7 minutos

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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
ECS antigo continua atendendo
        ↓
novo container Docker
        ↓
Traefik local
        ↓
origin externo
        ↓
CloudFront de homologação
        ↓
validação funcional
        ↓
só então planejar a virada final

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:

1
2
3
4
5
6
7
8
qual imagem roda no ECS?
qual porta a aplicação expõe?
qual health check funciona?
quais variáveis e secrets ela usa?
qual path público precisa ser preservado?
qual host Docker vai recebê-la?
o host usa Traefik ou outro proxy?
qual conta/registry contém a imagem?

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:

1
2
docker compose config --services
docker compose config --images

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:

1
docker compose up -d

no projeto inteiro.

A forma controlada era algo como:

1
2
docker compose pull <servico>
docker compose up -d --no-deps <servico>

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:

1
2
docker ps --filter name=<servico>
docker logs --tail=100 <servico>

Depois, descobrir o IP interno e testar diretamente a aplicação:

1
2
API_IP="$(docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' <servico>)"
curl -i "http://${API_IP}:8080/health"

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:

1
curl -i "http://${API_IP}:8080/<path>/health"

Isso valida o comportamento do próprio container antes do reverse proxy.

3. Traefik local #

Depois entra a regra de roteamento:

1
2
curl -i -H 'Host: hmg-api.example.com' \
  http://127.0.0.1/<path>/health

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:

1
2
curl -i -H 'Host: hmg-api.example.com' \
  http://<IP_DO_HOST>/<path>/health

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:

1
curl -i https://hmg-api.example.com/<path>/health

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:

1
2
3
4
5
6
7
8
9
viewer
  ↓
CloudFront HMG
  ↓
origin Docker
  ↓
Traefik
  ↓
API

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:

1
2
qual identidade o AWS CLI está usando?
para qual registry o Docker está autenticado?

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 -d geral 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:

1
2
3
4
5
falhou container? corrige container
falhou Traefik? corrige Traefik
falhou origin? corrige origin
falhou CloudFront HMG? corrige behavior/origin
falhou funcional? não muda produção

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.

TEM UM CENÁRIO PARECIDO?

Me chama.

Manda o contexto, os sintomas e o que já foi testado. Bora organizar as evidências antes de sair mexendo.

Falar com Castro →
Sem enrolação. Com evidência.