Lab 03 — Da imagem ao deploy sem indisponibilidade: ECR, rolling update e drenagem
O problema, e a empresa que o tem
A Cadência é uma equipe de duas pessoas que mantém uma API de pedidos para trinta lojas. A aplicação é a mesma do L01: .NET 8 em ECS Fargate, banco em sub-rede privada, balanceador na frente. Tudo o que este laboratório muda é como uma versão nova entra no ar.
Hoje o procedimento é escrito num documento e obedecido: avisar no canal, esperar o horário de menor movimento, rodar terraform apply, olhar o painel até o serviço responder de novo. Entre o aviso e a volta passam de seis a doze minutos. Ninguém considera isso um defeito — considera-se o custo de publicar.
A consequência real não é o tempo fora do ar. É que publicar dói, então publica-se pouco: a cada duas semanas, com trinta commits juntos. Quando algo quebra, há trinta suspeitos, e o diagnóstico que levaria minutos leva uma tarde. A janela de indisponibilidade e a lentidão da entrega são o mesmo problema, e quem resolve o primeiro resolve o segundo.
O que este laboratório NÃO é
Não é CI/CD. Aqui o deploy continua sendo disparado por uma pessoa, de um script. Tirar a credencial da máquina do dev e colocar o rollout num pipeline federado é o L54, e ele depende deste. Misturar as duas coisas faria você depurar permissão de OIDC enquanto tenta entender drenagem de conexão.
O que você vai conseguir fazer
Objetivos verificáveis: cada um se prova com um comando na seção de implantação, não com a sensação de ter entendido.
- Explicar por que o 503 do deploy aparece sem nenhuma linha nos logs da aplicação.
- Nomear os três relógios do rollout e dizer qual deles domina o tempo total.
- Derivar a ordem entre drenagem e SIGTERM a partir dos estados do ciclo de vida da task.
- Distinguir vivacidade de prontidão e justificar por que a prontidão consulta o banco e a vivacidade não.
- Configurar prazo de drenagem a partir do p99 medido, em vez de copiar um valor.
- Publicar com tag imutável e voltar para a versão anterior sem reconstruir imagem.
- Provar que o rollout não gerou erro de cliente, com medição durante a troca.
- Diagnosticar um rollout que não estabiliza, distinguindo três causas pelo sintoma.
O que a certificação cobra disto
| Conceito | Certificação | Como aparece aqui | O que dominar |
|---|---|---|---|
| Rolling update e capacidade | DVA-C02, SAA-C03 | o par mínimo/máximo que autoriza o excedente | por que máximo 200 sem mínimo 100 não garante nada |
| Drenagem de conexão | DVA-C02, SOA-C02 | atributo do grupo de destino, derivado do p99 | que o prazo é teto e não espera; e que ele é do grupo, não do ALB |
| Health check: ALB vs contêiner | DVA-C02, SOA-C02 | duas rotas, dois propósitos | qual decide receber tráfego e qual decide substituir o contêiner |
| Ciclo de vida da task | DVA-C02, SAP-C02 | DEACTIVATING precede STOPPING | onde ocorre a desregistragem e onde o sinal de parada é enviado |
| Imutabilidade de tag | DVA-C02, DOP-C02 | IMMUTABLE no repositório e SHA como tag | por que rollback depende de identidade de conteúdo |
| Disjuntor de implantação | DOP-C02 | volta automática de rollout que não estabiliza | a diferença entre rollout que falha e versão que responde errado |
| Métrica de 5xx: alvo vs balanceador | SOA-C02, DOP-C02 | o alarme mede o alvo | que o mesmo número nas duas métricas conta histórias opostas |
| Multi-stage build e superfície | DVA-C02 | SDK fora da imagem final | efeito no tempo de partida e na superfície de ataque |
Onde isto costuma ser cobrado errado
A pergunta clássica dá um serviço com uma task, mínimo 100% e máximo 100%, e pede por que o deploy não avança. A resposta é aritmética: sem excedente autorizado, o ECS não pode criar a task nova, e sem descer abaixo do mínimo não pode remover a velha. Não é lentidão — é impossibilidade.
Requisitos, e como cada um muda o desenho
Requisito não funcional que não aparece numa linha de configuração é intenção. A coluna da direita é onde cada um deixou marca.
| Requisito | Valor declarado | O que ele decide no desenho |
|---|---|---|
| Erro visível ao cliente durante o deploy | zero | obriga excedente: mínimo 100% e máximo 200%, com pelo menos duas tasks |
| Duração do rollout | abaixo de 3 min por versão | health check em 2 × 5 s em vez de 5 × 30 s, e prazo de drenagem em 30 s |
| p99 de resposta | 400 ms | é a medida que autoriza drenagem curta; com upload longo o valor seria outro |
| Rollback sem reconstruir | obrigatório | tag imutável com SHA, e revisões da task definition preservadas |
| Publicar sem vigília | requisito da equipe de duas pessoas | disjuntor de implantação com volta automática, mais alarme de 5xx do alvo |
| Migration na partida | existe, leva até 20 s | período de graça de 60 s, senão a task é morta antes de ficar pronta |
| Disponibilidade | 99,9% em horário comercial | duas tasks em duas AZs — o mesmo número que o rollout já exige |
| Rastreabilidade do que está no ar | auditável | a tag é o SHA do commit; publicar árvore suja é bloqueado no script |
Arquitetura mínima: o deploy que derruba
Este é o desenho que a Cadência tem hoje, e ele é legítimo como ponto de partida: publica de verdade, com poucas linhas. O laboratório começa por medir a janela dele — porque um número torna o defeito discutível, e "o deploy demora" não.
- → docker push :latest
- → update-service --force-new-deployment
- → para a antiga, sobe a nova
- → pull da imagem em :latest
- → HTTPS 443
- → encaminha só para alvo saudável
- Fora da AWS
- Compute
- Rede e entrega
Este desenho publica, e é por isso que ele sobrevive: são as menores linhas de Terraform que trocam a versão. A indisponibilidade não vem de nenhuma configuração errada — vem de duas ausentes e de uma tag que não identifica nada. Percorra os passos e repare que a janela sem alvo saudável é aritmética, não azar.
- A tag não identifica versão nenhuma. Com `:latest`, a task definition aponta para um nome, não para um conteúdo. Duas tasks subindo em momentos diferentes podem rodar imagens diferentes, e a pergunta "qual versão está no ar?" não tem resposta. Rollback também não tem: o envio anterior foi sobrescrito.
- Uma task só, e a aritmética fecha contra você. Com `desired_count = 1`, trocar a versão exige que a única task pare. Não há excedente para a nova conviver com a velha, e não existe configuração que resolva isso sem uma segunda task.
- A janela de 503 é a soma de duas esperas. Entre a velha parar e a nova ser declarada saudável passam o tempo de provisionar a interface de rede, baixar a imagem, iniciar o processo e — a maior parcela — os ciclos do health check. No padrão do grupo de destino, são 5 verificações de 30 s: 150 s só de espera.
- O cliente vê 503, e o log da aplicação está limpo. É a parte que mais atrasa o diagnóstico. O 503 é gerado pelo próprio ALB, porque não há alvo saudável para escolher. Nenhuma requisição chega à aplicação, então nenhum log dela registra o incidente.
- Deploy ruim não tem volta automática. Se a versão nova sobe e responde 500, ninguém desfaz nada. E como a tag foi sobrescrita, voltar exige reconstruir a imagem antiga a partir do código — na pressa, sem certeza de que é o mesmo binário.
- Por que alguém publica assim. Porque funciona, e porque é o menor número de linhas que leva código à nuvem. O aviso no canal antes do deploy é o remendo social de um defeito técnico — e enquanto ele existe, o defeito não é tratado como defeito.
Antes de mudar qualquer coisa, meça. Este laço roda durante um deploy do desenho atual e devolve a duração da janela em requisições perdidas, o que é mais honesto do que cronômetro no relógio de parede.
# Rode ANTES de mudar nada. O numero que sair daqui e a linha de base.
URL="https://$(terraform output -raw dominio)/api/health/ready"
ruins=0; total=0
( while true; do
c=$(curl -s -o /dev/null -w "%{http_code}" --max-time 3 "$URL")
total=$((total+1)); [ "$c" = "200" ] || ruins=$((ruins+1))
echo "$(date +%T) $c"; sleep 0.2
done ) &
LACO=$!
terraform apply -auto-approve # o deploy do desenho MINIMO
kill $LACO
# Na Cadencia: 1.847 requisicoes, 412 nao-200 — cerca de 82 s de janela real,
# contra os "seis a doze minutos" do documento. Medir corrigiu as duas pontas.Os dois 5xx do deploy não são a mesma falha
O 503 vem do balanceador e significa ausência de alvo saudável — nenhuma requisição chegou à aplicação, e é por isso que o log dela está limpo. O 502 e o 504 vêm de conexão cortada ou resposta inválida de um alvo que existia. O primeiro é problema de capacidade durante a troca; o segundo é problema de drenagem ou da versão nova. Tratá-los como "erro de deploy" indistintamente é o que faz o time ajustar o parâmetro errado.
Arquitetura para produção
Cada peça nova abaixo rastreia a uma linha da tabela de requisitos. Se você não conseguir apontar o requisito, a peça é adorno — e este desenho não tem nenhuma.
- → push com tag = SHA do commit
- → nova revisão da task definition
- → sobe a nova sem parar a velha
- → pull por digest imutável
- → health check até 2 respostas boas
- → só então: DEACTIVATING
- → drena até o alvo virar UNUSED
- → HTTPS 443 sem interrupção
- → escolhe entre os alvos saudáveis
- → alarme dispara volta à revisão anterior
- Fora da AWS
- Compute
- Rede e entrega
- Gestão e governança
A troca deixa de ser "para e sobe" e passa a ser "sobe, prova, drena e só então para". O que muda no desenho é a existência SIMULTÂNEA das duas versões e um grupo de destino com prazo de drenagem próprio. Percorra os passos: cada peça nova rastreia a um requisito da seção anterior.
- A identidade da imagem é o SHA do commit. Com tag imutável, a pergunta "qual versão está no ar?" tem uma resposta exata, e voltar é apontar para a revisão anterior da task definition — que continua existindo, com a imagem que continua existindo. Rollback deixa de ser reconstrução e passa a ser referência.
- O excedente é o que permite não haver janela. `maximum_percent = 200` autoriza o serviço a ter o dobro das tasks durante a troca; `minimum_healthy_percent = 100` proíbe descer abaixo da capacidade declarada. Um sem o outro não resolve: o máximo dá o espaço, o mínimo é quem obriga a usá-lo.
- Prontidão precisa dizer a verdade. Se a rota de saúde devolve 200 antes de a aplicação poder atender — sem o pool do banco aberto, por exemplo — o grupo de destino declara a task saudável e o ALB manda tráfego real para quem vai responder 500. O 5xx aparece DEPOIS do rollout e ninguém liga uma coisa à outra.
- A drenagem acontece antes do SIGTERM, não depois. O ciclo de vida da task é explícito: a desregistragem do grupo de destino ocorre em `DEACTIVATING`, e o `SIGTERM` só é enviado em `STOPPING`, que é o estado seguinte. A consequência é contraintuitiva e vale saber: aplicação que ignora `SIGTERM` não corta requisição HTTP em voo, desde que o prazo de drenagem cubra a requisição mais longa.
- O prazo de drenagem é um teto, não uma espera. O que o ECS aguarda é o balanceador informar que a conexão keep-alive do cliente fechou — o alvo entra no estado `UNUSED`. Se isso acontece em 2 s, o rollout segue em 2 s. Os 300 s do padrão são o limite de paciência, e é por isso que baixá-los acelera o deploy sem cortar nada.
- A volta é automática, e é ela que permite publicar sem plateia. Com o disjuntor de implantação ligado e a volta habilitada, um rollout que não estabiliza retorna à revisão anterior sozinho. O alarme de 5xx do alvo é o segundo par de olhos, para o caso em que a task fica saudável e a resposta, errada.
A diferença estrutural em relação ao desenho anterior não é uma caixa a mais: é a existência simultânea de duas versões. Tudo o mais — grupo de destino com prazo próprio, alarme, volta automática — existe para administrar essa convivência.
O ajuste com maior efeito por linha alterada
Das quatro parcelas, a que domina no padrão é a espera do health check: 5 verificações a cada 30 s são 150 s antes de a task nova receber a primeira requisição. Baixar para 2 × 5 s tira 140 s de cada rollout, e é uma linha. Otimizar o Dockerfile — que é onde a atenção costuma ir — mexe na parcela da partida, tipicamente dezenas de segundos.
O caminho de um deploy, ponta a ponta
Os nomes dos estados não são jargão: eles são a prova da ordenação. A desregistragem do grupo de destino acontece em DEACTIVATING, e o sinal de parada só é enviado em STOPPING — que é o estado seguinte. Isso é observável com describe-tasks durante o rollout, e a seção de provas faz exatamente isso.
A consequência contraintuitiva, e vale saber com precisão
Como a drenagem termina ANTES de o SIGTERM ser enviado, uma aplicação que ignora completamente o sinal não corta requisição HTTP em voo — desde que o prazo de drenagem cubra a requisição mais longa. Isso contraria o conselho mais repetido sobre deploy em contêiner. Tratar SIGTERM continua valendo, por dois motivos diferentes do que se costuma dizer: encurta o rollout (o processo sai em vez de esperar o stopTimeout inteiro) e protege trabalho que não é requisição — consumo de fila, tarefa agendada, escrita em lote.
O que o ECS aguarda durante a drenagem também merece precisão. Não é um relógio contando até o valor configurado: é o balanceador informar que a conexão keep-alive do cliente fechou, momento em que o alvo entra no estado UNUSED. O valor configurado é o limite de paciência. Numa API com resposta em 400 ms, a drenagem real termina em menos de um segundo — os 300 s do padrão nunca são gastos, mas o ECS os respeitaria se a conexão insistisse em ficar aberta.
// O que o EventBridge entrega quando o rollout muda de fase. E o payload que
// vale assinar para saber de deploy revertido sem ninguem olhando painel.
{
"source": "aws.ecs",
"detail-type": "ECS Deployment State Change",
"resources": ["arn:aws:ecs:us-east-1:111122223333:service/ffv-lab/ffv-lab-api"],
"detail": {
"eventType": "ERROR",
"eventName": "SERVICE_DEPLOYMENT_FAILED",
"deploymentId": "ecs-svc/9223370491062737000",
"updatedAt": "2026-08-07T14:22:31.618Z",
// A mensagem do disjuntor: o rollout nao estabilizou e foi revertido.
// Repare que a versao anterior volta sozinha — ninguem foi acordado.
"reason": "ECS deployment circuit breaker: rolling back to desired count."
}
}As decisões, e o que se perde em cada uma
📋 Publicar várias vezes por dia uma API .NET 8 em ECS Fargate, com duas pessoas no time, sem plantão noturno e sem orçamento para infraestrutura nova.
O rolling update não pede nenhum recurso adicional na fatura: o excedente durante a troca dura minutos e some. Ele resolve o problema declarado — publicar sem janela — e o disjuntor cobre a parte que o time não tem gente para vigiar, que é perceber o rollout ruim de madrugada. Blue/green e canário resolvem um problema DIFERENTE, que é decidir com tráfego real se a versão nova é boa; enquanto a pergunta for "como publicar sem derrubar", pagar por eles é comprar resposta para pergunta que ainda não se fez.
Alt: Blue/green com CodeDeploy — Duas frotas inteiras conviverem exige um segundo grupo de destino e dobra a capacidade durante a troca. É o desenho certo quando a migração de schema não é compatível para trás ou quando se quer trocar 100% do tráfego de uma vez — e é exatamente o L39.
Alt: Canário por peso no ALB — Dá decisão baseada em tráfego real, e cobra observabilidade que distinga as duas versões nas métricas. Sem essa distinção, o canário é rolling update com mais peças.
Alt: Recreate (parar tudo, subir tudo) — É o desenho mínimo deste módulo. Continua legítimo para ambiente de desenvolvimento, onde a janela não custa nada e a simplicidade vale mais.
Alt: Lambda com alias e deslocamento de peso — Publicação sem janela sai de graça no modelo, mas troca a arquitetura inteira: não é uma alternativa de deploy, é uma alternativa de computação. Fica no L21.
| Decisão | Escolha | Alternativas | Motivo | O que se perde |
|---|---|---|---|---|
| Identidade da imagem | SHA do commit, tag imutável | `:latest`; versão semântica; digest direto | rollback passa a ser referência, não reconstrução | a tag deixa de ser legível por humano; exige o commit para interpretar |
| Prazo de drenagem | 30 s, derivado do p99 de 400 ms | 300 s (padrão); 5 s (recomendação da AWS para resposta sub-segundo) | cobre o p99 com duas ordens de grandeza de folga e não alonga o rollout | se um dia entrar rota de upload longo, este valor passa a cortar conexão legítima |
| Health check do grupo | 2 × 5 s em `/health/ready` | 5 × 30 s (padrão); 2 × 10 s | tira 140 s de cada rollout e é a parcela dominante | mais requisições de verificação; e task instável entra em serviço mais rápido |
| Vivacidade e prontidão | duas rotas distintas | uma rota só para os dois usos | evita reinício em laço quando o banco cai e evita tráfego para quem não atende | mais código e a obrigação de manter a distinção clara para quem chegar depois |
| Estratégia de troca | rolling update do ECS | blue/green (L39); canário por peso; recreate | resolve o problema declarado sem recurso novo na fatura | as duas versões convivem: exige compatibilidade de schema para trás |
| Volta automática | disjuntor com rollback | só alarme; rollback manual | a equipe de duas pessoas não tem vigília noturna | rollout legitimamente lento pode ser revertido; ajusta-se com o período de graça |
| Sessão pegajosa | desligada | cookie do balanceador | conexão que dura mais faz drenagem durar mais, e não há estado em memória | nada aqui; seria diferente se houvesse sessão local |
A dívida que o rolling update cria, e que este módulo não paga
Se as duas versões convivem por dois minutos, uma migration que remove coluna quebra a versão velha enquanto ela ainda atende. O rolling update TRANSFERE o problema para o schema, e é honesto dizer isso aqui: a solução é expand/contract, e ela é o L18. Enquanto você não tiver isso, mantenha as migrations aditivas.
Construir: a imagem, e por que o SDK não vai para produção
O multi-stage não é economia de disco. Ele é duas coisas que importam ao rollout: menos bytes para baixar em cada task nova, que encurta a parcela de partida; e menos superfície, porque compilador e gerenciador de pacote não têm função em produção.
# Dockerfile — multi-stage, e por que cada estagio existe
# ── Estagio de build: traz o SDK inteiro, que nao vai para producao ───────────
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src
# Copiar SO o csproj antes do restore aproveita o cache de camada: mudar codigo
# nao invalida o restore, e o build cai de minutos para segundos. E tambem
# encurta a parcela `partida` da formula de rollout.
COPY ["Api/Api.csproj", "Api/"]
RUN dotnet restore "Api/Api.csproj"
COPY . .
RUN dotnet publish "Api/Api.csproj" -c Release -o /app/publish /p:UseAppHost=false
# ── Estagio final: runtime apenas ─────────────────────────────────────────────
# O SDK tem compilador, NuGet e cadeia de build — nada disso serve em producao, e
# tudo isso e superficie de ataque e bytes a baixar em CADA task nova.
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS final
WORKDIR /app
# Usuario sem privilegio. A imagem `aspnet` ja traz o `app` (UID 64198) criado.
USER app
# curl e para o health check do conteiner declarado na task definition. Se voce
# preferir imagem sem shell nem curl, troque por uma rota checada apenas pelo
# grupo de destino — mas ai perca a deteccao de conteiner vivo que nao serve.
COPY --from=build /app/publish .
EXPOSE 8080
ENV ASPNETCORE_URLS=http://+:8080
ENTRYPOINT ["dotnet", "Api.dll"]
A ordem do COPY é a otimização real
Copiar só o `.csproj` antes do `restore` faz a camada de dependências sobreviver a mudança de código. Sem isso, cada commit rebaixa todas as dependências e o build passa de segundos a minutos. É a diferença entre publicar quando precisa e publicar quando dá.
Construir: o registro com tag imutável
A imutabilidade é o que sustenta todo o raciocínio de rollback deste módulo. Sem ela, apontar para a revisão anterior da task definition não garante nada — a imagem daquela tag pode ter sido sobrescrita.
# ecr.tf — o registro, e a imutabilidade que torna rollback possível
resource "aws_ecr_repository" "api" {
name = "${var.projeto}-api"
# A trava central deste laboratório. Com IMMUTABLE, enviar de novo a mesma tag
# falha em vez de sobrescrever — então a task definition que aponta para
# `...:a1b2c3d` continua se referindo ao MESMO conteúdo daqui a seis meses.
# É o que transforma rollback de "reconstruir e esperar dar certo" em "apontar".
image_tag_mutability = "IMMUTABLE"
# Varredura na entrada. Reprovar aqui é barato; descobrir a CVE em produção não.
image_scanning_configuration {
scan_on_push = true
}
encryption_configuration {
encryption_type = "KMS"
kms_key = aws_kms_key.app.arn
}
}
# Sem política de ciclo de vida, tag imutável vira dívida de armazenamento: cada
# commit deixa uma imagem para sempre. Mantemos as 30 últimas — o suficiente para
# voltar alguns dias, que é o horizonte real de um rollback.
resource "aws_ecr_lifecycle_policy" "api" {
repository = aws_ecr_repository.api.name
policy = jsonencode({
rules = [{
rulePriority = 1
description = "Mantem as 30 imagens mais recentes"
selection = {
tagStatus = "any"
countType = "imageCountMoreThan"
countNumber = 30
}
action = { type = "expire" }
}]
})
}
# A task só precisa LER do registro, e quem lê é o agente de execução — não a
# aplicação. Menor privilégio aqui é separar os dois papéis, e é por isso que
# existem `execution_role` e `task_role` distintos.
data "aws_iam_policy_document" "pull_imagem" {
statement {
effect = "Allow"
actions = [
"ecr:GetAuthorizationToken",
"ecr:BatchCheckLayerAvailability",
"ecr:GetDownloadUrlForLayer",
"ecr:BatchGetImage",
]
# GetAuthorizationToken nao aceita recurso especifico: e uma operacao de
# conta, nao de repositorio. As outras tres sao restritas ao repositorio,
# e e assim que o `*` se justifica em vez de se esconder.
resources = ["*"]
}
}
output "repositorio_api" {
value = aws_ecr_repository.api.repository_url
description = "URL do repositorio; a tag e sempre o SHA do commit"
}
Tag imutável sem política de ciclo de vida é dívida
Se nada expira, cada commit deixa uma imagem para sempre, e o armazenamento do registro cresce sem teto. Trinta imagens cobrem o horizonte real de um rollback — que é de dias, não de meses. Escolha o número pela sua cadência de publicação, não pelo que parece seguro.
Construir: prontidão que diz a verdade
Duas rotas, duas perguntas. /health/live responde "o processo está vivo?" e não toca em dependência nenhuma. /health/ready responde "posso receber requisição agora?" e por isso consulta o banco.
Inverter isso produz o pior incidente desta arquitetura: se a vivacidade consultasse o banco, uma indisponibilidade do banco marcaria todos os contêineres como mortos, o ECS os substituiria em laço, e uma falha parcial — banco fora, aplicação de pé — viraria falha total, com o agravante de que as tasks novas também nasceriam mortas.
// Program.cs — prontidao que diz a verdade, e desligamento que devolve tempo
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContextPool<AppDb>(o =>
o.UseNpgsql(builder.Configuration.GetConnectionString("Padrao")));
// As duas rotas de saude respondem a perguntas DIFERENTES, e e por isso que sao
// duas. Misturá-las produz um dos dois defeitos: ou o balanceador manda trafego
// para quem nao pode atender, ou o ECS mata um conteiner que estava so ocupado.
builder.Services.AddHealthChecks()
// VIVACIDADE: "o processo esta vivo?". Nao toca em dependencia nenhuma.
// Se ela consultasse o banco, uma queda do banco reiniciaria todas as tasks
// em laco — trocando indisponibilidade parcial por indisponibilidade total.
.AddCheck("self", () => HealthCheckResult.Healthy(), tags: ["live"])
// PRONTIDAO: "posso receber requisicao AGORA?". Aqui a dependencia entra,
// porque uma task sem pool de conexao aberto vai responder 500 ao primeiro
// cliente — e esse 500 aparece depois do rollout, sem ninguem ligar as duas coisas.
.AddNpgSql(
builder.Configuration.GetConnectionString("Padrao")!,
name: "banco",
tags: ["ready"]);
// Coerente com o stopTimeout de 30 s da definicao da task: o .NET tem de
// terminar ANTES de o ECS perder a paciencia e enviar SIGKILL.
builder.Services.Configure<HostOptions>(o =>
o.ShutdownTimeout = TimeSpan.FromSeconds(
builder.Configuration.GetValue("SHUTDOWN_TIMEOUT_SECONDS", 25)));
var app = builder.Build();
app.MapHealthChecks("/health/live", new HealthCheckOptions
{
Predicate = r => r.Tags.Contains("live")
});
app.MapHealthChecks("/health/ready", new HealthCheckOptions
{
Predicate = r => r.Tags.Contains("ready")
});
// O ASP.NET Core ja trata SIGTERM: ele para de aceitar conexao nova, termina o
// que esta em voo e sai. O log abaixo existe para o laboratorio — e para tornar
// a ordem dos estados OBSERVAVEL, que e a prova da secao seguinte.
var vida = app.Services.GetRequiredService<IHostApplicationLifetime>();
var log = app.Services.GetRequiredService<ILogger<Program>>();
vida.ApplicationStopping.Register(() =>
log.LogWarning("SIGTERM recebido em {Instante} — drenagem do grupo de destino JA terminou",
DateTimeOffset.UtcNow.ToString("O")));
vida.ApplicationStopped.Register(() =>
log.LogWarning("processo encerrado em {Instante}", DateTimeOffset.UtcNow.ToString("O")));
app.MapGet("/api/pedidos/{id:guid}", async (Guid id, AppDb db) =>
await db.Pedidos.FindAsync(id) is { } p ? Results.Ok(p) : Results.NotFound());
app.Run();
Por que o log de SIGTERM está ali
Ele não é diagnóstico de produção: é instrumento do laboratório. O horário desse log, comparado ao instante em que o alvo saiu de `draining`, é a prova empírica de que a drenagem terminou antes de o sinal chegar. É a diferença entre saber a ordem dos estados e tê-la verificado.
Construir: os três relógios, explícitos no Terraform
Os três estão em objetos diferentes, e é isso que faz com que quase nunca sejam lidos juntos: o do health check e o da drenagem no grupo de destino, o do desligamento na definição da task. Os comentários amarram um ao outro.
# deploy.tf — os tres relogios do rollout, explicitos
# ── Relogio 1: quanto tempo até a task nova ser considerada pronta ────────────
resource "aws_lb_target_group" "api" {
name = "${var.projeto}-api"
port = 8080
protocol = "HTTP"
vpc_id = aws_vpc.principal.id
target_type = "ip" # awsvpc: o alvo e o IP da task, nao a instancia
health_check {
path = "/health/ready" # prontidao, nao vivacidade — sao rotas diferentes
# 2 × 5 s = 10 s para entrar em servico, contra 150 s do padrao
# (5 × 30 s). E o ajuste com maior efeito sobre a duracao do rollout.
interval = 5
timeout = 4 # < interval, senao verificacoes se sobrepoem
healthy_threshold = 2
unhealthy_threshold = 2
matcher = "200"
}
# ── Relogio 2: o TETO da drenagem ──────────────────────────────────────────
# Nao e uma espera fixa. O ECS aguarda o balanceador informar que a conexao
# keep-alive do cliente fechou (alvo em UNUSED); este valor e o limite dessa
# paciencia. Como o p99 desta API e 400 ms, 30 s cobre com folga de duas ordens
# de grandeza. O padrao de 300 s nao esta errado — esta apenas generoso, e
# multiplica por dez o tempo de cada rollout.
#
# NAO baixe para 5 s se a rota tiver upload lento, download longo, SSE ou
# WebSocket: ai o prazo passa a cortar conexao legitima.
deregistration_delay = 30
stickiness {
enabled = false # sessao pegajosa faz a conexao durar mais e a drenagem tambem
type = "lb_cookie"
}
}
resource "aws_ecs_task_definition" "api" {
family = "${var.projeto}-api"
requires_compatibilities = ["FARGATE"]
network_mode = "awsvpc"
cpu = 512
memory = 1024
execution_role_arn = aws_iam_role.execucao.arn
task_role_arn = aws_iam_role.task.arn
container_definitions = jsonencode([{
name = "api"
# O SHA do commit, injetado pelo pipeline. Nunca `:latest`: e o que permite
# dizer qual versao esta no ar e voltar para a anterior.
image = "${aws_ecr_repository.api.repository_url}:${var.sha_da_imagem}"
essential = true
portMappings = [{ containerPort = 8080, protocol = "tcp" }]
# ── Relogio 3: do SIGTERM ao SIGKILL ─────────────────────────────────────
# Em Fargate isto vai na definicao da task. O ECS_CONTAINER_STOP_TIMEOUT que
# a documentacao cita e opcao do AGENTE, configurada na instancia — e em
# Fargate voce nao tem instancia, entao ele nao se aplica. Confundir os dois
# faz alguem ajustar uma variavel que nunca sera lida.
stopTimeout = 30
healthCheck = {
# Health check do ECS ≠ health check do grupo de destino. Este decide se o
# CONTEINER e substituido; o do grupo decide se ele RECEBE TRAFEGO. Ter os
# dois evita a task viva que nao serve e nunca e trocada.
command = ["CMD-SHELL", "curl -f http://localhost:8080/health/live || exit 1"]
interval = 10
timeout = 5
retries = 3
startPeriod = 30
}
environment = [
{ name = "ASPNETCORE_URLS", value = "http://+:8080" },
# Coerente com o stopTimeout acima: o .NET encerra antes de o ECS perder a
# paciencia. Se ficar maior, o SIGKILL chega no meio do desligamento limpo.
{ name = "SHUTDOWN_TIMEOUT_SECONDS", value = "25" },
]
secrets = [{
name = "ConnectionStrings__Padrao"
valueFrom = aws_secretsmanager_secret.banco.arn
}]
logConfiguration = {
logDriver = "awslogs"
options = {
"awslogs-group" = aws_cloudwatch_log_group.api.name
"awslogs-region" = var.regiao
"awslogs-stream-prefix" = "api"
}
}
}])
}
resource "aws_ecs_service" "api" {
name = "${var.projeto}-api"
cluster = aws_ecs_cluster.principal.id
task_definition = aws_ecs_task_definition.api.arn
desired_count = 2 # duas AZs; e tambem o minimo para haver rollout sem janela
launch_type = "FARGATE"
# O par que autoriza a troca sem janela. O maximo da o espaco para a task nova
# existir; o minimo PROIBE descer abaixo da capacidade declarada. Um sem o
# outro nao resolve: com maximo 200 e minimo 0, o ECS pode parar tudo primeiro.
deployment_minimum_healthy_percent = 100
deployment_maximum_percent = 200
# Rollout que nao estabiliza volta sozinho para a revisao anterior. E o que
# permite publicar sem alguem de vigia — que era o problema real da equipe.
deployment_circuit_breaker {
enable = true
rollback = true
}
# Tempo em que o health check do balanceador e IGNORADO depois de a task subir.
# Sem isto, aplicacao com migration na partida e marcada insalubre, morta e
# reiniciada — um ciclo que parece "o deploy travou" e nao deixa erro claro.
health_check_grace_period_seconds = 60
network_configuration {
subnets = aws_subnet.privada[*].id
security_groups = [aws_security_group.task.id]
assign_public_ip = false
}
load_balancer {
target_group_arn = aws_lb_target_group.api.arn
container_name = "api"
container_port = 8080
}
depends_on = [aws_lb_listener.https]
}
# Mede o ALVO, nao o balanceador. A distincao e diagnostica: 5xx do alvo acusa a
# versao nova respondendo errado; 5xx do ALB acusa ausencia de alvo saudavel.
# O mesmo numero nas duas metricas conta historias opostas.
resource "aws_cloudwatch_metric_alarm" "erro_5xx_do_alvo" {
alarm_name = "${var.projeto}-api-5xx-alvo"
namespace = "AWS/ApplicationELB"
metric_name = "HTTPCode_Target_5XX_Count"
statistic = "Sum"
period = 60
evaluation_periods = 2
threshold = 5
comparison_operator = "GreaterThanThreshold"
treat_missing_data = "notBreaching" # ausencia de erro nao e ausencia de dado
dimensions = {
LoadBalancer = aws_lb.principal.arn_suffix
TargetGroup = aws_lb_target_group.api.arn_suffix
}
alarm_actions = [aws_sns_topic.alertas.arn]
}
A pegadinha do stopTimeout em Fargate
A documentação de drenagem cita `ECS_CONTAINER_STOP_TIMEOUT`, que é opção do AGENTE do ECS — configurada na instância EC2. Em Fargate você não tem instância, então essa variável não se aplica: o que vale é `stopTimeout` na definição do contêiner. Quem lê a página sem notar a distinção ajusta uma variável que nunca será lida e conclui que "não funciona".
| Onde | Parâmetro | Padrão | Aqui | Por quê |
|---|---|---|---|---|
| Grupo de destino | `interval` × `healthy_threshold` | 30 s × 5 = 150 s | 5 s × 2 = 10 s | é a parcela dominante do rollout, e a API responde em 400 ms |
| Grupo de destino | `deregistration_delay` | 300 s | 30 s | teto de paciência; cobre o p99 com folga sem alongar a troca |
| Definição da task | `stopTimeout` | 30 s | 30 s (explícito) | explícito para ficar ao lado do `ShutdownTimeout` do .NET, que é 25 s |
| Aplicação .NET | `HostOptions.ShutdownTimeout` | 30 s | 25 s | menor que o `stopTimeout`, senão o SIGKILL chega no meio do desligamento limpo |
| Serviço ECS | `health_check_grace_period_seconds` | 0 | 60 s | a migration na partida leva até 20 s; sem graça, a task é morta antes de ficar pronta |
| Serviço ECS | mínimo / máximo | 100% / 200% | 100% / 200% | o padrão já é o correto — mas explícito, porque é o par que autoriza o excedente |
Implantar, e provar que ninguém viu
#!/usr/bin/env bash
# implantar.sh — a ordem importa, e cada passo tem prova
set -euo pipefail
PROJETO="${PROJETO:?defina PROJETO}"
REGIAO="${REGIAO:-us-east-1}"
CONTA="$(aws sts get-caller-identity --query Account --output text)"
REPO="${CONTA}.dkr.ecr.${REGIAO}.amazonaws.com/${PROJETO}-api"
# A identidade da imagem. `--short=12` e legivel e nao colide na pratica; use o
# SHA completo se preferir. `--dirty` impede publicar arvore com alteracao nao
# comitada, que produziria imagem sem commit correspondente.
SHA="$(git describe --always --dirty --match='' --abbrev=12)"
if [[ "$SHA" == *-dirty ]]; then
echo "arvore com alteracao nao comitada: a imagem nao teria commit que a explique" >&2
exit 1
fi
aws ecr get-login-password --region "$REGIAO" \
| docker login --username AWS --password-stdin "${CONTA}.dkr.ecr.${REGIAO}.amazonaws.com"
docker build --platform linux/amd64 -t "${REPO}:${SHA}" .
# Com tag imutavel, reenviar a mesma tag FALHA — e essa falha e desejavel: ela
# diz que este commit ja foi publicado, e que nao ha nada novo a implantar.
docker push "${REPO}:${SHA}" || {
echo "push recusado: a tag ${SHA} ja existe (imutabilidade). Nada a publicar." >&2
exit 0
}
# Terraform cria a REVISAO nova da task definition e atualiza o servico. A
# revisao anterior continua existindo — e e para ela que o rollback aponta.
terraform apply -var="sha_da_imagem=${SHA}" -auto-approve
# `services-stable` volta quando o rollout terminou E estabilizou. Sem esta
# espera, o pipeline segue e um rollout que sera revertido em 90 s e reportado
# como sucesso.
aws ecs wait services-stable \
--cluster "${PROJETO}" --services "${PROJETO}-api" --region "$REGIAO"
echo "no ar: ${SHA}"
Quatro provas. Nenhuma delas aceita "parece que funcionou" como resultado — cada uma tem um número ou um estado esperado, e a quarta é a que mais gente esquece de fazer.
# provas.sh — quatro medições; nenhuma conclusão vem de "parece que funcionou"
PROJETO=ffv-lab; REGIAO=us-east-1
TG=$(aws elbv2 describe-target-groups --names "${PROJETO}-api" \
--query 'TargetGroups[0].TargetGroupArn' --output text)
# ── Prova 1: zero erro do cliente durante o rollout inteiro ──────────────────
# Um laco de requisicoes em paralelo ao deploy. E a unica prova que interessa ao
# criterio de aceite: qualquer codigo diferente de 200 aqui reprova o rollout.
URL="https://$(terraform output -raw dominio)/api/health/ready"
( while true; do
curl -s -o /dev/null -w '%{http_code} ' "$URL"; sleep 0.2
done ) & LACO=$!
./implantar.sh
kill $LACO
# Esperado: uma fileira continua de 200. Um unico 503 significa janela sem alvo
# saudavel; um 502 ou 504 significa conexao cortada na drenagem.
# ── Prova 2: a ordem dos estados, observada — não decorada ───────────────────
# Roda durante o rollout. A task velha tem de passar por DEACTIVATING ANTES de
# STOPPING, e e isto que mostra que a drenagem precede o SIGTERM.
watch -n1 "aws ecs describe-tasks --cluster $PROJETO \
--tasks \$(aws ecs list-tasks --cluster $PROJETO --service-name ${PROJETO}-api \
--desired-status ALL --query 'taskArns' --output text) \
--query 'tasks[].{ver:taskDefinitionArn,ultimo:lastStatus,desejado:desiredStatus}' \
--output table"
# ── Prova 3: o SIGTERM chega DEPOIS da drenagem ──────────────────────────────
# Compare o instante do log da aplicacao com o instante em que o alvo saiu de
# `draining`. O log da aplicacao tem de ser o mais TARDIO dos dois.
aws logs filter-log-events \
--log-group-name "/ecs/${PROJETO}-api" \
--filter-pattern "SIGTERM recebido" \
--query 'events[].message' --output text
# ── Prova 4: a imutabilidade da tag realmente impede sobrescrever ────────────
# Deve falhar. Se passar, `image_tag_mutability` nao esta como IMMUTABLE, e todo
# o raciocinio de rollback deste modulo esta sem base.
SHA=$(git describe --always --abbrev=12)
docker push "$(terraform output -raw repositorio_api):${SHA}" \
&& echo "FALHA DA PROVA: a tag foi sobrescrita" \
|| echo "OK: o registro recusou sobrescrever ${SHA}"
| Prova | Comando | Resultado que aprova | O que reprova, e o que significa |
|---|---|---|---|
| 1 · Cliente não vê erro | laço de `curl` durante o rollout | fileira contínua de 200 | um 503 é janela sem alvo; um 502/504 é conexão cortada na drenagem |
| 2 · A ordem dos estados | `describe-tasks` em laço | a task velha passa por `DEACTIVATING` antes de `STOPPING` | se você não vê `DEACTIVATING`, o serviço não está atrelado ao grupo de destino |
| 3 · SIGTERM depois da drenagem | log da aplicação vs. saída de `draining` | o log da aplicação é o mais tardio dos dois | ordem inversa indicaria que a task foi parada fora do fluxo do serviço |
| 4 · A tag é imutável | reenviar a mesma tag | o registro recusa | se aceitou, `image_tag_mutability` não está IMMUTABLE e o rollback não tem base |
| 5 · O rollback funciona | apontar para a revisão anterior | volta ao ar sem build | se exigiu reconstruir, a imagem antiga expirou no ciclo de vida |
# O rollback que o resto do modulo tornou possivel: sem build, sem push.
# A revisao anterior da task definition continua existindo, e a imagem dela
# tambem — porque a tag e imutavel e o ciclo de vida guarda 30.
PROJETO=ffv-lab
ANTERIOR=$(aws ecs describe-services --cluster $PROJETO \
--services ${PROJETO}-api \
--query "services[0].deployments[?status=='ACTIVE'].taskDefinition | [0]" \
--output text)
# Se o disjuntor ja reverteu sozinho, este comando nao tem o que fazer — e
# essa e a situacao desejada. O rollback manual e a rede sob a rede.
aws ecs update-service --cluster $PROJETO --service ${PROJETO}-api \
--task-definition "$ANTERIOR"
aws ecs wait services-stable --cluster $PROJETO --services ${PROJETO}-api
# Confirme QUAL versao voltou. "O rollback funcionou" sem o SHA na mao e fe.
aws ecs describe-task-definition --task-definition "$ANTERIOR" \
--query "taskDefinition.containerDefinitions[0].image" --output textQuebrar de propósito: três falhas e o diagnóstico
As três acontecem de verdade, e as três se parecem no sintoma superficial — "o deploy não terminou". O que as separa é onde se olha.
| Falha | Como provocar | Sintoma | Onde olhar | Correção |
|---|---|---|---|---|
| Prontidão que mente | faça `/health/ready` devolver 200 fixo, sem consultar o banco, e suba com o segredo do banco errado | rollout conclui com sucesso e o 5xx do alvo começa DEPOIS | alarme de `HTTPCode_Target_5XX_Count` sobe com o rollout já terminado | prontidão consulta a dependência; o disjuntor só protege quem admite estar doente |
| Graça insuficiente | ponha `health_check_grace_period_seconds = 0` e mantenha a migration na partida | tasks sobem e morrem em laço; o deploy nunca estabiliza | `describe-services` mostra eventos de alvo insalubre em sequência, sem erro na aplicação | período de graça maior que o pior tempo de partida, medido — não estimado |
| Drenagem curta demais | baixe `deregistration_delay` para 1 s e gere uma requisição de 3 s | 502 ou 504 esporádico, só durante o deploy, difícil de reproduzir | `RequestCount` do alvo em `draining` ainda diferente de zero quando ele fecha | prazo ≥ p99 real; e se houver rota longa, ela decide o valor, não a média |
A falha que o disjuntor NÃO pega
O disjuntor observa se o rollout estabiliza, ou seja, se as tasks ficam saudáveis. Uma versão que sobe, fica saudável e responde 500 para o cliente passa por ele sem ser notada — porque, do ponto de vista do ECS, o rollout deu certo. É exatamente por isso que o alarme de 5xx do alvo existe, e por que ele é medido no alvo e não no balanceador. As duas proteções cobrem falhas diferentes, e nenhuma cobre as duas.
Um serviço ECS com `desired_count = 2`, `minimum_healthy_percent = 100` e `maximum_percent = 100` não consegue concluir nenhum deploy. Por quê?
Segurança: o que muda quando o deploy fica fácil
Publicar sem dor aumenta a frequência de publicação, e isso desloca o risco: a superfície deixa de ser "a versão no ar" e passa a ser "o caminho pelo qual qualquer versão entra". Quem pode enviar imagem passa a poder executar código na sua VPC.
| Risco | Probabilidade | Impacto | Controle preventivo | Detecção | Resposta |
|---|---|---|---|---|---|
| Imagem com CVE conhecida entra em produção | alta | médio | varredura na entrada do registro; falhar o pipeline por severidade | achados do ECR e do Inspector | reconstruir com base atualizada; a tag imutável garante saber o que estava no ar |
| Credencial de longa duração na máquina do dev | alta | alto | este laboratório NÃO resolve — é o L54, com OIDC federado | CloudTrail: `PutImage` de fora do pipeline | revogar a chave, rotacionar, mover o deploy para o pipeline |
| Sobrescrita de tag para mascarar conteúdo | baixa | alto | `image_tag_mutability = "IMMUTABLE"` | CloudTrail em `PutImage` recusado | auditar quem tentou; a recusa já impediu o efeito |
| Segredo do banco no log de deploy | média | alto | segredo por referência de ARN no `secrets`, nunca em `environment` | busca por padrão de credencial no grupo de logs | rotacionar o segredo; corrigir a definição |
| Excesso de permissão no papel de execução | média | médio | papéis separados de execução e de task; leitura restrita ao repositório | IAM Access Analyzer sobre uso real | derivar a política do uso medido — é o L41 |
| Rollback para versão vulnerável | baixa | médio | ciclo de vida guarda 30 imagens, e o alarme não olha versão | varredura contínua também nas imagens retidas | marcar a versão como não elegível a rollback e avançar, não voltar |
O `*` que aparece na política, e por que ele se justifica
`ecr:GetAuthorizationToken` é uma operação de CONTA, não de repositório: ela não aceita recurso específico, e escrevê-la com ARN de repositório simplesmente não autoriza nada. As outras três ações da mesma política são restritas ao repositório. A regra não é "nunca use `*`" — é "todo `*` tem de ter uma frase explicando por que não pode ser mais estreito". Sem essa frase, é preguiça; com ela, é decisão.
Observabilidade: as perguntas que o painel tem de responder
Um painel de deploy tem uma função estreita: dizer, durante os três minutos do rollout, se ele deve continuar. Métrica que não ajuda nessa decisão pertence a outro painel.
| Pergunta | Métrica ou consulta | O que significa mudar | Limiar inicial |
|---|---|---|---|
| A troca gerou erro para o cliente? | `HTTPCode_ELB_5XX_Count` | ausência de alvo saudável — janela de capacidade | qualquer valor > 0 durante rollout |
| A versão nova está respondendo errado? | `HTTPCode_Target_5XX_Count` | a task está saudável e a resposta não presta | > 5 em 2 períodos de 1 min |
| Quantas tasks estão em serviço agora? | `HealthyHostCount` do grupo | abaixo do desejado durante a troca é o excedente não funcionando | < 2 por 2 min |
| O rollout avançou? | `describe-services` → `deployments[].rolloutState` | `IN_PROGRESS` por muito tempo é task que não fica pronta | > 5 min |
| A latência mudou com a versão nova? | `TargetResponseTime` p99 | regressão de desempenho que o health check não pega | > 2× a linha de base |
| Qual SHA está no ar? | imagem da task definition ativa | a pergunta que `:latest` tornava impossível | sempre respondível |
| Alguém publicou de fora do caminho? | CloudTrail em `PutImage` e `UpdateService` | deploy manual em produção | qualquer identidade fora da esperada |
A métrica que engana durante o rollout
`RunningTaskCount` sobe para 4 quando o excedente entra em ação, e um alarme de "tasks demais" dispara em todo deploy. O número não indica problema: ele é a condição de o deploy não ter janela. Alarme sobre contagem de tasks precisa de janela que tolere o excedente, ou vira ruído que ninguém mais lê.
Escala: 10, 10 mil, 1 milhão
| Volume | O que acontece com o rollout | O que passa a doer | O que fazer |
|---|---|---|---|
| 10 req/s, 2 tasks | duas substituições em sequência; cerca de 2 min | nada; é o cenário do laboratório | nada |
| 500 req/s, 8 tasks | oito substituições; o tempo cresce com a contagem | o rollout passa de 8 min e a janela de risco se alonga | aumentar o excedente (`maximum_percent` acima de 200) para trocar em lotes maiores |
| 5 mil req/s, 60 tasks | excedente de 60 tasks exige endereços e cota | esgotamento de IP na sub-rede e limite de tasks da conta | dimensionar a sub-rede pelo DOBRO da frota; conferir cota antes de escalar |
| Pico durante o deploy | a escala automática e o rollout competem | duas forças mexendo na contagem ao mesmo tempo | não é para impedir; é para saber. O mínimo de 100% protege a capacidade |
| Falha de AZ durante o rollout | metade dos alvos some no meio da troca | com 2 tasks, sobra 1 — o mínimo de 100% não pode ser satisfeito | a partir de 4 tasks o rollout sobrevive a uma AZ; com 2, ele pausa e espera |
| Requisição longa aparece no produto | a drenagem de 30 s passa a cortá-la | o valor derivado do p99 de 400 ms deixou de valer | rever o prazo quando a distribuição de latência mudar — é decisão com prazo de validade |
O gargalo que só aparece em frota grande
Em modo `awsvpc` cada task consome um endereço da sub-rede. Durante um rollout com excedente de 100%, a frota dobra — então a sub-rede precisa de espaço para o dobro do regime permanente. Quem dimensionou para a frota exata vê o deploy travar em `PROVISIONING` sem nenhum erro de aplicação. É o L02 cobrando o plano de endereçamento que ele mandou documentar.
Custo: o que este laboratório acrescenta à fatura
É o laboratório mais barato da banda, e vale entender por quê: quase tudo o que ele introduz é configuração, não recurso. O excedente existe por minutos e desaparece.
| Cenário | Volume | O que acrescenta | Tendência | Otimização |
|---|---|---|---|---|
| Protótipo | 1 deploy/semana, 2 tasks | minutos de excedente e alguns MB de registro | desprezível | nenhuma; otimizar aqui é gastar atenção onde não há dinheiro |
| Produção pequena | 5 deploys/dia, 2 tasks | excedente somado no mês fica na ordem de horas de uma task; 30 imagens no registro | baixa e previsível | ciclo de vida no registro; imagem menor reduz transferência do pull |
| Alta escala | 20 deploys/dia, 60 tasks | excedente de 60 tasks por vários minutos, 20 vezes ao dia | passa a ser linha visível na fatura de Fargate | trocar em lotes menores (excedente de 25%) troca custo por duração do rollout |
| Dimensão | Cobra por | Cuidado |
|---|---|---|
| Fargate durante o excedente | vCPU-segundo e GB-segundo das tasks extras | é proporcional a deploys × tasks × duração do rollout — encurtar o rollout economiza aqui |
| Armazenamento do ECR | GB-mês de imagem retida | tag imutável sem ciclo de vida cresce para sempre |
| Transferência do pull | saída do registro para a task | de graça na mesma região; com endpoint de VPC evita-se também o NAT (L44) |
| NAT durante o pull | GB processados | cada task nova baixa a imagem inteira; 20 deploys × 60 tasks × 200 MB soma |
| CloudWatch Logs | GB ingerido e retido | log de partida repetido a cada task nova; retenção curta em ambiente de teste |
| Alarmes | por alarme-mês | valor pequeno e fixo; não é onde se economiza |
O ganho de custo que não está na fatura de infraestrutura
Reduzir o rollout de 82 s de indisponibilidade para zero não aparece em nenhuma linha da AWS. Aparece na frequência de publicação: a Cadência passou de um deploy a cada duas semanas para vários por dia, e o lote de trinta commits virou lote de um. O custo que se atacou aqui era o tempo de diagnóstico, e ele estava na folha de pagamento.
Well-Architected nos seis pilares
| Pilar | Situação ao fim deste laboratório | Risco que fica | Melhoria | Prioridade |
|---|---|---|---|---|
| Excelência operacional | deploy roteirizado, com prova de ausência de erro e volta automática | o deploy ainda sai da máquina de uma pessoa | pipeline com credencial federada (L54) | alta |
| Segurança | imagem varrida, tag imutável, segredo por referência, papéis separados | credencial de longa duração no laptop | OIDC federado (L54) e política derivada do uso (L41) | alta |
| Confiabilidade | troca sem janela, disjuntor com volta, período de graça dimensionado | compatibilidade de schema entre versões que convivem | expand/contract nas migrations (L18) | alta |
| Eficiência de performance | rollout abaixo de 3 min; imagem enxuta; health check ajustado ao p99 | a escala ainda é manual no número de tasks | escala automática por métrica (L06) | média |
| Otimização de custos | excedente efêmero, ciclo de vida no registro | o pull passa pelo NAT e paga por GB | endpoint de VPC para o ECR (L44) | média |
| Sustentabilidade | imagem menor e menos bytes movidos por task | imagem retida sem uso ocupa armazenamento indefinidamente | o ciclo de vida já configurado cobre; revisar o número 30 com dados de uso | baixa |
Evolução em níveis: o que muda, e o que passa a doer
A terceira arquitetura não é um desenho: é a resposta a QUANDO trocar de desenho. Cada nível resolve um risco e compra outro, e é a segunda coluna que raramente se escreve.
Deploy manual com `:latest`, uma task, janela aceita. É onde a Cadência estava, e continua legítimo em ambiente de desenvolvimento.Tag por SHA, excedente, três relógios ajustados, disjuntor com volta e alarme de 5xx do alvo.O deploy sai da máquina do dev: credencial federada por OIDC, build reprodutível, aprovação para produção (L54).Canário ou blue/green com deslocamento de peso e rollback por métrica de negócio, não só por saúde (L39).Conta por ambiente, promoção de artefato entre contas, assinatura de imagem e admissão que recusa imagem não assinada.O histórico de rollouts vira dado: correlacionar SHA, métrica e incidente para prever qual mudança merece canário mais lento.A ordem não é negociável, e o motivo é concreto
Canário no nível 4 depende de observabilidade que distinga versões, que depende do SHA como identidade — que é o nível 2. Quem tenta canário antes de ter identidade de imagem monta a mecânica de deslocamento de tráfego sem conseguir dizer qual versão gerou qual métrica. É a decoração que o nível anterior evita.
Onde IA entra nesta arquitetura, e onde não entra
Neste módulo, IA não resolve o problema central, e forçá-la seria o antipadrão que a própria série critica. A pergunta "como publicar sem derrubar" tem resposta determinística: excedente autorizado, prontidão honesta e prazo de drenagem derivado do p99. Um modelo não melhora nenhuma das três — elas são aritmética e configuração.
Há um lugar onde IA acrescentaria valor real, e ele é modesto: decidir quanto risco uma mudança carrega. Hoje todo deploy é tratado igual, e não é: alterar uma consulta com índice novo não tem o mesmo perfil que trocar uma string. Um classificador sobre o histórico de rollouts — diff, arquivos tocados, SHA, métrica pós-deploy, incidente aberto — poderia sugerir canário mais lento para as mudanças que historicamente quebraram.
| Pergunta | Resposta honesta para este módulo |
|---|---|
| Qual problema a IA resolveria? | graduar o risco por mudança, em vez de tratar todo deploy igual |
| Por que uma regra não bastaria? | uma regra bastaria para começar: "toca migration → canário lento" cobre a maior parte. IA só se justifica depois que a regra simples estiver no lugar e mostrar seu limite |
| De onde viriam os dados? | CloudTrail, eventos de rollout do EventBridge, métricas do CloudWatch e o histórico de incidentes — tudo já existe |
| Qual o risco? | aprender de poucos incidentes e reprovar mudança boa, ou aprovar a ruim; exige avaliação com dado retido e caminho manual sempre disponível |
| Por que não agora? | porque a equipe tem 30 deploys de histórico. Modelo sobre 30 exemplos é superstição com aparência de estatística |
O uso de IA que parece atraente e é armadilha aqui
Pedir a um modelo para "analisar o log e dizer se o deploy foi bem" troca um sinal determinístico — a métrica de 5xx do alvo, que é exata — por um probabilístico. Onde existe medição direta, um modelo só acrescenta latência e a chance de errar com confiança. IA sobre observabilidade tem lugar quando o sinal é ambíguo ou volumoso demais para regra, e o deploy de duas tasks não é nenhum dos dois.
Anti-padrões deste laboratório
| Anti-padrão | Por que alguém faz | Por que é problema | Sintoma em produção | Forma correta | Quando é aceitável |
|---|---|---|---|---|---|
| `:latest` na task definition | é o padrão de todo tutorial e economiza uma variável | a versão no ar deixa de ser identificável e o rollback vira reconstrução | "não sei qual versão está rodando"; duas tasks com código diferente | tag = SHA do commit, com repositório imutável | nunca em produção; em teste local, indiferente |
| Uma rota de saúde para os dois usos | parece duplicação desnecessária | ou o balanceador manda tráfego para quem não pode atender, ou uma queda de dependência reinicia toda a frota em laço | banco cai e todas as tasks entram em ciclo de reinício | vivacidade sem dependência, prontidão com dependência | aplicação genuinamente sem dependência externa |
| Prazo de drenagem copiado de um blog | o número aparece pronto e "funcionou lá" | curto demais corta conexão legítima; longo demais multiplica o rollout por dez | 502 esporádico só durante deploy, irreprodutível depois | derivar do p99 medido, e revisar quando a latência mudar | aceitar o padrão de 300 s enquanto a duração do rollout não incomodar |
| Mínimo saudável em 0% | faz o deploy "andar mais rápido" e destrava rollout preso | autoriza o ECS a parar tudo antes de subir qualquer coisa: é a janela, por configuração | exatamente a indisponibilidade que se tentava eliminar | mínimo 100% com máximo 200% | job em lote sem tráfego, onde janela não tem custo |
| Período de graça em 0 com migration na partida | ninguém sabe que o parâmetro existe | a task é marcada insalubre e morta antes de terminar de subir, em laço | "o deploy travou" sem nenhum erro na aplicação | graça maior que o pior tempo de partida medido | aplicação que sobe em poucos segundos e não faz trabalho de partida |
| Confiar só no disjuntor | ele reverte sozinho e dá sensação de rede completa | ele mede estabilidade, não correção: versão saudável respondendo 500 passa | rollout "bem-sucedido" e 5xx do alvo subindo depois | disjuntor MAIS alarme de 5xx do alvo — cobrem falhas diferentes | nunca; são complementares, não alternativos |
| Ajustar `ECS_CONTAINER_STOP_TIMEOUT` em Fargate | é o que a documentação de drenagem menciona | é opção do agente, configurada na instância; em Fargate não há instância | o valor não tem efeito nenhum e conclui-se que "não funciona" | `stopTimeout` na definição do contêiner | em launch type EC2, onde o agente é seu |
Quando algo não funciona
| Sintoma | Causa provável | Como investigar | Onde olhar | Correção |
|---|---|---|---|---|
| 503 durante o deploy | sem excedente: mínimo 0% ou uma task só | confira mínimo, máximo e `desired_count` do serviço | `describe-services` → `deploymentConfiguration` | mínimo 100%, máximo 200%, ao menos 2 tasks |
| 502 ou 504 esporádico só no deploy | prazo de drenagem menor que a requisição mais longa | compare o p99 do alvo com o prazo configurado | `TargetResponseTime` e `deregistration_delay` | prazo ≥ p99; se há rota longa, ela decide o valor |
| Rollout em `IN_PROGRESS` sem avançar | a task nova nunca fica saudável | leia os eventos do serviço em ordem cronológica | `describe-services` → `events` | se é alvo insalubre, veja a rota de prontidão; se é `PROVISIONING`, veja endereços |
| Tasks subindo e morrendo em laço | período de graça menor que o tempo de partida | meça quanto a aplicação leva até responder na porta | log da aplicação vs. eventos de substituição | graça maior que o pior tempo de partida medido |
| Deploy trava em `PROVISIONING` | sub-rede sem endereço livre para o excedente | conte endereços disponíveis contra o dobro da frota | `describe-subnets` → `AvailableIpAddressCount` | sub-rede dimensionada para o dobro do regime — é o plano do L02 |
| `CannotPullContainerError` | sem caminho de saída, ou papel de execução sem permissão de leitura | distinga rede de permissão: um dá tempo esgotado, o outro nega acesso | razão de parada da task; CloudTrail no repositório | rota pelo NAT ou endpoint de VPC (L44); política de leitura no papel de execução |
| Push recusado com tag já existente | imutabilidade funcionando como projetado | confirme se o SHA já foi publicado | `describe-images` no repositório | nada a corrigir: não há mudança a publicar. Comite antes |
| Rollback não encontra a imagem | o ciclo de vida expirou a versão antiga | liste imagens e compare com a revisão que se quer | política de ciclo de vida e `describe-images` | aumentar a retenção; avançar com correção em vez de voltar |
| 5xx do alvo depois do rollout concluído | prontidão que não reflete a dependência | chame a rota de prontidão com a dependência derrubada | `HTTPCode_Target_5XX_Count` e a rota `/health/ready` | prontidão consulta o banco; o disjuntor só protege quem admite estar doente |
A pergunta que resolve metade destes casos
Antes de mexer em parâmetro, pergunte: o erro é do balanceador ou do alvo? 5xx do ELB significa que não havia destino — problema de capacidade durante a troca. 5xx do alvo significa que havia destino e ele respondeu mal — problema da versão nova ou da drenagem. As duas métricas têm nomes parecidos e apontam para lados opostos do diagnóstico.
Limpeza: o que o destroy não leva
Este laboratório acrescenta pouco recurso, mas dois dos que acrescenta sobrevivem ao terraform destroy — e um deles cobra por armazenamento indefinidamente.
# 1. Derrube o que o Terraform administra.
terraform destroy -auto-approve
# 2. REPOSITORIO ECR COM IMAGEM: o destroy falha se houver imagem dentro, a
# menos que force_delete esteja ligado. Se voce interrompeu no meio, o
# repositorio pode continuar de pe COM as 30 imagens, cobrando GB-mes.
aws ecr describe-repositories --query "repositories[].repositoryName" --output table
aws ecr list-images --repository-name ffv-lab-api \
--query "length(imageIds)" --output text
aws ecr batch-delete-image --repository-name ffv-lab-api \
--image-ids "$(aws ecr list-images --repository-name ffv-lab-api \
--query 'imageIds' --output json)" 2>/dev/null || true
aws ecr delete-repository --repository-name ffv-lab-api --force
# 3. GRUPO DE LOGS: tem retencao propria e nao pertence ao ciclo do servico.
# Fica cobrando ingestao retida depois de tudo apagado.
aws logs delete-log-group --log-group-name /ecs/ffv-lab-api 2>/dev/null || true
# 4. REVISOES DA TASK DEFINITION: nao cobram, mas acumulam e poluem. Um
# rollout de 20 deploys deixa 20 revisoes ATIVAS.
for arn in $(aws ecs list-task-definitions --family-prefix ffv-lab-api \
--query "taskDefinitionArns" --output text); do
aws ecs deregister-task-definition --task-definition "$arn" >/dev/null
done
# 5. ALARME e topico SNS, se voce os criou fora do Terraform.
aws cloudwatch delete-alarms --alarm-names ffv-lab-api-5xx-alvo 2>/dev/null || true
# 6. E o que veio do L01 e continua cobrando por hora: ALB, RDS, endpoint de
# VPC, NAT Gateway e o Elastic IP dele. Se voce nao vai seguir para o L04,
# rode a limpeza do L01 tambem — e confira o Elastic IP orfao, que cobra
# justamente por NAO estar associado a nada.
aws ec2 describe-addresses --query "Addresses[?AssociationId==null].PublicIp" \
--output table
# 7. Prova final: nada com nome do projeto de pe.
aws resourcegroupstaggingapi get-resources \
--tag-filters Key=Projeto,Values=ffv-lab \
--query "ResourceTagMappingList[].ResourceARN" --output table| Recurso | Sai no destroy? | Cobra parado? | Por que fica |
|---|---|---|---|
| Serviço e definição de task | sim | não | as revisões permanecem registradas, sem custo |
| Repositório ECR | só com `force_delete` | sim, GB-mês | o destroy falha se houver imagem dentro; interrupção no meio deixa tudo de pé |
| Imagens no registro | não | sim, GB-mês | o ciclo de vida expira aos 30, o que não é o mesmo que apagar tudo |
| Grupo de logs | depende de `skip_destroy` | sim, retenção | tem ciclo próprio; sobrevive ao serviço que o alimentava |
| Grupo de destino e ALB | sim | sim, por hora | vêm do L01; conferir se o listener foi removido antes |
| Alarme e tópico SNS | sim se em Terraform | centavos | alarme criado à mão no console não aparece no estado |
| Elastic IP do NAG (L01/L02) | sim | sim, justamente quando ocioso | cobra por NÃO estar associado; é o campeão de fatura esquecida |
Resumo: problema, serviço e motivo
| Problema | Peça | Por que ela, e não outra |
|---|---|---|
| Não sei qual versão está no ar | tag = SHA com repositório imutável | identidade de conteúdo é o que torna a pergunta respondível e o rollback possível |
| Janela sem alvo saudável | mínimo 100% / máximo 200% com 2 tasks | o máximo dá espaço ao excedente; o mínimo obriga a usá-lo |
| Tráfego para task que não pode atender | `/health/ready` consultando o banco | prontidão que mente é a única falha que o disjuntor não pega |
| Frota reiniciando quando o banco cai | `/health/live` sem dependência | vivacidade com dependência transforma falha parcial em total |
| Conexão cortada na troca | `deregistration_delay` derivado do p99 | o prazo é teto de paciência; derivá-lo do dado evita cortar e evita alongar |
| Rollout ruim de madrugada | disjuntor com volta automática | a equipe de duas pessoas não tem vigília — a volta não pode depender de gente |
| Versão saudável respondendo errado | alarme de 5xx do ALVO | o disjuntor mede estabilidade, não correção; são falhas diferentes |
| Task morta antes de terminar de subir | período de graça de 60 s | a migration na partida leva 20 s, e o balanceador não sabe disso |
| Falha | O que a protege | O que ela NÃO protege |
|---|---|---|
| Janela de indisponibilidade no deploy | excedente com mínimo 100% | migration destrutiva entre versões que convivem |
| Versão nova que não sobe | disjuntor com volta automática | versão que sobe saudável e responde errado |
| Versão que responde errado | alarme de 5xx do alvo | regressão de latência sem erro — precisa de alarme de p99 |
| Conexão cortada na drenagem | prazo ≥ p99 | rota longa nova que mudou a distribuição de latência |
| Imagem sobrescrita | tag imutável | imagem legítima com vulnerabilidade — é a varredura |
| Deploy travado por falta de endereço | sub-rede para o dobro da frota | cota de tasks da conta, que é limite separado |
- Commit gera um SHA, que passa a ser a identidade da imagem.
- A imagem é construída em multi-stage e enviada ao registro imutável.
- O Terraform cria uma revisão nova da definição de task apontando para aquele SHA.
- O ECS sobe a task nova sem parar a velha, porque o máximo autoriza o excedente.
- O grupo de destino verifica a prontidão e, em 2 × 5 s, declara a task apta.
- A task velha entra em DEACTIVATING e o grupo drena a conexão do cliente.
- Só então vem STOPPING, com o SIGTERM, e a aplicação encerra sozinha.
- Se o rollout não estabiliza, o disjuntor devolve a revisão anterior.
- Se estabiliza e o 5xx do alvo sobe, o alarme avisa — é a outra falha.
- O laço de curl durante tudo isso registrou 200 do começo ao fim.
Perguntas frequentes
❓ Por que meu deploy no ECS causa erro 503 se a aplicação está saudável?
❓ O ECS envia SIGTERM antes ou depois de drenar as conexões?
❓ Qual valor devo usar em deregistration_delay?
❓ Preciso de dois health checks? Não posso usar a mesma rota nos dois lugares?
❓ Por que não usar a tag :latest, se é mais simples?
❓ O disjuntor de implantação me protege de qualquer deploy ruim?
❓ Por que meu deploy trava em PROVISIONING sem nenhum erro de aplicação?
❓ Rolling update ou blue/green — qual devo escolher?
Fixando
Sua aplicação ignora completamente o SIGTERM. O prazo de drenagem é 60 s e o p99 de resposta é 300 ms. O que acontece com as requisições em voo durante o deploy?
Um rollout conclui com sucesso, o disjuntor não reverte nada, e cinco minutos depois o `HTTPCode_Target_5XX_Count` está subindo. Qual é a causa mais provável?
Conhecimentos, próximo módulo e documentação
| Item | Conteúdo |
|---|---|
| Conhecimentos anteriores necessários | L01 no ar (ECS Fargate, RDS em sub-rede privada, ALB), Docker, Git e Terraform básicos |
| Conhecimentos adquiridos | os três relógios do rollout e qual domina; a ordem entre drenagem e SIGTERM derivada do ciclo de vida; vivacidade vs prontidão; identidade de imagem como base do rollback; o que o disjuntor cobre e o que não cobre |
| Limitação que fica | o deploy ainda sai da máquina de uma pessoa, com credencial de longa duração; e as duas versões convivem, então migration destrutiva quebra a velha |
| Próximo exemplo recomendado | L04 — segredo fora do código com rotação. Reutiliza a definição de task deste módulo, e é onde a credencial do banco deixa de ser estática |
| Também habilitado por este módulo | L39 (blue/green e canário) e L54 (pipeline com OIDC) dependem do rollout confiável construído aqui; L18 (migration sem parar) resolve a dívida de schema que ele criou |
| Data da última validação técnica | 7 de agosto de 2026 |
Documentação oficial consultada: Amazon ECS task lifecycle — os estados e o que ocorre em cada um, que é a fonte da ordenação entre desregistragem e sinal de parada; e Optimize load balancer connection draining parameters for Amazon ECS — a natureza do prazo de drenagem como teto e a recomendação de 5 s para serviços com resposta abaixo de um segundo, com a ressalva explícita para conexão longa. Os valores de preço não aparecem neste módulo por decisão: use o AWS Pricing Calculator, porque preço varia por região e envelhece mais rápido que o conteúdo.
O que não foi verificado, e você deve conferir na sua conta
Os tempos de partida citados — cerca de 40 s para a task e até 20 s de migration — são os medidos na aplicação de exemplo, e servem como ordem de grandeza, não como referência. O período de graça e o limiar do health check devem ser derivados da SUA medição: o pior tempo de partida da sua aplicação, não o típico. Da mesma forma, o prazo de drenagem de 30 s vale para um p99 de 400 ms sem rota longa; se o seu produto tem upload, streaming ou relatório demorado, esse número está errado para você.
Terminou de ler?
Marcar como concluído registra o XP, mantém sua sequência e coloca 3 cartas deste módulo na fila de revisão espaçada.
Próximos passos sugeridos
Temas deste módulo
Discussão
Carregando comentários…