Lab 39 — Blue/green e canário no ECS
O problema, e a empresa que o tem
A Cadência, do L03, cresceu: de trinta para trezentas lojas, e o rolling update com tag imutável e disjuntor de implantação virou rotina — várias publicações por dia, sem drama. Um deploy recente introduziu uma consulta extra por item de um pedido, um N+1 disfarçado de refatoração pequena.
A rota /health/ready aprovou a task normalmente: uma conexão ao banco é barata, e o disjuntor de implantação não interveio, porque o rollout ESTABILIZOU — exatamente o limite do disjuntor que o L03 já havia nomeado. O problema só apareceu na hora de pico, quando trinta e poucas requisições simultâneas esgotaram o pool de conexões do banco e o p99 disparou para segundos. Àquela altura, o excedente do rolling update já tinha deslocado a maior parte do tráfego para a versão nova.
O defeito não é o N+1 em si — é que nada no L03 tinha COMO pegá-lo antes de expor a maioria dos clientes. Prontidão testa uma conexão; o rolling update desloca tráfego assim que os primeiros alvos passam nela. "Deploy ruim só aparece com 100% do tráfego dentro" é o resumo exato do que a Cadência viveu.
O que este laboratório NÃO é
Não é uma reescrita do L03 com nomes diferentes. O rolling update continua sendo a resposta certa para a maioria dos deploys da Cadência — este módulo resolve um problema específico que ele não resolve: limitar a exposição de um defeito que só aparece sob carga real. E não é blue/green "porque é mais robusto": blue/green direto (all-at-once) tem o MESMO problema de exposição total que o rolling update, só que mais rápido — quem resolve o problema declarado é o canário.
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 um defeito de concorrência passa no health check do L03 sem ser notado.
- Distinguir estruturalmente rolling update (L03) de blue/green: uma frota substituída aos poucos contra duas frotas completas convivendo.
- Nomear o que o listener de teste valida que o health check não valida.
- Configurar um deployment canário e explicar por que ele resolve o problema declarado e o blue/green direto não.
- Escopar um alarme de CloudWatch no grupo de destino verde, e explicar por que escopá-lo errado faz o alarme nunca disparar.
- Diferenciar o alarme do CodeDeploy do disjuntor de implantação do L03, incluindo por que eles não coexistem no mesmo serviço.
- Provar, com falha injetada, que a reversão automática limita a exposição a uma fatia e a uma janela.
- Explicar por que `deployment_controller = CODE_DEPLOY` exige `ignore_changes` no Terraform do serviço ECS.
O que a certificação cobra disto
| Conceito | Certificação | Como aparece aqui | O que dominar |
|---|---|---|---|
| Blue/green vs rolling update | DVA-C02, SAP-C02 | duas frotas completas convivendo, contra substituição gradual | blue/green cria a frota nova INTEIRA antes de expor qualquer tráfego |
| Deployment configurations do CodeDeploy | DVA-C02, DOP-C02 | canário 10%/5min escolhido sobre linear e all-at-once | canário = 2 incrementos; linear = incrementos iguais; all-at-once = tudo de uma vez |
| Listener de produção vs listener de teste | DVA-C02, SOA-C02 | o de teste valida a frota verde antes de expor cliente real | os dois moram no mesmo balanceador; só o de produção é obrigatório |
| Restrição de NLB para canário | SAA-C03, DVA-C02 | a decisão de usar ALB em vez de NLB neste desenho | Network Load Balancer só suporta CodeDeployDefault.ECSAllAtOnce |
| Alarme do CodeDeploy vs disjuntor de implantação | DOP-C02, SAP-C02 | dois mecanismos de reversão, para dois tipos de controlador de serviço | deployment_controller CODE_DEPLOY e ECS (rolling) não coexistem no mesmo serviço |
| Hooks do AppSpec para ECS | DVA-C02 | AfterAllowTestTraffic valida a frota verde com tráfego real | a ordem dos cinco hooks, e que só um deles foi usado aqui de propósito |
| Termination wait time da frota azul | DVA-C02, SAP-C02 | janela de retenção depois do deploy bem-sucedido | teto de 2880 minutos; sem padrão documentado, decisão explícita |
| Rollback: reapontar listener vs reconstruir | SAP-C02 | a reversão automática troca uma regra de listener, não reimplanta imagem | por que isso é mais rápido que o rollback por revisão anterior do L03 |
Onde isto costuma ser cobrado errado
A pergunta clássica descreve um serviço com Network Load Balancer configurado com um deployment configuration de canário, e pergunta por que a implantação falha ao ser criada. A resposta não é sobre alarme nem sobre IAM: NLB só suporta CodeDeployDefault.ECSAllAtOnce. Canário e linear exigem Application Load Balancer, porque dependem de regras de listener por peso que o NLB (camada 4) não modela.
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 |
|---|---|---|
| Defeito de concorrência detectado antes de atingir a maioria | exposição inicial ≤ 10%, por janela observável | canário (2 incrementos) em vez de linear ou all-at-once |
| Rollback mais rápido que reconstruir | segundos, não minutos | duas frotas completas convivendo; reversão = trocar listener, não recriar task |
| Validar a frota verde com tráfego real sem expor cliente | obrigatório | listener de teste, com origem restrita por security group ao hook de validação |
| Reversão sem intervenção humana | durante toda a janela de canário | alarme do alvo verde + auto_rollback_configuration com DEPLOYMENT_STOP_ON_ALARM |
| Migration continua aditiva (herdado do L18) | compatibilidade por MAIS tempo que o L03 | a janela de canário (5-15 min) supera em muito a drenagem em segundos do L03 — a restrição fica mais severa, não desaparece |
| Escala atual: 300 lojas, ~90 mil req/dia | pico concentrado em hora cheia | período do alarme em 60 s, para não perder o sinal dentro dos 5 min do primeiro incremento |
| Orçamento: capacidade extra só durante o deploy | sim | termination_wait_time curto (30 min) — a frota azul dobra o custo enquanto existe |
| Rastreável: qual revisão estava em cada fatia de tráfego | auditável | AppSpec registra o ARN exato da task definition; CloudTrail registra quem chamou CreateDeployment |
A restrição do L18 não desaparece — ela fica mais cara de ignorar
No L03, duas versões conviviam por segundos, o tempo da drenagem. Aqui elas convivem pelos 5 a 15 minutos da janela de canário, ou até 48 horas se a frota azul for mantida por retenção longa. Uma migration destrutiva que "quase não dava problema" com a janela curta do L03 tem muito mais tempo para colidir com a versão antiga aqui.
Arquitetura mínima: o que a Cadência já tem, e por que não basta
O L03 já cobriu o rolling update em profundidade — os três relógios, a ordem entre drenagem e SIGTERM, a prova de zero erro de cliente. O desenho abaixo é o mesmo resumido ao que ESTE módulo precisa contrastar: uma frota, um grupo de destino, sem segunda frota nem listener de teste.
- → dispara nova revisão da task definition
- → sobe a nova sem parar a velha
- → reduz conforme a nova fica saudável
- → HTTPS 443, sem saber qual versão vai responder
- → encaminha para qualquer alvo saudável do grupo único
- → ainda recebe, até sair do grupo
- → passa a receber assim que o health check aprova
- Fora da AWS
- Rede e entrega
- Compute
É o desenho do L03, resumido ao que este módulo precisa contrastar: existe UMA frota e UM grupo de destino, e a task nova nasce dentro dele, ao lado da velha, sem uma segunda frota para testar antes. Percorra os passos: nenhum deles impõe um teto de quanto tráfego real a versão nova recebe.
- Um comando, sem orquestrador externo. O próprio ECS decide o ritmo de substituição, dentro do par mínimo/máximo já configurado — não existe uma ferramenta separada decidindo quando avançar, testar ou reverter.
- As duas versões dividem o MESMO grupo de destino. Não há um segundo grupo esperando para receber a versão nova depois de validada — ela entra no grupo que já está recebendo tráfego real, junto com a velha.
- Passou no health check, já é tráfego de cliente de verdade. Não existe listener de teste nem fatia controlada: a partir do momento em que a task nova responde à sondagem do grupo de destino, ela concorre pelo mesmo tráfego que a velha, sem teto de exposição.
- Reverter é reconstruir, não reapontar. Como a frota velha vai encolhendo conforme a nova sobe, não existe "a frota de antes" inteira e à espera. Reverter exige subir a versão anterior de novo — é o gap que o desenho de produção fecha.
- Nenhuma peça aqui impõe um teto de exposição. O disjuntor de implantação e o alarme de 5xx do L03 decidem se o rollout ESTABILIZA — nenhum dos dois limita QUANTO tráfego real a versão nova recebe antes dessa decisão. Com duas tasks e máximo de 200%, a versão nova pode chegar perto de metade do tráfego já no primeiro ciclo. É esta ausência, e não a arquitetura em si, que motiva o desenho de produção.
O que falta no desenho do L03, e é isso que motiva este módulo: nenhuma peça dele limita QUANTO tráfego real chega à versão nova antes de alguém — humano ou alarme — perceber um problema. O disjuntor mede se o rollout estabiliza; o alarme de 5xx mede o alvo; nenhum dos dois impõe um teto de exposição. Assim que os primeiros alvos passam no health check, o excedente do rolling update os expõe, e a proporção cresce rápido — com duas tasks e máximo de 200%, a versão nova pode chegar perto de metade do tráfego já no primeiro ciclo de substituição.
O disjuntor de implantação não existe mais depois deste módulo
Ao mudar o `deployment_controller` do serviço ECS de rolling (padrão) para `CODE_DEPLOY`, o campo `deployment_circuit_breaker` deixa de se aplicar — não é que ele fique desligado, é que a opção não existe para este tipo de controlador. Quem migra para blue/green assumindo que o disjuntor do L03 continua ativo por baixo publica sem NENHUMA rede de segurança automática até configurar o alarme e o `auto_rollback_configuration` deste módulo.
Arquitetura para produção: duas frotas completas, um listener que troca
Cada peça nova abaixo rastreia a uma linha da tabela de requisitos. A diferença estrutural em relação ao L03 não é uma caixa a mais — é a existência de uma SEGUNDA frota completa e de um SEGUNDO grupo de destino, coisa que o rolling update nunca tem.
- → push com tag = SHA do commit
- → CreateDeployment com a revisão (AppSpec)
- → CreateTaskSet: cria a frota verde inteira
- → pull por tag imutável
- → invoca AfterAllowTestTraffic
- → concorrência real pelo listener de TESTE
- → encaminha: teste, depois 10%, depois 100%
- → listener de produção, o que ainda não migrou
- → tráfego de produção, sem interrupção
- → tráfego de teste e depois de canário
- → DEPLOYMENT_STOP_ON_ALARM
- → reversão: listener volta 100% para a azul
- → HTTPS 443
- Fora da AWS
- Compute
- Gestão e governança
- Rede e entrega
A frota verde nasce INTEIRA antes de qualquer cliente vê-la — não é uma task de cada vez, como no L03. O listener de teste prova a verde com tráfego real e sem plateia; o canário limita a exposição pública a uma fatia; e quando o alarme dispara, a reversão é reapontar o listener para a azul, que nunca parou de existir. Percorra os passos: cada um explica uma peça que o rolling update do L03 não tem.
- A frota verde nasce inteira, antes de qualquer clique real. CreateTaskSet sobe o conjunto de tarefas verde ao tamanho TOTAL do serviço, num segundo grupo de destino vazio. Não há "primeira task nova recebendo tráfego assim que passa no health check", como no rolling update do L03 — a frota inteira existe antes de qualquer decisão de expor.
- O listener de teste valida com carga real, sem cliente exposto. O hook `AfterAllowTestTraffic` chama o listener de TESTE — o mesmo balanceador, uma porta diferente — com concorrência simulada. É o ponto que resolve o que o `/health/ready` do L03 nunca resolveu: confirmar não só que o processo responde, mas que ele aguenta carga concorrente sem esgotar o pool do banco.
- O canário limita a exposição a uma fatia, não ao total. Com `CodeDeployDefault.ECSCanary10Percent5Minutes`, o listener de produção desloca 10% do tráfego para a verde e observa por 5 minutos antes de decidir o resto. É a peça que resolve "deploy ruim só aparece com 100% do tráfego dentro" — blue/green direto (all-at-once) não resolveria isto, só a velocidade da reversão.
- A frota azul não é drenada — ela fica de pé como plano B. Ao contrário da task velha do L03, que entra em `DEACTIVATING` assim que a nova é aprovada, a frota azul continua recebendo os 90% (ou o que sobrar) do tráfego durante toda a janela, e segue viva pelo período de retenção depois do deploy — é o que torna o rollback uma troca de rota, não uma reconstrução.
- O alarme mede o alvo VERDE, e dispara a reversão — não um novo deploy. O alarme de `HTTPCode_Target_5XX_Count`, escopado ao grupo de destino verde, aciona `DEPLOYMENT_STOP_ON_ALARM`. O CodeDeploy reage reapontando a regra do listener de produção 100% de volta para o grupo azul. Não há reconstrução de imagem nem revisão de task definition envolvida — é a mesma diferença entre "apontar" e "reconstruir" que o L03 ensinou sobre a tag imutável.
- Por que alguém pula o canário e vai direto para all-at-once. Porque é uma linha a menos de configuração e o deploy termina em segundos, não em cinco minutos. Funciona bem para migração de schema incompatível, onde as duas versões não podem conviver de qualquer jeito — e falha silenciosamente para o problema deste laboratório, porque 100% do tráfego ainda chega de uma vez, só que mais rápido.
O canário não é uma "opção mais cautelosa" do blue/green — é a peça que resolve o problema declarado. Blue/green direto (all-at-once) já resolve velocidade de rollback, porque a reversão continua sendo reapontar o listener; ele não resolve exposição, porque desloca 100% do tráfego de uma vez, só que instantaneamente.
O que muda de fato em relação ao L03
Não é o alarme (o L03 já tinha um) nem a tag imutável (idem). É que agora existem DUAS frotas completas ao mesmo tempo, em DOIS grupos de destino, com um listener de teste no meio. O rollback deixa de ser "aplicar a revisão anterior da task definition" e passa a ser "reapontar uma regra de listener" — mais rápido porque a frota de destino nunca parou de existir.
Como funciona ponta a ponta: a ordem dos cinco hooks
A ordem dos hooks do AppSpec não é jargão — é a prova de quando cada validação pode acontecer, e de quando uma reversão ainda é possível. Start, Install, AllowTestTraffic, AllowTraffic e End não são roteirizáveis: são o CodeDeploy operando ECS e ELB por conta própria.
O payload que confirma uma reversão por alarme, para quem assina notificações do CodeDeploy em vez de olhar o console a cada deploy:
// O que o EventBridge entrega quando o CodeDeploy reverte por alarme. Repare
// que a mensagem nomeia a CAUSA — o alarme, nao "falha generica".
{
"source": "aws.codedeploy",
"detail-type": "CodeDeploy Deployment State-Change Notification",
"resources": ["arn:aws:codedeploy:us-east-1:111122223333:deploymentgroup:ffv-lab-api/ffv-lab-api"],
"detail": {
"deploymentId": "d-ABC123XYZ",
"state": "STOP",
// O motivo distingue de um DEPLOYMENT_FAILURE comum: o alarme do alvo
// verde disparou DURANTE a janela do canario, nao antes nem depois dela.
"deploymentTrigger": "DEPLOYMENT_STOP_ON_ALARM",
"instanceGroupId": null
}
}Confundir o alarme do CodeDeploy com o disjuntor de implantação do L03
São mecanismos de reversão automática, mas de sistemas diferentes por baixo: o disjuntor (`deployment_circuit_breaker`) pertence ao controlador ECS nativo e observa se o rollout estabiliza. O alarme deste módulo pertence à deployment group do CodeDeploy e observa uma métrica do CloudWatch. Um serviço com `deployment_controller = CODE_DEPLOY` não tem acesso ao primeiro — só ao segundo, e só se o `alarm_configuration` e o `auto_rollback_configuration` estiverem os dois habilitados.
As decisões, e o que se perde em cada uma
📋 A Cadência cresceu de 30 para 300 lojas (~90 mil requisições/dia) e publica várias vezes ao dia com o rolling update do L03. Um deploy recente introduziu uma consulta extra por item de pedido (um N+1 disfarçado). O `/health/ready` aprovou a task — uma conexão ao banco é barata — e o disjuntor de implantação não interveio, porque o rollout ESTABILIZOU. O problema só apareceu sob concorrência real de hora de pico, quando o pool de conexões esgotou: por essa altura, o rolling update já tinha deslocado a maior parte do tráfego.
O canário limita o alcance de um defeito de concorrência a uma fatia pequena, por uma janela observável, sem exigir reconstrução nenhuma se algo der errado: a reversão é apontar o listener de produção de volta para uma frota azul que nunca parou de existir. O listener de teste ainda cobre o que o health check do L03 não cobre — carga concorrente real, não uma conexão isolada.
Alt: Blue/green direto (all-at-once) — Resolve velocidade de rollback — a reversão continua sendo reapontar o listener —, mas NÃO resolve o problema declarado: 100% do tráfego ainda chega de uma vez, só que instantaneamente em vez de aos poucos. É a escolha certa quando a migração de schema é incompatível e as duas versões não podem conviver por minuto nenhum — não quando o risco é concorrência.
Alt: Continuar com o rolling update do L03 — Já está em produção e continua correto para a maioria dos deploys. Falha exatamente no cenário deste laboratório: expõe o defeito assim que os primeiros alvos passam no health check, sem teto de exposição.
Alt: Aumentar o rigor do health check para simular carga — Tentador porque não pede recurso novo, mas não reproduz concorrência: um health check síncrono de uma requisição não estressa um pool de conexões do jeito que 60 requisições simultâneas estressam. É configuração tentando resolver um problema de desenho.
Alt: Feature flag por usuário na aplicação — Granularidade mais fina que porcentagem de infraestrutura — dá para liberar por conta específica, não por fatia aleatória —, mas exige mudar a aplicação para ler a flag, e é uma camada acima desta arquitetura, não uma substituta dela.
| Decisão | Escolha | Alternativas | Motivo | O que se perde |
|---|---|---|---|---|
| Estratégia de tráfego | canário 10%/5min | linear 10%/1min; linear 10%/3min; all-at-once; rolling update do L03 | fatia pequena e janela que cobre um platô de tráfego, não só um instante | o deploy leva no mínimo 5-6 min, mesmo quando a versão é boa |
| Listener de teste | presente | omitir e confiar só no health check da task | o hook gera carga concorrente real antes de qualquer cliente ver a versão | mais uma regra de listener/porta e um grupo de segurança para manter |
| Escopo do alarme | alvo do grupo VERDE, especificamente | 5xx do serviço inteiro; 5xx do ALB | sinal misturado com a frota azul saudável nunca aponta para o deploy | um alarme por deployment group, não um fixo reaproveitado de outro módulo |
| Rollback automático | disjuntor equivalente (`auto_rollback_configuration`) + alarme | só `DEPLOYMENT_FAILURE`; só alarme; rollback manual | cobrem falhas diferentes: uma pega frota que não sobe, a outra pega frota saudável respondendo errado | mais um evento e mais um alarme para coordenar e testar |
| Retenção da frota azul | 30 min | 0 (imediato); padrão do provedor; 2880 min (teto) | cobre o tempo humano de notar um problema sutil sem dobrar custo por muito tempo | capacidade dobrada por meia hora a cada deploy, mesmo quando tudo corre bem |
Construir: dois grupos de destino, dois listeners
O grupo azul continua sendo, conceitualmente, o mesmo do L03 — só ganha um nome que reflete que agora é metade de um par. O grupo verde nasce vazio: quem o povoa, a cada deploy, é o CodeDeploy, não o Terraform.
# blue-green.tf — dois grupos de destino completos, e o listener de teste
#
# O grupo AZUL e o mesmo conceito do L03 (aws_lb_target_group.api), agora com
# um segundo nome porque passa a ser metade de um par, nao mais o unico. O
# grupo VERDE nasce vazio: quem o povoa, a cada deploy, e o CodeDeploy.
resource "aws_lb_target_group" "blue" {
name = "${var.projeto}-api-blue"
port = 8080
protocol = "HTTP"
vpc_id = aws_vpc.principal.id
target_type = "ip"
health_check {
path = "/health/ready"
interval = 5
timeout = 4
healthy_threshold = 2
unhealthy_threshold = 2
matcher = "200"
}
# A drenagem continua existindo aqui - e atributo do GRUPO, nao do par
# blue/green. O valor derivado do p99 no L03 nao muda: a pergunta que ele
# responde (quanto esperar a conexao do cliente fechar) e a mesma.
deregistration_delay = 30
}
resource "aws_lb_target_group" "green" {
name = "${var.projeto}-api-green"
port = 8080
protocol = "HTTP"
vpc_id = aws_vpc.principal.id
target_type = "ip"
health_check {
path = "/health/ready"
interval = 5
timeout = 4
healthy_threshold = 2
unhealthy_threshold = 2
matcher = "200"
}
deregistration_delay = 30
}
# Listener de PRODUCAO: obrigatorio no schema, e o unico caminho do cliente
# real. Aponta para o grupo azul ate o primeiro deploy - depois disso, quem
# move a regra e o CodeDeploy, nao mais o Terraform.
resource "aws_lb_listener" "producao" {
load_balancer_arn = aws_lb.principal.arn
port = 443
protocol = "HTTPS"
ssl_policy = "ELBSecurityPolicy-TLS13-1-2-2021-06"
certificate_arn = aws_acm_certificate.api.arn
default_action {
type = "forward"
target_group_arn = aws_lb_target_group.blue.arn
}
}
# Listener de TESTE: opcional no schema da AWS, obrigatorio na pratica deste
# laboratorio - e o unico caminho pelo qual o hook AfterAllowTestTraffic
# alcanca a frota verde pelo MESMO balanceador do cliente, sem expor a frota
# ao publico. Porta separada; origem restrita ao grupo de seguranca do hook.
resource "aws_lb_listener" "teste" {
load_balancer_arn = aws_lb.principal.arn
port = 8443
protocol = "HTTPS"
ssl_policy = "ELBSecurityPolicy-TLS13-1-2-2021-06"
certificate_arn = aws_acm_certificate.api.arn
default_action {
type = "forward"
target_group_arn = aws_lb_target_group.blue.arn # o CodeDeploy troca para green durante o teste
}
}
resource "aws_security_group_rule" "listener_teste_so_da_validacao" {
type = "ingress"
from_port = 8443
to_port = 8443
protocol = "tcp"
security_group_id = aws_security_group.alb.id
source_security_group_id = aws_security_group.hook_validacao.id
description = "so o hook de validacao alcanca o listener de teste; nunca a internet"
}
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
launch_type = "FARGATE"
# A troca central deste laboratorio: quem decide substituir tasks deixa de
# ser o proprio ECS (rolling update do L03) e passa a ser o CodeDeploy. Os
# dois controladores sao mutuamente exclusivos - e e por isso que o
# disjuntor de implantacao do L03 (deployment_circuit_breaker) NAO EXISTE
# como opcao valida neste bloco.
deployment_controller {
type = "CODE_DEPLOY"
}
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.blue.arn # so a associacao INICIAL
container_name = "api"
container_port = 8080
}
# A partir do primeiro deploy pelo CodeDeploy, estes tres campos deixam de
# refletir a realidade no Terraform: o CodeDeploy cria revisoes de task
# definition e troca o grupo de destino por fora do state. Sem
# ignore_changes, o PROXIMO `terraform apply` tenta devolver o servico ao
# que o state ainda acha que e verdade - a guerra entre ferramentas que o
# antipadrao desta secao nomeia.
lifecycle {
ignore_changes = [task_definition, load_balancer, desired_count]
}
}
Sem `ignore_changes`, o Terraform briga com o CodeDeploy
Depois do primeiro deploy pelo CodeDeploy, `task_definition`, `load_balancer` e `desired_count` do serviço ECS deixam de refletir o que o Terraform acha que é verdade — porque o CodeDeploy os modifica por fora do state. Sem `ignore_changes`, o próximo `terraform apply` tenta devolver o serviço à revisão antiga: uma guerra entre as duas ferramentas que parece, à primeira vista, um bug do provedor.
Construir: o CodeDeploy, o alarme e a reversão automática
A aplicação e o grupo de implantação do CodeDeploy, o alarme escopado ao alvo verde, e o papel que a AWS já documenta como menor privilégio para este uso.
# codedeploy.tf — quem orquestra a troca, o alarme e a reversao automatica
resource "aws_codedeploy_app" "api" {
name = "${var.projeto}-api"
compute_platform = "ECS"
}
resource "aws_codedeploy_deployment_group" "api" {
app_name = aws_codedeploy_app.api.name
deployment_group_name = "${var.projeto}-api"
service_role_arn = aws_iam_role.codedeploy.arn
# Canario, nao linear e nao all-at-once: desloca 10% por 5 minutos antes de
# decidir o resto. E o parametro que responde ao problema declarado -
# limita a exposicao a uma fatia pequena, por uma janela observavel. Um
# Network Load Balancer so aceita CodeDeployDefault.ECSAllAtOnce; canario e
# linear exigem Application Load Balancer, que e o que este laboratorio usa.
deployment_config_name = "CodeDeployDefault.ECSCanary10Percent5Minutes"
deployment_style {
deployment_type = "BLUE_GREEN"
deployment_option = "WITH_TRAFFIC_CONTROL"
}
ecs_service {
cluster_name = aws_ecs_cluster.principal.name
service_name = aws_ecs_service.api.name
}
load_balancer_info {
target_group_pair_info {
prod_traffic_route {
listener_arns = [aws_lb_listener.producao.arn]
}
target_group {
name = aws_lb_target_group.blue.name
}
target_group {
name = aws_lb_target_group.green.name
}
test_traffic_route {
listener_arns = [aws_lb_listener.teste.arn]
}
}
}
blue_green_deployment_config {
deployment_ready_option {
action_on_timeout = "CONTINUE_DEPLOYMENT" # nao trava esperando aprovacao manual
}
terminate_blue_instances_on_deployment_success {
action = "TERMINATE"
# Sem padrao documentado pela AWS: declare explicitamente. 30 min cobre
# o tempo humano de perceber um problema sutil que nem o alarme nem o
# canario pegaram, sem manter a frota azul dobrada por muito tempo. O
# teto do campo e 2880 minutos (48 h) - meca o SEU cenario antes de
# copiar este numero.
termination_wait_time_in_minutes = 30
}
}
# Duas reversoes independentes, cobrindo gatilhos diferentes - mesma logica
# de "duas redes de seguranca" do disjuntor + alarme do L03, com um
# mecanismo diferente por baixo:
# DEPLOYMENT_FAILURE -> o proprio CodeDeploy detectou falha (ex.: um
# hook respondeu Failed, ou a frota verde
# nunca ficou saudavel)
# DEPLOYMENT_STOP_ON_ALARM -> um alarme do CloudWatch associado disparou
# DURANTE a janela de teste ou de canario
auto_rollback_configuration {
enabled = true
events = ["DEPLOYMENT_FAILURE", "DEPLOYMENT_STOP_ON_ALARM"]
}
alarm_configuration {
alarms = [aws_cloudwatch_metric_alarm.erro_5xx_do_alvo_verde.alarm_name]
enabled = true
}
}
# Mede o alvo VERDE, especificamente - nao "o servico" e nao o alvo azul, que
# continua saudavel e nao tem relacao com o deploy em curso. Escopar no grupo
# errado e o jeito mais comum de este alarme nunca disparar.
resource "aws_cloudwatch_metric_alarm" "erro_5xx_do_alvo_verde" {
alarm_name = "${var.projeto}-api-5xx-alvo-verde"
namespace = "AWS/ApplicationELB"
metric_name = "HTTPCode_Target_5XX_Count"
statistic = "Sum"
period = 60 # curto de proposito: o primeiro incremento do canario dura so 5 min
evaluation_periods = 2
threshold = 5
comparison_operator = "GreaterThanThreshold"
treat_missing_data = "notBreaching"
dimensions = {
LoadBalancer = aws_lb.principal.arn_suffix
TargetGroup = aws_lb_target_group.green.arn_suffix
}
alarm_actions = [aws_sns_topic.alertas.arn]
}
# O papel do CodeDeploy: descreve alarmes, chama a Lambda dos hooks, e
# modifica ECS e ELB em nome do deploy. Managed policy oficial - nao
# reescrever a mao: ja e o menor privilegio documentado para este uso.
resource "aws_iam_role" "codedeploy" {
name = "${var.projeto}-codedeploy-ecs"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Principal = { Service = "codedeploy.amazonaws.com" }
Action = "sts:AssumeRole"
}]
})
}
resource "aws_iam_role_policy_attachment" "codedeploy_ecs" {
role = aws_iam_role.codedeploy.name
policy_arn = "arn:aws:iam::aws:policy/AWSCodeDeployRoleForECS"
}
Por que ECSCanary e não ECSLinear
Linear desloca em incrementos iguais até o fim — 10% a cada minuto, por exemplo — e não tem um "platô de observação" claro: o tráfego nunca para de crescer enquanto o alarme decide. Canário desloca uma fatia, PARA, e observa por uma janela fixa antes do resto. Para um defeito que só aparece sob concorrência sustentada — não na primeira rajada —, a pausa do canário é o que dá tempo ao alarme de reagir antes de mais tráfego chegar.
Construir: o AppSpec, e o hook que testa concorrência de verdade
O AppSpec é a revisão que o `CreateDeployment` consome — aponta para a task definition nova e declara qual hook validar. O hook, em .NET 8, é o que faz o trabalho que o `/health/ready` do L03 nunca fez: gerar carga concorrente real.
# appspec.yaml.tpl — a revisao que CreateDeployment registra
#
# TaskDefinition e o ARN completo, COM revisao - e o unico lugar que aponta
# para a imagem nova. ContainerName/ContainerPort tem de bater com o
# load_balancer da definicao de servico original. So um hook esta em uso
# aqui, de proposito - os outros quatro (BeforeInstall, AfterInstall,
# BeforeAllowTraffic, AfterAllowTraffic) existem e aceitam Lambda, mas
# adicionar validacao em todos sem necessidade so alonga o deploy sem
# alongar a cobertura: o teste de concorrencia so faz sentido depois que a
# frota verde existe e antes de qualquer trafego de producao chegar nela -
# que e exatamente o AfterAllowTestTraffic.
version: 0.0
Resources:
- TargetService:
Type: AWS::ECS::Service
Properties:
TaskDefinition: "${NOVA_TASK_DEFINITION_ARN}"
LoadBalancerInfo:
ContainerName: "api"
ContainerPort: 8080
Hooks:
- AfterAllowTestTraffic: "ffv-lab-validar-frota-verde"
// ValidarFrotaVerde/Function.cs — hook AfterAllowTestTraffic, em .NET 8
//
// O que o health check do L03 nao pode provar: que a frota aguenta
// CONCORRENCIA real, nao so uma conexao. Este hook dispara requisicoes
// SIMULTANEAS pelo listener de TESTE — o mesmo caminho do balanceador que o
// cliente usa, so que sem cliente nenhum — e so devolve Succeeded se a taxa
// de erro e a latencia ficarem dentro do que a versao anterior ja provou
// suportar. E o achado deste laboratorio: prontidao confirma que o PROCESSO
// responde; isto confirma que ele responde BEM sob carga real.
using Amazon.Lambda.Core;
using Amazon.CodeDeploy;
using Amazon.CodeDeploy.Model;
[assembly: LambdaSerializer(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))]
public class Function
{
// Um socket por requisicao simultanea reproduz concorrencia real; reusar
// uma unica conexao mascararia exatamente o defeito que este hook existe
// para achar (pool de conexao do banco esgotando sob carga).
private static readonly HttpClient _http = new(new SocketsHttpHandler
{
MaxConnectionsPerServer = 50
});
private const string ListenerTeste = "https://interno.ffv-lab.local:8443/api/pedidos/existente";
private const int Concorrencia = 30; // ~metade do pico medido em hora cheia
private const double LimiteErro = 0.02; // 2%: acima disso, a versao anterior nunca chegou
private const int LimiteP99Ms = 800; // 2x o p99 historico de 400 ms medido no L03
public async Task FunctionHandler(HookEvent evt, ILambdaContext ctx)
{
using var codedeploy = new AmazonCodeDeployClient();
var tarefas = Enumerable.Range(0, Concorrencia).Select(_ => MedirUmaRequisicao());
var resultados = await Task.WhenAll(tarefas);
var taxaErro = resultados.Count(r => !r.Ok) / (double)Concorrencia;
var amostrasOrdenadas = resultados.Select(r => r.Ms).OrderBy(x => x).ToArray();
var p99 = Percentil(amostrasOrdenadas, 0.99);
var status = (taxaErro <= LimiteErro && p99 <= LimiteP99Ms)
? LifecycleEventStatus.Succeeded
: LifecycleEventStatus.Failed;
ctx.Logger.LogInformation(
$"validacao da frota verde: erro={taxaErro:P1} p99={p99}ms -> {status}");
// O CodeDeploy so segue (ou reverte) depois desta chamada. Sem ela
// dentro de 1 hora, a implantacao inteira e marcada como falha.
await codedeploy.PutLifecycleEventHookExecutionStatusAsync(
new PutLifecycleEventHookExecutionStatusRequest
{
DeploymentId = evt.DeploymentId,
LifecycleEventHookExecutionId = evt.LifecycleEventHookExecutionId,
Status = status
});
}
private static async Task<(bool Ok, long Ms)> MedirUmaRequisicao()
{
var relogio = System.Diagnostics.Stopwatch.StartNew();
try
{
var resp = await _http.GetAsync(ListenerTeste);
return (resp.IsSuccessStatusCode, relogio.ElapsedMilliseconds);
}
catch
{
return (false, relogio.ElapsedMilliseconds);
}
}
private static long Percentil(long[] amostrasOrdenadas, double p)
{
var indice = (int)Math.Ceiling(p * amostrasOrdenadas.Length) - 1;
return amostrasOrdenadas[Math.Clamp(indice, 0, amostrasOrdenadas.Length - 1)];
}
}
public class HookEvent
{
public string DeploymentId { get; set; } = "";
public string LifecycleEventHookExecutionId { get; set; } = "";
}
O hook tem uma hora para responder
Se `PutLifecycleEventHookExecutionStatus` não for chamado dentro de 1 hora, o CodeDeploy marca a implantação inteira como falha — mesmo que a Lambda ainda esteja rodando. Um `try/finally` que garanta a chamada mesmo em exceção não tratada evita que um bug no hook produza um deploy travado em vez de revertido.
Implantar, e provar que a exposição fica contida
#!/usr/bin/env bash
# implantar-canario.sh — registra a revisao nova e pede ao CodeDeploy para
# desloca-la aos poucos; nao ao ECS para substituir tudo de uma vez.
set -euo pipefail
PROJETO="${PROJETO:?defina PROJETO}"
SHA="$(git describe --always --dirty --abbrev=12)"
[[ "$SHA" == *-dirty ]] && { echo "arvore suja, comite antes" >&2; exit 1; }
# 1. Mesma disciplina do L03: imagem por SHA, registro imutavel.
REPO="$(terraform output -raw repositorio_api)"
docker build --platform linux/amd64 -t "${REPO}:${SHA}" .
docker push "${REPO}:${SHA}"
# 2. Task definition em REVISAO NOVA - nao toca no servico ainda. Quem faz o
# servico apontar para ela e o CodeDeploy, nao um `update-service`.
NOVA=$(aws ecs register-task-definition \
--cli-input-json "$(envsubst < task-definition.json)" \
--query 'taskDefinition.taskDefinitionArn' --output text)
echo "revisao registrada: ${NOVA}"
# 3. O AppSpec e a revisao que o CodeDeploy consome - referencia a task
# definition do passo 2.
NOVA_TASK_DEFINITION_ARN="$NOVA" envsubst < appspec.yaml.tpl > /tmp/appspec.yaml
DEPLOY_ID=$(aws deploy create-deployment \
--application-name "${PROJETO}-api" \
--deployment-group-name "${PROJETO}-api" \
--revision "revisionType=AppSpecContent,appSpecContent={content=$(cat /tmp/appspec.yaml)}" \
--query 'deploymentId' --output text)
echo "$DEPLOY_ID" > /tmp/deploy-id.txt
echo "deployment ${DEPLOY_ID}: canario de 10% por 5 min, depois 100% se o alarme ficar quieto"
aws deploy wait deployment-successful --deployment-id "$DEPLOY_ID"
echo "no ar: ${SHA}"
Cinco provas. A primeira muda em relação ao L03: aqui o critério de aceite não é "zero erro" — é "erro contido a uma fatia pequena, por uma janela curta". O canário existe para limitar exposição, não para eliminar risco de versão ruim.
# provas.sh — cinco medicoes; a primeira e a que MUDA em relacao ao L03: aqui
# o criterio de aceite nao e "zero erro", e "erro contido a uma fatia pequena
# e por uma janela curta" — porque o canario existe para limitar exposicao,
# nao para eliminar risco de deploy ruim.
PROJETO=ffv-lab
TG_VERDE=$(aws elbv2 describe-target-groups --names "${PROJETO}-api-green" \
--query 'TargetGroups[0].TargetGroupArn' --output text)
# ── Prova 1: a exposicao fica contida no percentual do canario ──────────────
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=$!
./quebrar-de-proposito.sh
kill $LACO
# Esperado: uma fatia pequena e finita de erros durante a janela do primeiro
# incremento (nunca mais que ~10-15% no total, mesmo somando 5 minutos), e
# ZERO erro depois da reversao. Erro crescendo sem teto indica que o alarme
# nao disparou ou o auto_rollback_configuration nao inclui DEPLOYMENT_STOP_ON_ALARM.
# ── Prova 2: a frota verde existe inteira antes do primeiro cliente ─────────
aws ecs describe-task-sets --cluster $PROJETO --service ${PROJETO}-api \
--query 'taskSets[].{grupo:targetGroupInfo.targetGroupArn,status:status,estavel:computedDesiredCount}' \
--output table
# Esperado: dois conjuntos de tarefas durante o deploy, os dois com o
# computedDesiredCount total do servico — nao um crescendo aos poucos.
# ── Prova 3: o alarme mede o alvo verde, e so ele ────────────────────────────
aws cloudwatch describe-alarms --alarm-names "${PROJETO}-api-5xx-alvo-verde" \
--query 'MetricAlarms[0].{estado:StateValue,dimensoes:Dimensions}'
# Esperado: StateValue em ALARM durante a falha injetada, com a dimensao
# TargetGroup apontando para o grupo VERDE — se apontar para o azul ou para o
# ALB inteiro, o alarme nunca vai medir o que importa.
# ── Prova 4: a reversao e reapontar o listener, nao reconstruir nada ────────
aws elbv2 describe-listeners --load-balancer-arn "$(terraform output -raw alb_arn)" \
--query "Listeners[?Port==\`443\`].DefaultActions[0].TargetGroupArn" --output text
# Esperado, apos a reversao: o ARN do grupo AZUL. Nenhum build, nenhum push,
# nenhuma nova revisao de task definition foi criada para reverter.
# ── Prova 5: a frota azul continua viva depois da reversao ──────────────────
aws ecs describe-task-sets --cluster $PROJETO --service ${PROJETO}-api \
--query 'taskSets[?status==`PRIMARY`].{status:status,estavel:stabilityStatus}'
# Esperado: PRIMARY com STEADY_STATE. Se a azul tivesse sido derrubada, nao
# haveria para onde reverter — e o rollback deste laboratorio depende dela
# estar de pe.
| Prova | Comando | Resultado que aprova | O que reprova, e o que significa |
|---|---|---|---|
| 1 · Exposição contida | laço de `curl` durante o deploy com falha injetada | fatia pequena e finita de erros, zero depois da reversão | erro crescendo sem teto indica alarme que não disparou ou evento ausente no auto_rollback_configuration |
| 2 · A verde nasce inteira | `describe-task-sets` durante o deploy | dois conjuntos, os dois no tamanho TOTAL do serviço | um conjunto menor que o outro sugere que você está olhando um rolling update, não um blue/green |
| 3 · O alarme mede o alvo certo | `describe-alarms` no alarme do alvo verde | ALARM durante a falha, com dimensão TargetGroup = verde | dimensão apontando para o azul ou para o ALB inteiro explica um alarme que nunca dispara |
| 4 · Reversão é reapontar, não reconstruir | `describe-listeners` no listener de produção | default action aponta para o grupo AZUL depois da reversão | se apontar para o verde, a reversão não ocorreu — confira o auto_rollback_configuration |
| 5 · A azul sobrevive à reversão | `describe-task-sets` filtrando PRIMARY | PRIMARY em STEADY_STATE | azul derrubada = não havia para onde reverter; confira o termination_wait_time |
Quebrar de propósito: reproduzir o incidente real
A falha que motivou este laboratório, reproduzida sob controle: uma variável que força a mesma consulta lenta sob concorrência, sem alterar o health check.
# quebrar-de-proposito.sh — injeta o MESMO defeito do incidente real: uma
# variavel que forca uma consulta lenta sob concorrencia, sem mexer no
# health check (que continua respondendo 200, exatamente como no incidente
# que motivou este laboratorio).
# Ver Program.cs: sob 30+ chamadas simultaneas, cada uma prende o pool por
# 2 s a mais. Uma requisicao isolada nao mostra nada — e por isso o defeito
# passa no health check e so aparece sob carga real.
export SIMULAR_REGRESSAO_CONCORRENCIA=true
PROJETO=ffv-lab ./implantar-canario.sh &
IMPLANTAR_PID=$!
# Esperado: perto dos 5 minutos do primeiro incremento, o alarme de 5xx do
# alvo VERDE dispara, o CodeDeploy para o deslocamento e devolve o listener
# de producao 100% para a azul — sem voce tocar em nada.
sleep 360
aws deploy get-deployment --deployment-id "$(cat /tmp/deploy-id.txt)" \
--query 'deploymentInfo.{estado:status,motivo:errorInformation.message}'
wait $IMPLANTAR_PID || echo "implantacao terminou com reversao — e o resultado esperado deste teste"
| Falha | Como provocar | Sintoma | Onde olhar | Correção |
|---|---|---|---|---|
| Regressão de concorrência (o incidente original) | variável que prende o pool por 2 s extras sob 30+ chamadas simultâneas | health check e disjuntor aprovam; 5xx do alvo verde sobe só sob carga | alarme do alvo verde durante o canário; log do hook de validação | listener de teste com concorrência real pega ANTES de expor cliente |
| Alarme escopado no grupo errado | aponte o `dimensions.TargetGroup` do alarme para o grupo azul | a falha acontece, mas o alarme nunca sai de OK | `describe-alarms` mostra StateValue=OK durante 5xx real no verde | dimensão do alarme tem de ser o ARN do grupo VERDE, não um fixo |
| Evento de rollback ausente na configuração | remova `DEPLOYMENT_STOP_ON_ALARM` de `auto_rollback_configuration.events` | o alarme dispara, mas o deploy segue e conclui com 100% na versão ruim | `get-deployment` mostra status Succeeded apesar do alarme em ALARM | o evento do alarme tem de estar explicitamente na lista, não é implícito |
O que o hook pega, e o que ele não pega
O hook prova capacidade sob concorrência simulada — não substitui o alarme de produção, que mede tráfego real. Uma regressão que só aparece com o VOLUME real de pico (não a concorrência, mas a quantidade sustentada por horas) pode passar no hook e só aparecer depois, durante o canário — é por isso que as duas proteções existem juntas, e nenhuma substitui a outra.
Um serviço ECS foi migrado do rolling update do L03 para `deployment_controller = CODE_DEPLOY`. O time mantém o Terraform como estava, esperando que o disjuntor de implantação (`deployment_circuit_breaker`) continue protegendo contra rollout que não estabiliza. O que realmente acontece?
Segurança: o que muda com uma segunda frota e um listener novo
| Risco | Probabilidade | Impacto | Prevenção | Detecção | Resposta |
|---|---|---|---|---|---|
| Listener de teste exposto sem restrição de origem | baixa | alto | regra de security group restrita ao security group do hook de validação, nunca 0.0.0.0/0 | CloudTrail em `ModifySecurityGroupRules`; varredura periódica de regras do ALB | remover a regra ampla, reduzir ao SG do hook, invalidar credencial se houve acesso |
| Papel de serviço do CodeDeploy com permissão ampla sobre ELB/ECS/Lambda | média | alto | usar o managed policy `AWSCodeDeployRoleForECS`; restringir a trust policy ao princípio `codedeploy.amazonaws.com` | IAM Access Analyzer sobre uso real do papel | revisar quem pode assumir o papel; nunca anexar permissão extra "só por garantia" |
| Hook Lambda que sempre responde Succeeded (falso positivo) | média | alto | revisão de código do hook; teste do hook isolado antes de usá-lo em produção | taxa de reversão automática permanentemente em zero é sinal de alerta, não de sucesso | corrigir a lógica de validação; auditar deploys aprovados no período afetado |
| `alarm_configuration.enabled = false` por engano | média | alto | gate de revisão que confere `enabled = true` no plano do Terraform antes do apply | nenhuma reversão automática registrada apesar de incidentes conhecidos | religar o alarme; auditar deploys feitos com a proteção desligada |
| Frota azul retida além do necessário | baixa | médio | revisar `termination_wait_time_in_minutes` periodicamente, mantendo-o curto | custo de Fargate consistentemente acima do esperado entre deploys | reduzir o tempo de retenção; investigar por que a decisão de reverter demora |
O papel do CodeDeploy precisa de `cloudwatch:DescribeAlarms`
É a permissão que permite ao CodeDeploy CONSULTAR o estado do alarme durante a janela de canário — sem ela, `alarm_configuration` fica declarado mas nunca é verificado. Ela já está incluída no managed policy `AWSCodeDeployRoleForECS`, junto com `lambda:InvokeFunction` (para os hooks) e as ações de ECS e ELB necessárias para criar o conjunto de tarefas e trocar o listener.
Observabilidade: as perguntas que este painel acrescenta ao do L03
| Pergunta | Métrica ou consulta | O que significa mudar | Limiar inicial |
|---|---|---|---|
| A frota verde passou na validação de concorrência? | log do hook AfterAllowTestTraffic | Failed aqui impede qualquer tráfego real de chegar à verde | qualquer Failed |
| O canário atingiu o limiar de erro? | `HTTPCode_Target_5XX_Count` do alvo VERDE | defeito que passou no hook mas aparece sob volume real | > 5 em 2 períodos de 1 min |
| A reversão automática disparou? | evento `DeploymentStateChangeNotification` com `deploymentTrigger` | distingue reversão por alarme de reversão por falha simples do CodeDeploy | qualquer DEPLOYMENT_STOP_ON_ALARM |
| A frota azul ainda está de pé? | `describe-task-sets` → status PRIMARY | ausência dela significa que não há para onde reverter | sempre respondível durante a janela de retenção |
| Quanto do tráfego já migrou? | peso da regra do listener de produção | acompanha o progresso do canário/linear em tempo real | informativo |
| Alguém disparou um deploy fora do pipeline? | CloudTrail em `CreateDeployment` | deploy manual sem passar pelas mesmas checagens | qualquer identidade fora da esperada |
O alarme do alvo azul não serve para nada durante o canário
A frota azul continua saudável e recebendo tráfego normal — monitorá-la durante o deploy só confirma o óbvio. O painel de deploy tem uma função estreita, herdada do L03: dizer se ESTE rollout deve continuar. Isso exige olhar o alvo verde, sempre.
Escala: 10, 10 mil, 1 milhão
| Volume | O que acontece com o canário | O que passa a doer | O que fazer |
|---|---|---|---|
| 10 req/s, 2 tasks por frota | canário de 10% é ~1 requisição a cada poucos segundos | amostra pequena demais para o alarme distinguir sinal de ruído | considerar all-at-once com termination wait time maior, se o risco de concorrência for baixo |
| 500 req/s, 8 tasks por frota | canário de 10% já é uma amostra estatisticamente útil | a frota verde inteira (8 tasks) convive com a azul — 16 tasks por alguns minutos | confirmar cota de tasks da conta e endereços de sub-rede para o dobro da frota |
| 5 mil req/s, 60 tasks por frota | 10% ainda são centenas de requisições — sinal forte | 120 tasks simultâneas exigem sub-rede dimensionada e cota revisada | dimensionar sub-rede pelo DOBRO da frota, igual ao gargalo já visto no L03 |
| Pico durante o deploy | o canário compete com a escala automática do serviço | a frota verde nasce no tamanho ATUAL, que pode já estar desatualizado no fim da janela | travar a escala automática durante o deploy, ou aceitar reconciliar depois |
| Falha de AZ durante a janela de canário | a frota que perde capacidade pode ser a azul OU a verde | com duas frotas pequenas, uma falha de AZ pode deixar as duas abaixo do mínimo saudável ao mesmo tempo | dimensionar cada frota para sobreviver a uma AZ, não só o par combinado |
Escolher NLB por latência trava a estratégia de tráfego mais tarde
A decisão de balanceador costuma ser tomada pensando em protocolo e latência, não em deploy — mas escolher Network Load Balancer aqui fecha a porta para canário e linear de uma vez: só `CodeDeployDefault.ECSAllAtOnce` fica disponível. Se exposição limitada for requisito, o balanceador tem de ser decidido com o deploy em mente, não só com o tráfego em regime permanente.
Custo: o que este laboratório acrescenta ao L03
Diferente do excedente breve do L03, aqui a frota INTEIRA dobra por toda a janela de canário mais o tempo de retenção — é o preço estrutural de ter as duas frotas completas convivendo.
| Cenário | Volume | O que acrescenta | Tendência | Otimização |
|---|---|---|---|---|
| Protótipo | 1 deploy/semana, 2 tasks por frota | a frota inteira dobrada por ~35 min (5 do canário + 30 de retenção) | baixa, mas visível — bem mais que o excedente parcial do L03 | termination_wait_time curto; considerar all-at-once em ambiente sem risco real |
| Produção pequena | 5 deploys/dia, 2 tasks por frota | até 5 × 35 min de capacidade dobrada por dia | linha visível na fatura de Fargate | reduzir a retenção se o histórico de reversão manual for raro |
| Alta escala | 20 deploys/dia, 60 tasks por frota | 60 tasks extras por até 35 min, vinte vezes ao dia | custo comparável ao das próprias tasks de regime permanente | encurtar a janela de canário para incrementos que decidam mais rápido (linear 1min em vez de canário 5min), se o histórico permitir |
A frota azul retida não é "de graça"
Ela dobra o custo de compute pela janela inteira, e um `termination_wait_time` longo esquecido é o Elastic IP órfão do L01 de novo: paga-se por estar ligado, não por ser usado. Em alta escala, a diferença entre 30 e 300 minutos de retenção multiplicada por vinte deploys ao dia é a maior linha de custo que este laboratório introduz — e a mais fácil de esquecer configurada.
Well-Architected nos seis pilares
| Pilar | Situação ao fim deste laboratório | Risco que fica | Melhoria | Prioridade |
|---|---|---|---|---|
| Excelência operacional | deploy canário com validação de concorrência e reversão automática | o `CreateDeployment` ainda é disparado por script, não por pipeline federado | mover para pipeline com credencial OIDC (L54) | alta |
| Segurança | listener de teste restrito por SG; papel do CodeDeploy com managed policy oficial | hook Lambda com permissão de rede para alcançar o listener de teste é superfície nova | revisar a política da role de execução do hook periodicamente | média |
| Confiabilidade | exposição de defeito de concorrência contida a uma fatia e uma janela | a validação de concorrência é sintética; volume real de pico pode revelar algo que o hook não simula | ampliar o cenário do hook com base em incidentes reais medidos | alta |
| Eficiência de performance | defeito de pool detectado antes de 100% do tráfego | a causa raiz (N+1) continua existindo até ser corrigida no código | revisão de consultas por item de pedido; considerar carregamento em lote | média |
| Otimização de custos | termination wait time curto; frota extra só durante o deploy | frota inteira dobrada custa mais que o excedente parcial do L03 | medir se linear com incrementos maiores atinge o mesmo resultado por menos tempo | média |
| Sustentabilidade | janela de capacidade dobrada é finita e configurável | cada deploy consome o dobro de recursos por minutos, mesmo quando a versão é boa | revisar a necessidade de canário em serviços de baixo risco, onde all-at-once basta | baixa |
Evolução em níveis: o que muda, e o que passa a doer
A terceira arquitetura não é um desenho novo: é a resposta a QUANDO trocar de estratégia de deploy — e este laboratório é só um degrau da escada que o L03 já começou.
Deploy manual, uma task, `:latest`. Legítimo só em desenvolvimento.Tag imutável, excedente, disjuntor de implantação, alarme de 5xx do alvo.Duas frotas completas, listener de teste, canário de 10%, alarme escopado no alvo verde.`CreateDeployment` sai da máquina de uma pessoa; credencial OIDC, aprovação para produção.A aplicação lê uma flag e decide, por conta ou por atributo, quem vê a versão nova — independente de qual fatia de infraestrutura está no ar.Histórico de deploys (diff, arquivos tocados, incidentes anteriores) alimenta uma decisão de qual estratégia usar — canário longo para mudança de risco alto, all-at-once para mudança trivial.A ordem não é negociável, e o motivo é concreto
Canário graduado por dado (nível 6) depende de ter histórico de canários reais para aprender com eles — que depende de já estar no nível 3. Quem tenta pular direto para "IA decide a estratégia de deploy" sem ter passado pelo canário manual está treinando um modelo sem dado nenhum para treinar.
Onde IA entra nesta arquitetura, e onde não entra
Neste módulo, como no L03, o problema central tem resposta determinística: exposição limitada por percentual e janela, validação por concorrência simulada, reversão por regra de listener. Nenhuma das três é decisão que um modelo melhore.
O nível 6 da escada anterior aponta onde IA acrescentaria valor real, e ele continua sendo modesto: GRADUAR o risco de uma mudança para escolher a estratégia — canário longo para uma mudança que mexe em consulta ao banco, all-at-once para uma mudança de texto de tela. Hoje toda mudança usa a mesma estratégia, e correção de string custa os mesmos 5-6 minutos de canário que uma migração de schema.
| Pergunta | Resposta honesta para este módulo |
|---|---|
| Qual problema a IA resolveria? | graduar a estratégia de deploy pelo risco real da mudança, em vez de aplicar canário uniforme a tudo |
| Por que uma regra não bastaria? | uma regra cobre a maior parte: "tocou consulta ou schema → canário; só front-end → all-at-once". IA só se justifica quando a regra simples mostrar seu limite |
| De onde viriam os dados? | CloudTrail, eventos de deployment do CodeDeploy, métricas do CloudWatch por deploy, histórico de reversões — tudo já existe depois deste laboratório |
| Qual o risco? | aprender de poucos incidentes e liberar all-at-once uma mudança que precisava de canário; exige avaliação com dado retido e um caminho manual sempre disponível |
| Por que não agora? | a Cadência tem dezenas de deploys de histórico, não centenas — modelo sobre poucos exemplos é superstição com aparência de estatística, o mesmo limite que o L03 já registrou |
O uso de IA que parece atraente e é armadilha aqui
Pedir a um modelo para "olhar o log do hook e decidir se libera" troca um limiar determinístico — taxa de erro e p99, medidos e comparados a um número — por um julgamento probabilístico sobre o MESMO dado. Onde já existe limiar claro, IA só acrescenta latência e a chance de aprovar com confiança algo que devia reprovar.
Anti-padrões deste laboratório
| Anti-padrão | Por que alguém faz | Por que é problema | Sintoma em produção | Forma correta |
|---|---|---|---|---|
| Blue/green sem listener de teste | menos uma porta, uma regra de segurança e um recurso para manter | nada valida a frota verde com tráfego real antes do canário começar — o defeito do incidente original chegaria a clientes de qualquer jeito | o mesmo tipo de regressão de concorrência que motivou este laboratório reaparece | listener de teste com hook de validação, restrito por security group |
| Alarme genérico do serviço em vez de escopado no alvo verde | parece mais simples reaproveitar o alarme do L03 | o sinal se mistura com a frota azul saudável, e o alarme praticamente nunca dispara durante um canário real | reversões automáticas quase inexistentes, mesmo com incidentes conhecidos | um alarme por deployment group, com dimensão TargetGroup apontando para o verde |
| `termination_wait_time_in_minutes = 0` | "por que manter algo que já passou no canário?" | elimina a rede de segurança humana — um problema sutil percebido minutos depois do deploy concluído não tem mais para onde reverter | incidente detectado tarde vira reconstrução, não reapontamento | retenção curta, mas não-zero (15-30 min é um ponto de partida razoável, a validar com o seu tempo médio de detecção) |
| Confundir o alarme do CodeDeploy com o disjuntor de implantação do L03 | os dois "revertem sozinho", parecem intercambiáveis | um serviço `CODE_DEPLOY` não tem o disjuntor como opção — supor que ele existe deixa o serviço sem proteção nenhuma até o alarme ser configurado | primeiro deploy ruim vai a 100% sem nenhuma reversão automática | configurar `alarm_configuration` e `auto_rollback_configuration` explicitamente; não presumir herança do L03 |
| Terraform sem `ignore_changes` no serviço ECS | ninguém percebeu que o CodeDeploy modifica o serviço por fora do state | `terraform apply` de rotina tenta devolver o serviço à revisão de task definition antiga do state | deploy "revertido" sozinho por um apply que não tinha nada a ver com o CodeDeploy | `lifecycle { ignore_changes = [task_definition, load_balancer, desired_count] }` |
| Usar o hook de validação só para checar `/health/ready` | reaproveitar o que já existe é menos código | não testa NADA que o health check da task já não testasse — o defeito de concorrência continua invisível | o mesmo incidente original se repete, mesmo com toda a infraestrutura de blue/green no lugar | o hook precisa gerar concorrência real, não reconsultar a mesma rota que o health check já consulta |
Se só corrigir um destes seis, corrija o escopo do alarme
Um alarme apontado para o grupo de destino errado é o único item desta lista que faz TODOS os outros parecerem estar funcionando — o canário desloca, a janela observa, e nada reverte, porque o sinal que decidiria a reversão nunca chega. É também o mais fácil de testar isoladamente: confira a dimensão `TargetGroup` do alarme antes de confiar em qualquer prova de rollback automático.
Quando algo não funciona
| Sintoma | Causa provável | Como investigar | Onde olhar | Correção |
|---|---|---|---|---|
| `CreateDeployment` falha com erro de papel | trust policy do papel do CodeDeploy não permite `codedeploy.amazonaws.com` | confira o `assume_role_policy` do papel de serviço | IAM → papel do CodeDeploy → Trust relationships | corrigir o `Principal` para `codedeploy.amazonaws.com` |
| Deployment travado em `AllowTestTraffic` | o hook nunca chama `PutLifecycleEventHookExecutionStatus` | confira o log da Lambda e se ela lança exceção antes de notificar | CloudWatch Logs da função do hook | garantir a chamada em `finally`, mesmo em falha de validação |
| Falha injetada não dispara o alarme | alarme escopado no grupo de destino errado, ou `enabled = false` | compare a dimensão `TargetGroup` do alarme com o ARN do grupo verde | `describe-alarms` → `Dimensions` | apontar a dimensão para o grupo verde; confirmar `alarm_configuration.enabled = true` |
| Alarme dispara, mas o deploy conclui em 100% mesmo assim | `DEPLOYMENT_STOP_ON_ALARM` ausente de `auto_rollback_configuration.events` | confira a lista de eventos configurada na deployment group | Terraform state ou console → deployment group → configuração de rollback | incluir `DEPLOYMENT_STOP_ON_ALARM` explicitamente na lista |
| `terraform apply` de rotina reverte um deploy recente | ausência de `ignore_changes` no serviço ECS | compare o `task_definition` no state com o realmente ativo no serviço | diff do plano do Terraform antes de aplicar | adicionar `lifecycle { ignore_changes = [...] }` no `aws_ecs_service` |
| NLB configurado com canário nunca aceita a deployment group | Network Load Balancer só suporta `CodeDeployDefault.ECSAllAtOnce` | confira o tipo do load balancer associado | documentação do CodeDeploy sobre considerações de blue/green para ECS | trocar para Application Load Balancer, ou aceitar all-at-once com o NLB |
| Rollback ocorre, mas a frota azul já não existe | `termination_wait_time_in_minutes` baixo demais ou o deploy anterior já a removeu | confira `describe-task-sets` para status PRIMARY antes de confiar em rollback automático | histórico de deployments do CodeDeploy | aumentar a retenção; para incidente detectado tarde, o caminho vira reconstrução, não reversão |
A pergunta que resolve metade destes casos
Antes de mexer em parâmetro, pergunte: o hook validou de verdade, ou só confirmou que a rota responde? A maioria dos deploys "canário" que não pegam regressão real tem um hook que reconsulta o mesmo health check da task — sem concorrência, sem carga, sem diferença nenhuma do que o L03 já fazia.
Limpeza: o que o destroy não leva, além do L03
Este laboratório acrescenta poucos recursos ao L03, mas dois deles cobram por hora mesmo vazios — o segundo grupo de destino e o listener de teste.
# limpar.sh — o que este laboratorio acrescenta ao L03, e o que sobrevive
# 1. Deployment group e app do CodeDeploy — nao cobram por hora, mas ficam
# orfaos se voce apagar o servico ECS primeiro.
aws deploy delete-deployment-group \
--application-name ffv-lab-api --deployment-group-name ffv-lab-api
aws deploy delete-application --application-name ffv-lab-api
# 2. Segundo grupo de destino, e o listener de teste — cobram por hora como
# qualquer grupo/listener do L01, mesmo vazios.
terraform destroy -target=aws_lb_listener.teste \
-target=aws_lb_target_group.green \
-auto-approve
# 3. O alarme do alvo verde nao desaparece sozinho quando o grupo de destino
# e apagado — ele so fica "sem dado", o que NAO e a mesma coisa que apagado.
aws cloudwatch delete-alarms --alarm-names ffv-lab-api-5xx-alvo-verde
# 4. Revisoes de task definition acumuladas pelos deploys do CodeDeploy —
# nao cobram, mas poluem a lista tanto quanto no L03.
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. Resto do Terraform (blue.tf, codedeploy.tf) e o que o L03/L01 ja
# cobravam — ALB, RDS, NAT, Elastic IP. Rode a limpeza deles tambem se nao
# for seguir para o L54.
terraform destroy -auto-approve
| Recurso | Sai no destroy? | Cobra parado? | Por que fica |
|---|---|---|---|
| Deployment group e app do CodeDeploy | sim | não | fica órfão se o serviço ECS for apagado primeiro |
| Grupo de destino verde | sim, se em Terraform | sim, por hora | idêntico ao grupo azul do L01 — cobra existindo, mesmo vazio |
| Listener de teste | sim, se em Terraform | sim, por hora | regra a mais no mesmo ALB do L01 |
| Alarme do alvo verde | depende | centavos | não desaparece sozinho quando o grupo de destino é apagado — fica "sem dado" |
| Revisões de task definition dos deploys do CodeDeploy | não | não | acumulam a cada deploy, como no L03 |
| O que já vinha do L01/L03 | depende | sim, por hora | ALB original, RDS, NAT, Elastic IP — rode a limpeza deles também |
Resumo: problema, peça e motivo
| Problema | Peça | Por que ela, e não outra |
|---|---|---|
| Defeito de concorrência não visto no health check | listener de teste + hook com carga simulada | prontidão testa uma conexão; o hook testa dezenas simultâneas, o que o health check nunca fez |
| Deploy ruim exposto a 100% do tráfego | canário 10%/5min, não all-at-once | blue/green direto resolve velocidade de rollback, não exposição — só o incremento pequeno resolve exposição |
| Alarme que nunca dispara | escopo explícito no grupo de destino VERDE | sinal misturado com a frota azul saudável não aponta para o deploy em curso |
| Rollback lento | duas frotas completas convivendo | reversão é reapontar o listener, não reconstruir imagem |
| Terraform brigando com o CodeDeploy | `ignore_changes` no serviço ECS | o CodeDeploy modifica task_definition/load_balancer/desired_count por fora do state |
| Problema sutil percebido depois do deploy "concluído" | termination wait time da frota azul | é a rede de segurança humana, entre o alarme automático e a reconstrução manual |
- O deploy é disparado com `CreateDeployment`, referenciando um AppSpec com a task definition nova.
- O CodeDeploy cria a frota verde no tamanho TOTAL, num segundo grupo de destino, antes de qualquer tráfego real.
- O listener de teste é associado à verde, e o hook AfterAllowTestTraffic gera concorrência real contra ela.
- Se o hook aprova, o listener de produção começa a deslocar 10% do tráfego para a verde.
- Por 5 minutos, o alarme do alvo verde observa; a frota azul continua recebendo o resto, intacta.
- Se o alarme dispara, o CodeDeploy reaponta o listener de produção 100% de volta para a azul.
- Se o alarme fica quieto, o restante do tráfego migra para a verde.
- A frota azul é mantida pelo termination wait time, e só então é removida.
Perguntas frequentes
❓ O disjuntor de implantação do L03 também funciona em serviços blue/green do CodeDeploy?
❓ Blue/green resolve sozinho um defeito que só aparece com tráfego real?
❓ Preciso mesmo de um listener de teste, ou o health check da task já basta?
❓ Quanto tempo a frota azul fica de pé depois de um deploy bem-sucedido?
❓ Canário do CodeDeploy funciona com Network Load Balancer?
❓ O que acontece se o hook Lambda nunca chamar de volta o CodeDeploy?
❓ Blue/green muda como eu atualizo o serviço ECS pelo Terraform?
❓ Qual é a diferença prática entre canário e linear no CodeDeploy?
Fixando
Um hook `AfterAllowTestTraffic` chama apenas a rota `/health/ready` da frota verde, com uma única requisição, e sempre aprova. Uma regressão que só aparece sob 30 chamadas simultâneas ao banco chega a produção sem ser detectada. Qual é o erro de raciocínio no desenho deste hook?
Durante a janela do canário, o alarme do alvo verde dispara `ALARM`, e minutos depois `describe-listeners` mostra a regra de produção apontando 100% para o grupo de destino AZUL de novo, sem nenhum build ou push novo ter acontecido. O que aconteceu, exatamente?
Conhecimentos, próximo módulo e documentação
| Item | Conteúdo |
|---|---|
| Conhecimentos anteriores necessários | L03 no ar (tag imutável, disjuntor, rolling update) e L18 (migration aditiva); noção de listener e grupo de destino de ALB |
| Conhecimentos adquiridos | diferença estrutural entre rolling update e blue/green; canário como resposta a "defeito só aparece com 100% do tráfego"; listener de teste; alarme do CodeDeploy vs disjuntor do L03; termination wait time da frota azul |
| Limitação que fica | a validação de concorrência do hook é sintética — volume real de pico sustentado pode revelar algo que a simulação não reproduz; o `CreateDeployment` ainda sai de um script, não de um pipeline |
| Próximo exemplo recomendado | L54 — pipeline com credencial federada (OIDC). Reutiliza o AppSpec e a deployment group deste módulo, e é onde o `CreateDeployment` deixa de sair da máquina de uma pessoa |
| Também habilitado por este módulo | módulos futuros de liberação por feature flag e de canário graduado por dado de risco dependem da deployment group e do alarme construídos aqui |
| Data da última validação técnica | 7 de agosto de 2026 |
Documentação oficial consultada: CodeDeploy blue/green deployments for Amazon ECS — os dois grupos de destino, o listener de teste opcional e as restrições de NLB; Working with deployment configurations in CodeDeploy — os percentuais exatos de canário, linear e all-at-once; AppSpec 'hooks' section for an Amazon ECS deployment — a ordem dos cinco hooks e o que cada um pode reprovar; e BlueInstanceTerminationOption (API Reference) — o teto de 2880 minutos para a retenção da frota azul. 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
A AWS não documenta um valor padrão para `termination_wait_time_in_minutes` — os 30 minutos usados aqui são uma escolha explícita da Cadência, não uma referência oficial; derive o seu do tempo real que sua equipe leva para perceber um problema sutil. Os limites de concorrência e latência do hook (30 chamadas, 2% de erro, 800 ms de p99) são medidos na aplicação de exemplo e servem como ordem de grandeza — meça o pico real do seu serviço antes de copiá-los. E a restrição de NLB para `ECSAllAtOnce` está confirmada na documentação oficial consultada; comportamentos de versão de console podem mudar, então confira a página antes de desenhar em torno dela.
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…