Lab 34 — EKS quando ECS não basta: o que muda de verdade
O problema, e a empresa que o tem
A Cadência tem sete serviços em produção, todos em ECS Fargate — o de Notificações, extraído no L31, é um deles. O novo VP de Engenharia veio de uma empresa onde tudo rodava em Kubernetes, e a primeira frase dele numa reunião de arquitetura foi "vamos de Kubernetes". Nenhum requisito acompanhou a frase.
Na mesma semana, o parceiro de detecção de fraude que a Cadência acabou de contratar — a Aegis — informa que o produto é distribuído apenas como um Helm chart com um Operator próprio, que observa um recurso customizado (`AegisPolicy`) para aplicar política de risco por pedido. Não existe pacote para ECS. Essa é a única frase da semana com um requisito dentro.
O papel deste laboratório não é decidir entre ECS e EKS de forma abstrata: é pegar UM serviço que já existe nos dois mundos possíveis — o mesmo binário, os dois orquestradores — e medir, com número, o que muda. A pergunta que a Cadência precisa responder para o VP não é "Kubernetes é bom?". É "o que ele custa em operação, e onde esse custo se paga?".
O que este laboratório NÃO é
Não é um laboratório de Kubernetes do zero — não ensina `kubectl`, conceitos de pod ou o modelo de objetos do Kubernetes como currículo geral. O que ele ensina é a fronteira exata onde o EKS acrescenta operação sobre o que o ECS já resolvia, usando um serviço real como régua. Se você nunca viu um manifesto Kubernetes, os comentários do YAML cobrem o necessário para acompanhar — mas não substituem um curso de Kubernetes.
O que você vai conseguir fazer
Objetivos verificáveis: cada um se prova com um comando ou uma tabela preenchida na seção de implantação, não com a sensação de ter entendido.
- Nomear os três motivos legítimos que justificam EKS sobre ECS, e testar o caso da Cadência contra os três.
- Explicar por que o papel de execução do Fargate Pod NÃO concede permissão ao contêiner, e o que concede.
- Implantar o mesmo serviço em ECS Fargate e em EKS com perfil Fargate, a partir da mesma imagem.
- Configurar IRSA para uma ServiceAccount e provar, com CloudTrail, que a credencial foi assumida por federação OIDC.
- Instalar o AWS Load Balancer Controller e explicar por que ele é uma peça a mais que o ECS não tem.
- Diagnosticar uma falha de IRSA silenciosa e uma falha de agendamento por perfil Fargate mal configurado.
- Medir a taxa fixa do control plane do EKS e decidir se ela se paga para o seu caso.
- Recomendar, com dado medido, se a Cadência deve migrar tudo, nada, ou só a fatia que o parceiro Aegis exige.
O que a certificação cobra disto
| Conceito | Certificação | Como aparece aqui | O que dominar |
|---|---|---|---|
| ECS vs. EKS: quando cada um se justifica | SAP-C02, DOP-C02 | a tabela de requisitos e a decision box | os três motivos legítimos — portabilidade real, ecossistema que o time já usa, profundidade de equipe — e por que "é o padrão do mercado" não é um deles |
| Taxa do control plane do EKS | SAP-C02 | custo fixo por cluster-hora, cobrado independente de pods | que ele existe mesmo com o cluster vazio, e que suporte estendido custa mais |
| Papel de execução do Fargate Pod | DVA-C02, SAP-C02 | a Identidade 1 do desenho EKS | que ele autoriza só kubelet, pull de imagem e log — nunca a aplicação |
| IAM roles for service accounts (IRSA) | SAP-C02, SOA-C02 | a Identidade 2, ligada por OIDC a uma ServiceAccount | o papel do provedor OIDC e da condição `sub` no trust policy |
| RBAC do Kubernetes | SAP-C02 | camada de permissão dentro do cluster, separada do IAM | que ela responde "quem no cluster pode o quê", não "quem na AWS pode o quê" |
| Add-ons gerenciados (VPC CNI, CoreDNS) | SAP-C02, DOP-C02 | citados como custo operacional oculto | que versão de add-on incompatível com a versão do cluster quebra sem erro claro |
| AWS Load Balancer Controller | DVA-C02, SAP-C02 | observa Ingress/Service e provisiona ALB | que ele é um POD, com sua própria IRSA, e não parte do control plane gerenciado |
Onde isto costuma ser cobrado errado
A pergunta clássica dá um pod Running que não consegue chamar S3 ou SQS, com o papel de execução do Fargate Pod aparentemente correto, e pede a causa. Quem responde "faltou anexar a política ao papel de execução" erra o alvo: esse papel nunca é herdado pelo contêiner, então anexar qualquer política nele não muda nada para a aplicação. A resposta é IRSA — uma role separada, ligada à ServiceAccount.
Requisitos, e como cada um muda o desenho
Requisito sem coluna de desenho é intenção, não decisão. Repare que só um dos seis abaixo é o que de fato obriga Kubernetes — os outros cinco governam COMO o piloto é conduzido, não SE ele acontece.
| Requisito | Valor declarado | O que ele decide no desenho |
|---|---|---|
| Distribuição do parceiro Aegis | somente Helm chart + Operator (CRD `AegisPolicy`) | é o único requisito que obriga ALGUM cluster Kubernetes a existir — nenhum outro serviço da Cadência tem requisito equivalente |
| Profundidade de Kubernetes do time | 2 de 7 engenheiros usaram em projeto pessoal | decide que o piloto tem de ser um serviço pequeno e reversível, não a plataforma inteira |
| Orçamento do piloto | uma pessoa-mês para provar ou descartar | decide o escopo: comparar UM serviço nos dois desenhos, não migrar sete |
| SLA de Notificações | 99,9%, inalterado pelo piloto | decide que o EKS roda EM PARALELO ao ECS durante a medição — nunca substitui antes de provar |
| Nenhuma chave estática | obrigatório nos dois desenhos | decide que o EKS usa IRSA para tudo; nenhuma credencial de longa duração em variável de ambiente ou Secret |
| Custo operacional mensurável | número, não opinião | decide que o piloto registra horas de engenharia gastas em operação — não só a fatura da AWS |
O requisito que mais se confunde com preferência
"O time gosta de Kubernetes" e "o time já opera Kubernetes com profundidade que gera produtividade real" são frases parecidas e decisões opostas. A segunda é um dos três motivos legítimos da decision box adiante; a primeira não é motivo nenhum — é gosto, e gosto não aparece em nenhuma linha desta tabela.
Arquitetura mínima: o serviço em ECS Fargate
Este é o desenho que a Cadência já tem, desde o L31. Ele entra aqui como a linha de base porque o resto do laboratório o compara, peça por peça, com o desenho seguinte — e a defeito nenhum: continua sendo uma arquitetura legítima e implantável.
- → poll em long-polling
- → HTTPS administrativo
- → encaminha
- → health check + /admin
- → pull da imagem
- → concede sqs+ses, uma credencial só
- → agenda e substitui
- → SendEmail
- → logs e métricas
- Integração de apps
- Fora da AWS
- Compute
- Rede e entrega
- Segurança e identidade
- Gestão e governança
É o mesmo serviço extraído no L31, sem nenhuma peça nova. Percorra os passos contando quantos objetos você precisa entender para explicar este desenho a alguém: essa contagem é o número que o desenho seguinte vai dobrar.
- A fila e o parceiro de e-mail já existem; só a orquestração é nova aqui. Este laboratório não mexe no L31 nem no L33: a fila continua entregando o mesmo evento, e o SES continua sendo o efeito colateral. O que este passo isola é a pergunta que o resto do módulo responde — o que roda o contêiner, e o que isso custa em operação.
- Uma única identidade, injetada pelo metadata do Fargate. O papel de tarefa é a única credencial AWS que o contêiner tem, e ele a recebe automaticamente pelo endpoint de metadados do Fargate — sem cofre, sem variável de ambiente. Duas ações, um papel: guarde este número, porque o desenho seguinte dobra ele.
- O agendador do ECS decide onde a task roda — e é só isso. O serviço ECS é a peça de orquestração inteira: agenda a task, substitui a que morre, e não exige mais nada do operador para isso acontecer. Não há plano de controle separado para gerenciar, nem add-on para atualizar.
- A rota administrativa é o único tráfego HTTP que existe. O serviço não atende cliente final: a única audiência humana é o time de suporte, reenviando um e-mail perdido. O ALB existe por causa dessa rota, e só dela.
- O grupo de destino decide quem recebe esse tráfego. Mesmo com um volume pequeno de chamadas administrativas, o grupo de destino continua sendo quem verifica saúde e encaminha — a mesma peça que o L03 ensinou, sem nenhuma variação neste desenho.
- O efeito observável: e-mail enviado, log gravado. Do ponto de vista de quem recebe o e-mail, nada neste laboratório muda nada. É a frase que vale guardar até o próximo desenho: a diferença inteira entre os dois é operacional, não funcional.
Conte as peças antes de continuar
Um serviço, uma task definition, um grupo de destino, um papel de tarefa. Quatro objetos de orquestração e identidade para explicar o desenho inteiro a alguém novo. Guarde esse número — a seção seguinte não troca nenhum deles por outro; ela ACRESCENTA.
Arquitetura para produção: o mesmo serviço em EKS
Cada peça nova abaixo existe por um motivo nomeado nos passos do diagrama — se você não conseguir apontar o motivo, ela é adorno, e este desenho não pretende ter nenhum. A topologia é estruturalmente diferente da anterior: não é "ECS mais um nó", é uma segunda camada inteira de operação por cima da mesma capacidade de computação.
- → poll em long-polling
- → HTTPS administrativo
- → roteia por IP do pod (target-type ip)
- → cria e atualiza via Ingress + Service
- → agenda no perfil Fargate
- → agenda como pod do sistema
- → agenda no mesmo perfil Fargate
- → só pull de imagem e log — não chega no contêiner
- → concede sqs+ses via ServiceAccount
- → concede permissão de ELB via ServiceAccount própria
- → pull da imagem
- → SendEmail
- → aplica política de fraude (CRD observado)
- Integração de apps
- Fora da AWS
- Compute
- Rede e entrega
- Segurança e identidade
- Gestão e governança
Conte de novo os objetos que alguém precisa entender para explicar este desenho. A capacidade de computação é a mesma do anterior (Fargate). O que multiplicou foi a operação — e cada peça nova existe por um motivo específico, não por hábito.
- O cluster cobra antes do primeiro pod existir. A taxa do control plane é por cluster-hora, e ela corre independente de quantos pods rodam — mesmo com o cluster vazio. É o primeiro custo deste desenho que não tem equivalente no ECS, onde o plano de controle não é faturado à parte.
- Duas identidades onde o ECS tinha uma. O papel de execução do Fargate Pod só autoriza o kubelet a registrar o pod e a puxar imagem e log — o contêiner NUNCA assume essa permissão. A permissão que o app de fato usa vem de uma role separada, ligada por OIDC a uma ServiceAccount específica: é IRSA. Confundir os dois papéis é a armadilha central deste laboratório.
- O ALB nasce de um controlador, não do Terraform. No desenho ECS, o `aws_lb` e o `load_balancer {}` do serviço criam o balanceador de forma declarativa e direta. Aqui, um Ingress e um Service descrevem a INTENÇÃO, e um pod que fica rodando dentro do cluster — o AWS Load Balancer Controller — observa essa intenção e chama a API da AWS para provisionar o ALB. É uma peça a mais que pode ficar para trás, travar ou perder permissão sem que o Terraform veja nada de errado.
- O controller também precisa da própria identidade. Ele chama `elasticloadbalancing:CreateLoadBalancer` e ações vizinhas em nome do cluster inteiro, então também recebe uma role por IRSA — com o alcance errado, ela cria ou apaga balanceador de qualquer serviço na conta, não só do seu.
- O Operator existe porque o parceiro só distribui assim. É o motivo real, e não hipotético, deste cluster existir: o Aegis publica um CRD (`AegisPolicy`) e um Operator que o observa. Não há Helm chart nem Operator para ECS — o modelo de distribuição do parceiro pressupõe a API do Kubernetes.
- RBAC é uma segunda camada de permissão, por cima do IAM. IRSA resolve QUEM o contêiner é para a AWS. Continua existindo uma pergunta separada: quem, dentro do cluster, pode ler ou alterar aquela ServiceAccount e o Secret do token OIDC associado a ela. Essa pergunta é respondida pelo RBAC do Kubernetes, e ele não tem equivalente no ECS — lá, IAM já é a única resposta.
- O mesmo e-mail sai no fim, pelas duas topologias. Apesar de toda a máquina nova, o comportamento observável por quem recebe o e-mail é idêntico ao do desenho anterior. O custo deste desenho é inteiramente operacional — é essa a distinção que a seção de custo mede com número, não com opinião.
O erro que produz AccessDenied silencioso
Dar ao papel de execução do Fargate Pod a permissão de `sqs:ReceiveMessage` ou `ses:SendEmail`, achando que isso resolve o acesso do contêiner, não resolve nada — e não gera erro nenhum na hora de aplicar. O pod sobe, fica Running, passa no health check. Só quando o código tenta de fato falar com a AWS é que o SDK devolve `AccessDenied`, porque a credencial que ele resolveu não é a do papel de execução: é a ausência de credencial nenhuma, já que a ServiceAccount não estava vinculada a nada.
O caminho de uma notificação, ponta a ponta
O código da aplicação não sabe qual dos dois desenhos o hospeda, e é exatamente essa a prova de que a diferença entre eles é operacional. O que muda são as três primeiras etapas — quem decide onde o contêiner roda, e com qual identidade ele nasce.
O evento que a fila entrega, herdado do L31/L33. Nenhum campo muda entre os dois orquestradores — e e por isso que o payload nao aparece nos diagramas deste modulo: ele nao e o objeto de estudo aqui.
{
"source": "cadencia.pedidos",
"detail-type": "PedidoPago",
"detail": {
"pedidoId": "b7e4b8b0-1c2d-4e3f-9a1b-8f7c6d5e4a3b",
"tentativa": 1,
"ocorridoEm": "2026-08-07T13:04:11Z"
}
}Por que o SDK não sabe onde está rodando
A cadeia padrão de resolução de credencial do SDK da AWS consulta, nesta ordem, variáveis de ambiente, depois o arquivo de configuração, depois o endpoint de metadados do ambiente. No Fargate do ECS, é o metadados que responde. No EKS, o webhook de identidade de pod injeta `AWS_ROLE_ARN` e `AWS_WEB_IDENTITY_TOKEN_FILE` como variáveis de ambiente ANTES de o SDK ser inicializado — então a mesma cadeia padrão encontra a resposta num lugar diferente, sem que o código precise saber disso.
As decisões, e o que se perde em cada uma
📋 A Cadência opera sete serviços em ECS Fargate em produção. O novo VP de Engenharia, vindo de uma empresa que usava Kubernetes para tudo, pede "vamos de Kubernetes" sem apontar um requisito específico. Ao mesmo tempo, o parceiro de detecção de fraude Aegis anuncia que só distribui o produto como Helm chart com um Operator.
O requisito real está isolado numa única peça: o Operator do Aegis. Nenhum dos outros seis serviços tem um motivo escrito para sair do ECS — nem portabilidade multi-nuvem genuína, nem dependência de um ecossistema de Helm charts que o time já usa, nem profundidade de equipe em Kubernetes que devolveria produtividade (dois de sete engenheiros usaram Kubernetes só em projeto pessoal). Abrir um cluster pequeno resolve o bloqueio real — o parceiro — sem pagar a segunda camada de operação (RBAC, add-ons, IRSA, upgrade de versão) nos seis serviços que não precisam dela. O piloto medido neste laboratório é exatamente essa peça isolada.
Alt: Migrar a plataforma inteira para EKS — Multiplica RBAC, add-ons e upgrade de versão por sete serviços que não têm nenhum requisito que o ECS não atenda. O ganho de "padronizar em Kubernetes" é organizacional, não técnico — e não aparece na tabela de requisitos deste módulo.
Alt: Recusar Kubernetes e negociar outro fornecedor de detecção de fraude — É uma saída legítima, mas troca um problema técnico por um de contrato e migração de dados de fraude acumulados — fora do escopo deste laboratório, e não necessariamente mais barato.
Alt: Kubernetes autogerenciado em EC2, sem o plano de controle gerenciado da AWS — Evita a taxa fixa do control plane e adiciona exatamente o oposto do que se quer: agora o próprio plano de controle também é operado pelo time. É mais operação, não menos.
| Decisão | Escolha | Alternativas | Motivo | O que se perde |
|---|---|---|---|---|
| Escopo do piloto | um serviço, em paralelo ao ECS existente | migração completa da plataforma; nenhum piloto, decisão só por opinião | mede custo operacional real com risco reversível | não testa efeito de múltiplos times no mesmo cluster — isso é nível 3 da evolução |
| Identidade do contêiner | IRSA por ServiceAccount | permissão no papel de execução do pod (não funciona); credencial estática num Secret | é a única forma que concede permissão de negócio ao contêiner sem chave de longa duração | uma role IAM a mais para gerenciar por serviço, contra uma única task role no ECS |
| Exposição do control plane | endpoint privado, sem acesso público | endpoint público com lista de IP liberado; público sem restrição | a API do Kubernetes administra o cluster inteiro — expô-la é o equivalente a expor o plano de controle do ECS, que a AWS nunca expõe | acesso à API exige estar na VPC ou atrás de VPN/bastion |
| Provisionamento do ALB | AWS Load Balancer Controller via Ingress | Terraform criando o ALB direto, como no ECS; NLB pelo Service type LoadBalancer | é o padrão do ecossistema Kubernetes para este caso, e mantém a definição do balanceador junto do manifesto da aplicação | uma peça a mais rodando dentro do cluster, com sua própria IRSA e seu próprio log |
| Escopo do namespace | um namespace só, `notificacoes` | compartilhar o namespace `default`; um namespace por ambiente | isola o RBAC deste serviço do resto do cluster desde o primeiro dia | nenhuma; é o mínimo aceitável mesmo num piloto de um serviço só |
| Suporte da versão do cluster | padrão, com upgrade planejado | suporte estendido, aceitando a taxa maior; deixar vencer sem plano | a taxa padrão é a mais barata das duas, e o upgrade é a alternativa real a ela | exige janela de upgrade periódica — é hora de engenharia, não fatura |
A dívida que este piloto cria, e que este módulo não paga
Rodar dois desenhos do mesmo serviço em paralelo por um mês significa duas filas de log, dois alarmes de 5xx, duas superfícies para o time de suporte confundir. Isso é aceitável para um piloto de escopo pequeno e prazo definido — não é um estado estável. Ao final da medição, um dos dois desenhos é desligado; manter os dois é o próprio antipadrão que a seção de anti-padrões nomeia adiante.
Construir: o binário que não muda entre os dois desenhos
Esta é a prova mais direta de que a diferença é de operação, não de aplicação: o mesmo `Program.cs`, a mesma imagem de contêiner, rodam sem alteração nos dois ambientes. O que muda inteiramente está do lado de fora do binário.
// Program.cs — o mesmo binário nos dois desenhos, sem um `#if` sequer
var builder = WebApplication.CreateBuilder(args);
// A cadeia de credencial padrão do SDK da AWS resolve para os dois casos sem
// nenhuma configuração explícita: no ECS ela lê o endpoint de metadados do
// Fargate; no EKS ela lê AWS_ROLE_ARN e AWS_WEB_IDENTITY_TOKEN_FILE, que o
// webhook de identidade de pod injeta quando a ServiceAccount está anotada.
// É por isso que este arquivo não muda entre os dois desenhos.
builder.Services.AddDefaultAWSOptions(builder.Configuration.GetAWSOptions());
builder.Services.AddAWSService<IAmazonSQS>();
builder.Services.AddAWSService<IAmazonSimpleEmailServiceV2>();
builder.Services.AddHostedService<ConsumidorDeFila>();
builder.Services.AddHealthChecks()
.AddCheck("self", () => HealthCheckResult.Healthy(), tags: ["live"])
.AddCheck<FilaAcessivelCheck>("fila", tags: ["ready"]); // valida sqs:GetQueueAttributes
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") });
// Única rota administrativa que existe. O time de suporte é a única audiência
// humana deste serviço nos dois desenhos.
app.MapPost("/admin/reenviar/{pedidoId:guid}", async (Guid pedidoId, IAmazonSimpleEmailServiceV2 ses, AppDb db) =>
{
var pedido = await db.Pedidos.FindAsync(pedidoId);
if (pedido is null) return Results.NotFound();
await EnviarConfirmacao.Executar(ses, pedido);
return Results.Accepted();
});
app.Run();
O teste que prova isso, antes de qualquer Terraform
Rode a mesma imagem localmente com Docker, apontando `AWS_ROLE_ARN` para uma role assumível via `sts:AssumeRole` comum (sem OIDC) e compare com rodá-la em Fargate puro. Se o comportamento diverge, a divergência está em configuração de ambiente, nunca no código — e é isso que orienta onde procurar quando algo falha só num dos dois desenhos.
Construir: a infraestrutura ECS — a linha de base
Quatro objetos: papel de tarefa, task definition, serviço, grupo de destino. É o mesmo padrão do L03, aplicado ao serviço de Notificações.
# ecs.tf — a linha de base: papel de tarefa, grupo de destino, serviço
# Uma identidade só, e ela é explícita: duas ações, dois recursos, nenhum "*"
# que não se justifique.
data "aws_iam_policy_document" "notificacoes_task" {
statement {
sid = "ConsumirFila"
effect = "Allow"
actions = ["sqs:ReceiveMessage", "sqs:DeleteMessage", "sqs:GetQueueAttributes"]
resources = [aws_sqs_queue.pedidos_pagos.arn] # a fila do L31/L33, por ARN
}
statement {
sid = "EnviarConfirmacao"
effect = "Allow"
actions = ["ses:SendEmail", "ses:SendRawEmail"]
resources = [aws_sesv2_email_identity.dominio.arn] # restrito à identidade verificada
}
}
resource "aws_iam_role" "notificacoes_task" {
name = "${var.projeto}-notificacoes-task"
assume_role_policy = data.aws_iam_policy_document.confianca_ecs_tasks.json
}
resource "aws_iam_role_policy" "notificacoes_task" {
role = aws_iam_role.notificacoes_task.id
policy = data.aws_iam_policy_document.notificacoes_task.json
}
resource "aws_lb_target_group" "notificacoes_admin" {
name = "${var.projeto}-notif-admin"
port = 8080
protocol = "HTTP"
vpc_id = aws_vpc.principal.id
target_type = "ip"
health_check {
path = "/health/ready"
interval = 10
timeout = 5
healthy_threshold = 2
unhealthy_threshold = 2
}
}
resource "aws_ecs_task_definition" "notificacoes" {
family = "${var.projeto}-notificacoes"
requires_compatibilities = ["FARGATE"]
network_mode = "awsvpc"
cpu = 256
memory = 512
execution_role_arn = aws_iam_role.execucao.arn # ECR + logs — mesmo papel do L03
task_role_arn = aws_iam_role.notificacoes_task.arn
container_definitions = jsonencode([{
name = "notificacoes"
image = "${aws_ecr_repository.notificacoes.repository_url}:${var.sha_da_imagem}"
essential = true
portMappings = [{ containerPort = 8080, protocol = "tcp" }]
environment = [
{ name = "FILA_URL", value = aws_sqs_queue.pedidos_pagos.url },
]
logConfiguration = {
logDriver = "awslogs"
options = {
"awslogs-group" = aws_cloudwatch_log_group.notificacoes.name
"awslogs-region" = var.regiao
"awslogs-stream-prefix" = "notificacoes"
}
}
}])
}
resource "aws_ecs_service" "notificacoes" {
name = "${var.projeto}-notificacoes"
cluster = aws_ecs_cluster.principal.id
task_definition = aws_ecs_task_definition.notificacoes.arn
desired_count = 2
launch_type = "FARGATE"
network_configuration {
subnets = aws_subnet.privada[*].id
security_groups = [aws_security_group.notificacoes.id]
}
load_balancer {
target_group_arn = aws_lb_target_group.notificacoes_admin.arn
container_name = "notificacoes"
container_port = 8080
}
}
# O objeto inteiro de orquestração deste desenho: um serviço, uma task
# definition, um grupo de destino, um papel de tarefa. Conte com a seção
# "Implantar, e provar" — é o número 1 da segunda prova.
Por que ses:SendEmail não precisa de `*`
Diferente do `ecr:GetAuthorizationToken` do L03, que é operação de conta e não aceita recurso, `ses:SendEmail` aceita o ARN da identidade verificada como recurso. Restringir a ele é possível e é o que a política faz — não há justificativa para `*` aqui.
Construir: o cluster EKS — onde a segunda camada de operação começa
O mesmo resultado final pede um objeto de cluster, um perfil Fargate, um provedor OIDC e TRÊS papéis IAM novos — contra um só no desenho ECS. Cada role tem um comentário explicando por que ela existe e o que ela NÃO faz.
# eks.tf — o cluster, o perfil Fargate, e as DUAS identidades novas
resource "aws_iam_role" "cluster" {
name = "${var.projeto}-eks-cluster"
assume_role_policy = data.aws_iam_policy_document.confianca_eks.json
}
resource "aws_iam_role_policy_attachment" "cluster" {
role = aws_iam_role.cluster.name
policy_arn = "arn:aws:iam::aws:policy/AmazonEKSClusterPolicy"
}
resource "aws_eks_cluster" "principal" {
name = "${var.projeto}-eks"
role_arn = aws_iam_role.cluster.arn
vpc_config {
subnet_ids = concat(aws_subnet.privada[*].id, aws_subnet.publica[*].id)
endpoint_public_access = false # o painel de administração acessa via VPN/bastion
endpoint_private_access = true
}
# Cobra por hora a partir daqui, ANTES de existir um perfil Fargate ou um
# pod. Suporte padrão e suporte estendido têm taxas diferentes — confira o
# valor atual no Pricing Calculator; o que não muda é que a cobrança é por
# cluster-hora, não por uso.
}
# ── Identidade 1: o papel de execução do Fargate Pod ─────────────────────────
# Só deixa o kubelet se registrar no cluster e puxar imagem/log. O contêiner
# NUNCA herda isto — é a distinção que a seção de construção do manifesto
# reforça, porque é onde mais gente erra.
data "aws_iam_policy_document" "confianca_fargate_pod" {
statement {
effect = "Allow"
actions = ["sts:AssumeRole"]
principals {
type = "Service"
identifiers = ["eks-fargate-pods.amazonaws.com"]
}
condition {
test = "ArnLike"
variable = "aws:SourceArn"
values = ["arn:aws:eks:${var.regiao}:${var.conta}:fargateprofile/${var.projeto}-eks/*"]
}
}
}
resource "aws_iam_role" "fargate_pod_execution" {
name = "${var.projeto}-eks-fargate-pod-execution"
assume_role_policy = data.aws_iam_policy_document.confianca_fargate_pod.json
}
resource "aws_iam_role_policy_attachment" "fargate_pod_execution" {
role = aws_iam_role.fargate_pod_execution.name
policy_arn = "arn:aws:iam::aws:policy/AmazonEKSFargatePodExecutionRolePolicy"
}
resource "aws_eks_fargate_profile" "notificacoes" {
cluster_name = aws_eks_cluster.principal.name
fargate_profile_name = "notificacoes"
pod_execution_role_arn = aws_iam_role.fargate_pod_execution.arn
subnet_ids = aws_subnet.privada[*].id
# Seletor por namespace + rótulo. Pod fora deste par não é agendado no
# Fargate — fica Pending, e é a Falha 3 da seção de quebrar de propósito.
selector {
namespace = "notificacoes"
}
}
# ── OIDC: a ponte que faz uma ServiceAccount virar identidade AWS ───────────
data "tls_certificate" "oidc" {
url = aws_eks_cluster.principal.identity[0].oidc[0].issuer
}
resource "aws_iam_openid_connect_provider" "cluster" {
client_id_list = ["sts.amazonaws.com"]
thumbprint_list = [data.tls_certificate.oidc.certificates[0].sha1_fingerprint]
url = aws_eks_cluster.principal.identity[0].oidc[0].issuer
}
# ── Identidade 2: IRSA — a permissão que o contêiner de fato usa ────────────
# Mesmas duas ações do papel de tarefa do ECS. A DIFERENÇA não é o que ela
# autoriza — é a QUEM ela confia: aqui é uma ServiceAccount específica,
# amarrada pelo `sub` do token OIDC, não uma task inteira.
data "aws_iam_policy_document" "confianca_irsa_notificacoes" {
statement {
effect = "Allow"
actions = ["sts:AssumeRoleWithWebIdentity"]
principals {
type = "Federated"
identifiers = [aws_iam_openid_connect_provider.cluster.arn]
}
condition {
test = "StringEquals"
variable = "${replace(aws_iam_openid_connect_provider.cluster.url, "https://", "")}:sub"
values = ["system:serviceaccount:notificacoes:notificacoes-sa"]
}
}
}
resource "aws_iam_role" "irsa_notificacoes" {
name = "${var.projeto}-irsa-notificacoes"
assume_role_policy = data.aws_iam_policy_document.confianca_irsa_notificacoes.json
}
resource "aws_iam_role_policy" "irsa_notificacoes" {
role = aws_iam_role.irsa_notificacoes.id
policy = data.aws_iam_policy_document.notificacoes_task.json # mesma policy do ECS, reaproveitada
}
# ── Identidade 3: IRSA do próprio Load Balancer Controller ──────────────────
# Ele age em nome do CLUSTER inteiro — por isso a política dele é sobre
# elasticloadbalancing, não sobre sqs/ses. Ver a seção de manifestos para a
# versão completa e a ressalva sobre a política oficial.
data "aws_iam_policy_document" "confianca_irsa_lbcontroller" {
statement {
effect = "Allow"
actions = ["sts:AssumeRoleWithWebIdentity"]
principals {
type = "Federated"
identifiers = [aws_iam_openid_connect_provider.cluster.arn]
}
condition {
test = "StringEquals"
variable = "${replace(aws_iam_openid_connect_provider.cluster.url, "https://", "")}:sub"
values = ["system:serviceaccount:kube-system:aws-load-balancer-controller"]
}
}
}
resource "aws_iam_role" "irsa_lbcontroller" {
name = "${var.projeto}-irsa-lb-controller"
assume_role_policy = data.aws_iam_policy_document.confianca_irsa_lbcontroller.json
}
# Conte os recursos IAM deste arquivo contra os dois do ecs.tf: é a Prova 2
# da seção de implantação.
| Identidade | Quem a assume | O que ela concede | Existe no ECS? |
|---|---|---|---|
| Papel do cluster | o serviço EKS em si | gerenciar recursos do control plane em nome da conta | não — é equivalente ao papel que a própria AWS usa para operar o ECS, e lá não é seu |
| Papel de execução do Fargate Pod | o kubelet, ao registrar o pod | pull de imagem do ECR e escrita de log | sim — é o equivalente direto do papel de EXECUÇÃO do ECS |
| IRSA de Notificações | a ServiceAccount `notificacoes-sa` | sqs:ReceiveMessage/DeleteMessage, ses:SendEmail | sim — é o equivalente direto do papel de TAREFA do ECS |
| IRSA do Load Balancer Controller | a ServiceAccount `aws-load-balancer-controller` | criar/atualizar ALB em nome do cluster inteiro | não — o ECS não tem um controlador de balanceador rodando como carga de trabalho |
A role que, com o escopo errado, vira o maior risco deste desenho
A IRSA do Load Balancer Controller age em nome do CLUSTER inteiro, não de um serviço. Com uma condição de confiança larga demais no OIDC — por exemplo, sem restringir o `sub` a uma ServiceAccount específica — qualquer pod no cluster capaz de ler aquele token pode assumi-la e criar, alterar ou apagar balanceador de QUALQUER carga na conta, não só de Notificações. É o equivalente, em impacto, a uma credencial de administrador de rede vazada — e é por isso que ele aparece de novo na seção de segurança.
Construir: os manifestos Kubernetes e o Load Balancer Controller
Nada aqui declara recurso da AWS diretamente — os manifestos descrevem INTENÇÃO, e quem transforma intenção em recurso é o controller instalado a seguir. É a inversão central deste desenho em relação ao Terraform do ECS.
# notificacoes.yaml — os manifestos Kubernetes; o binário não muda, só a moldura
apiVersion: v1
kind: Namespace
metadata:
name: notificacoes # tem de bater com o `selector.namespace` do perfil Fargate
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: notificacoes-sa
namespace: notificacoes
annotations:
# É esta linha que liga a ServiceAccount à role IRSA. Sem ela, o pod sobe
# com a mesma identidade fraca de qualquer outro no namespace, e a
# aplicação recebe AccessDenied de todas as chamadas de negócio.
eks.amazonaws.com/role-arn: arn:aws:iam::111122223333:role/ffv-lab-irsa-notificacoes
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: notificacoes
namespace: notificacoes
spec:
replicas: 2
selector:
matchLabels: { app: notificacoes }
template:
metadata:
labels: { app: notificacoes }
spec:
serviceAccountName: notificacoes-sa # sem isto, IRSA não se aplica ao pod
containers:
- name: notificacoes
image: 111122223333.dkr.ecr.us-east-1.amazonaws.com/ffv-lab-notificacoes:SHA_DA_IMAGEM
ports: [{ containerPort: 8080 }]
env:
- name: FILA_URL
value: https://sqs.us-east-1.amazonaws.com/111122223333/ffv-lab-pedidos-pagos
readinessProbe:
httpGet: { path: /health/ready, port: 8080 }
periodSeconds: 5
livenessProbe:
httpGet: { path: /health/live, port: 8080 }
periodSeconds: 10
---
apiVersion: v1
kind: Service
metadata:
name: notificacoes
namespace: notificacoes
spec:
type: ClusterIP # o ALB não fala com o pod direto; fala com este Service
selector: { app: notificacoes }
ports: [{ port: 80, targetPort: 8080 }]
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: notificacoes-admin
namespace: notificacoes
annotations:
kubernetes.io/ingress.class: alb
alb.ingress.kubernetes.io/scheme: internal # equivalente ao ALB interno do desenho ECS
alb.ingress.kubernetes.io/target-type: ip # aponta direto ao IP do pod, não a um nó
alb.ingress.kubernetes.io/healthcheck-path: /health/ready
spec:
rules:
- http:
paths:
- path: /admin
pathType: Prefix
backend:
service: { name: notificacoes, port: { number: 80 } }
A anotação que, esquecida, não gera erro nenhum
Se `eks.amazonaws.com/role-arn` estiver ausente ou com o ARN errado na ServiceAccount, o `kubectl apply` é aceito, o Deployment sobe, os pods ficam `Running`. Nada no caminho de implantação verifica se a anotação aponta para uma role que existe ou que confia na ServiceAccount certa — a única forma de descobrir é a aplicação tentar chamar a AWS e falhar, ou a Prova 3 da seção de implantação, feita de propósito.
# instalar-lb-controller.sh — a peça que o ECS não tem, instalada uma vez por cluster
helm repo add eks https://aws.github.io/eks-charts
helm repo update
helm install aws-load-balancer-controller eks/aws-load-balancer-controller \
--namespace kube-system \
--set clusterName="${PROJETO}-eks" \
--set serviceAccount.create=false \
--set serviceAccount.name=aws-load-balancer-controller
# A ServiceAccount `aws-load-balancer-controller` já existe (criada à parte,
# com a mesma anotação de IRSA do manifesto de Notificações) e aponta para a
# role `irsa_lbcontroller` do Terraform. A política ANEXADA a essa role é a
# oficial do projeto — ela é longa (dezenas de ações de elasticloadbalancing,
# ec2, acm, wafv2, shield) e não está reproduzida aqui por completo: use o
# `iam_policy.json` publicado no repositório do AWS Load Balancer Controller,
# porque uma versão resumida daria à role menos — ou mais — do que ela precisa.
Por que a política do controller não está reproduzida por inteiro aqui
A política oficial cobre dezenas de ações em `elasticloadbalancing`, `ec2`, `acm`, `wafv2` e `shield`, porque o controller pode gerenciar ALB, NLB, grupo de segurança, certificado e WAF a partir de anotações do Ingress. Reproduzir uma versão resumida aqui daria à role menos permissão do que ela precisa — quebrando o controller — ou mais do que o mínimo necessário, sem que o motivo apareça em nenhum comentário. Use o `iam_policy.json` publicado no repositório oficial do projeto.
Implantar, e provar o que cada desenho custa de verdade
Cinco provas. As quatro primeiras confirmam que cada peça faz o que os comentários dizem; a quinta é o motivo de este laboratório existir — nenhuma opinião sobre "Kubernetes ser mais difícil" substitui um número medido.
# provas.sh — cinco medições; a quinta é a que este laboratório existe para fazer
PROJETO=ffv-lab; REGIAO=us-east-1
# ── Prova 1: tempo até o cluster ficar ACTIVE ─────────────────────────────────
# NAO verificado na documentacao — e a medicao do ambiente de exemplo, e serve
# so como ordem de grandeza. Meca no seu ambiente: o tempo varia por regiao e
# por versao do Kubernetes.
inicio=$(date +%s)
terraform apply -target=aws_eks_cluster.principal -auto-approve
aws eks wait cluster-active --name "${PROJETO}-eks" --region "$REGIAO"
echo "cluster ACTIVE em $(( $(date +%s) - inicio )) s"
# Na Cadencia: 11 min 42 s. O servico ECS equivalente ficou pronto em segundos.
# ── Prova 2: quantos objetos IAM para o MESMO resultado ──────────────────────
echo "-- ECS --"; grep -c 'resource "aws_iam_role"' ecs.tf
echo "-- EKS --"; grep -c 'resource "aws_iam_role"' eks.tf
# Esperado: 1 no ECS (o papel de tarefa; o de execucao ja existe desde o L03).
# 4 no EKS (cluster, execucao do pod, IRSA de Notificacoes, IRSA do controller).
# ── Prova 3: IRSA está de fato injetando credencial no pod ───────────────────
kubectl exec -n notificacoes deploy/notificacoes -- env | grep AWS_ROLE_ARN
# Esperado: a variavel aparece, apontando para a role irsa_notificacoes. Se
# vier vazia, a ServiceAccount nao esta anotada ou o Deployment nao a referencia.
aws cloudtrail lookup-events --lookup-attributes AttributeKey=EventName,AttributeValue=AssumeRoleWithWebIdentity \
--query 'Events[0].EventName' --output text
# Esperado: AssumeRoleWithWebIdentity aparece no CloudTrail apos o pod subir.
# ── Prova 4: o ALB nasceu do controller, nao do Terraform ────────────────────
kubectl get ingress -n notificacoes notificacoes-admin -w
# Esperado: a coluna ADDRESS comeca vazia e preenche com um DNS de ALB em ate
# alguns minutos. Se ficar vazia por muito mais tempo, o controller nao tem
# permissao ou nao encontrou sub-rede com a tag kubernetes.io/role/elb.
# ── Prova 5: a que este laboratorio existe para fazer — horas de operacao ────
# NAO verificado na documentacao — e o numero que a Cadencia mediu, registrado
# em planilha de horas durante o piloto de um mes. Registre o seu.
echo "ECS: 40 min do zero ao servico saudavel (mesma pessoa, mesma tarde)"
echo "EKS: 3 h 10 min para o mesmo resultado, incluindo a primeira vez"
echo " debugando IRSA e a primeira vez configurando o LB Controller"
| Prova | Comando | Resultado que aprova | O que reprova, e o que significa |
|---|---|---|---|
| 1 · Tempo até o cluster ficar pronto | `aws eks wait cluster-active` | minutos, não segundos — é o padrão do EKS | se travar muito além disso, confira cota de ENI da conta na sub-rede do cluster |
| 2 · Contagem de objetos IAM | `grep` nos dois arquivos `.tf` | 1 role nova no ECS contra 4 no EKS para o mesmo resultado | número diferente do esperado indica que uma identidade foi reaproveitada indevidamente |
| 3 · IRSA injetando credencial | `kubectl exec ... env` + CloudTrail | `AWS_ROLE_ARN` presente no pod, e `AssumeRoleWithWebIdentity` no CloudTrail | variável ausente é ServiceAccount sem anotação; evento ausente é trust policy com `sub` errado |
| 4 · O ALB nasceu do controller | `kubectl get ingress -w` | coluna ADDRESS preenche em minutos | ADDRESS vazio por muito tempo é IRSA do controller sem permissão ou sub-rede sem tag de ELB |
| 5 · Horas de operação, medidas | planilha de horas durante o piloto | o número existe e foi registrado — não a sensação de qual foi mais difícil | sem registro, a decisão final vira opinião de novo, e o piloto não serviu para nada |
O que a Prova 5 mostrou na Cadência, e a ressalva sobre ela
Quarenta minutos no ECS contra três horas e dez no EKS, para o mesmo resultado — na primeira vez. Essa diferença cai MUITO na segunda e na terceira vez, porque grande parte das três horas foi curva de aprendizado de IRSA e do Load Balancer Controller, não trabalho repetível. Isso não é NÃO verificado na documentação: é a medição do ambiente de exemplo, e o número certo para decisão é o seu, medido no seu piloto.
Quebrar de propósito: três falhas e o diagnóstico
As três só existem no desenho EKS — é outra forma de medir a segunda camada de operação: são três formas novas de quebrar que o desenho ECS simplesmente não tem.
| Falha | Como provocar | Sintoma | Onde olhar | Correção |
|---|---|---|---|---|
| IRSA mal configurado | remova a anotação `eks.amazonaws.com/role-arn` da ServiceAccount, ou aponte para uma role cujo trust policy tem o `sub` errado | pod fica Running, health check passa, mas nenhum e-mail sai — e nenhum log de erro óbvio aparece, porque a aplicação trata a falha de envio como retry silencioso | CloudTrail sem eventos de `AssumeRoleWithWebIdentity`; `kubectl exec ... env` sem `AWS_ROLE_ARN` | corrigir a anotação e o `sub` do trust policy; reiniciar o pod para reler o token |
| Load Balancer Controller sem permissão | remova uma ação de `elasticloadbalancing:CreateLoadBalancer` da política da role dele | o Ingress fica com ADDRESS vazio indefinidamente; ninguém consegue chamar a rota administrativa | `kubectl logs -n kube-system deploy/aws-load-balancer-controller` mostra erro de permissão a cada tentativa de reconciliação | restaurar a ação na política; o controller tenta de novo sozinho, sem reiniciar nada |
| Seletor do perfil Fargate não bate | implante o Deployment num namespace diferente de `notificacoes`, ou sem o rótulo que o seletor exige | o pod fica preso em `Pending` para sempre; nenhum nó nem capacidade Fargate é atribuída | `kubectl describe pod` mostra evento `FailedScheduling`, sem candidato disponível | corrigir o namespace ou o rótulo para bater com o `selector` do perfil Fargate |
A falha que mais demora a ser notada
A de IRSA mal configurado é a mais perigosa das três porque o sintoma — nenhum e-mail saindo — não gera alarme técnico nenhum: não há pod reiniciando, não há 5xx, não há log de erro destacado. Ela só aparece quando um cliente reclama que não recebeu confirmação, o que pode levar horas. É o equivalente EKS da "prontidão que mente" do L03: a peça está tecnicamente saudável e não está fazendo o trabalho que deveria.
Um pod no perfil Fargate do EKS está com status `Running`. O papel de execução do Fargate Pod está corretamente anexado ao perfil e tem a política gerenciada da AWS. Toda chamada do contêiner à fila SQS retorna `AccessDenied`. Qual é a causa mais provável?
Segurança: duas camadas de permissão para acertar, não uma
Todo risco novo desta seção nasce do mesmo fato: o EKS soma RBAC do Kubernetes por cima do IAM da AWS, e menor privilégio agora precisa ser verdade nos dois sistemas ao mesmo tempo, não só num deles.
| Risco | Probabilidade | Impacto | Controle preventivo | Detecção | Resposta |
|---|---|---|---|---|---|
| IRSA do Load Balancer Controller com escopo largo demais | média | alto | condição `sub` restrita à ServiceAccount exata no trust policy | IAM Access Analyzer sobre o uso real da role | apertar o trust policy; auditar ALB criados fora do próprio namespace |
| ClusterRoleBinding amplo demais concedido para "destravar" um erro de RBAC | média | alto | Role/RoleBinding por namespace; nunca `cluster-admin` fora de depuração local | `kubectl get clusterrolebindings` revisado periodicamente | remover o binding; reproduzir o erro original para entender o escopo mínimo real |
| Confundir papel de execução do Fargate Pod com permissão do app | alta | médio | nenhuma; é erro de raciocínio, não de configuração — o controle é o comentário no código e a Prova 3 | CloudTrail sem `AssumeRoleWithWebIdentity` correspondente ao tráfego esperado | mover a permissão para IRSA; o papel de execução nunca deveria tê-la recebido |
| Endpoint público do control plane sem restrição | baixa | alto | `endpoint_public_access = false`, acesso só pela VPC | AWS Config: regra de conformidade sobre acesso público do cluster | desativar o acesso público; girar credencial de qualquer identidade exposta |
| Segredo do banco em variável de ambiente do manifesto | média | alto | Secret do Kubernetes referenciado por `secretKeyRef`, nunca literal no YAML versionado | busca por padrão de credencial no repositório de manifestos | rotacionar o segredo; mover para Secrets Manager com o CSI driver, se disponível |
| Imagem do Operator do parceiro sem procedência verificada | baixa | alto | varredura da imagem do Aegis como a de qualquer outra no ECR, antes de confiar no chart | achados de varredura no repositório onde a imagem for espelhada | suspender o Operator; validar com o parceiro antes de reativar |
O `*` que continua se justificando, e o que mudou
O `ecr:GetAuthorizationToken` do papel de execução do Fargate Pod continua sendo operação de conta, exatamente como no L03 — o `*` ali se justifica pelo mesmo motivo. O que muda no EKS é que existe uma SEGUNDA superfície de menor privilégio a auditar: não basta a política IAM estar certa, o RBAC do cluster também precisa negar por padrão o que não foi explicitamente concedido.
Observabilidade: as perguntas que o painel dos dois desenhos tem de responder
Boa parte do painel é idêntica entre os dois — CPU, memória, 5xx do alvo. As linhas abaixo são as que só existem porque o EKS existe.
| Pergunta | Métrica ou consulta | O que significa mudar | Limiar inicial |
|---|---|---|---|
| O contêiner está de fato autenticado na AWS? | CloudTrail: contagem de `AssumeRoleWithWebIdentity` por hora | queda para zero com pods rodando é IRSA quebrado, silenciosamente | 0 em 15 min com pods ativos |
| O Load Balancer Controller está reconciliando? | logs do controller: taxa de erro por reconciliação | erro persistente é permissão faltando ou sub-rede sem tag de ELB | > 3 erros seguidos |
| Algum pod não conseguiu ser agendado? | `kube_pod_status_scheduled` (métrica do add-on de métricas) ou evento `FailedScheduling` | perfil Fargate com seletor errado, ou capacidade da conta esgotada | qualquer ocorrência |
| O cluster está com a versão do Kubernetes perto de expirar suporte padrão? | `aws eks describe-cluster` → campo de versão, comparado ao calendário da AWS | expirar sem upgrade move o cluster para suporte estendido, mais caro | 90 dias antes do fim do suporte padrão |
| Quanto tempo o control plane está processando por requisição? | métricas de latência da API do EKS, expostas via CloudWatch Container Insights | degradação aqui afeta todo `kubectl apply`, inclusive o do Load Balancer Controller | p99 acima do dobro da linha de base |
A métrica que não existe no ECS e por isso ninguém cria alarme para ela
Contagem de `AssumeRoleWithWebIdentity` é uma métrica que só faz sentido depois que existe IRSA — times que migram do ECS trazem o painel de CPU/memória/5xx pronto e esquecem desta, porque ela não tinha equivalente antes. É justamente a que teria pego a Falha 1 da seção anterior em minutos, em vez de horas.
Escala: 10, 10 mil, 1 milhão — e por que a resposta é sempre "igual"
A descoberta desta seção é ela mesma o argumento do módulo: em volume de tráfego, os dois desenhos não divergem, porque os dois rodam em Fargate. A escala que realmente diferencia os dois é outra, e vem por último.
| Volume | O que acontece com a capacidade | O que passa a doer | O que fazer |
|---|---|---|---|
| 10 e-mails/dia, 1 pod/task de cada lado | nenhuma diferença — mesma CPU, mesma memória | nada de capacidade; só a curva de aprendizado operacional do EKS | ECS sozinho já bastaria, se não fosse o Aegis |
| 600 pedidos/dia (volume atual) | ainda nenhuma diferença de capacidade | RBAC e IRSA mal configurados custam hora de engenheiro, não throughput perdido | medir hora de operação, não requisição por segundo — é o que a Prova 5 faz |
| 1 milhão de e-mails/dia, dezenas de pods/tasks | cota de envio do SES e da fila dominam, iguais nos dois orquestradores | esgotamento de IP por sub-rede em modo `awsvpc`/Fargate — o MESMO limite nos dois | dimensionar a sub-rede pelo dobro da frota, independente de qual orquestrador |
| Falha de AZ | o control plane do EKS já é multi-AZ, gerenciado pela AWS — a mesma garantia que o ECS sempre teve | nenhum risco novo de disponibilidade de control plane | a diferença de resiliência entre os dois, nesta dimensão, é zero |
| Escala de ADOÇÃO — não de tráfego | quando um segundo, terceiro serviço migra para o mesmo cluster | RBAC por namespace deixa de ser boa prática opcional e vira necessidade | é o Nível 3 da evolução em níveis, adiante — e é aí que o custo operacional de fato cresce |
O gargalo que este laboratório não tem, e por quê
Um laboratório sobre escala de tráfego mediria latência e taxa de erro sob carga. Este não mede isso porque, para este serviço, a resposta é sempre "igual nos dois" — os dois orquestradores agendam o MESMO Fargate. O gargalo real da decisão ECS-ou-EKS não é de tráfego: é organizacional, e aparece quando o número de serviços e times no cluster cresce, não quando o número de requisições cresce.
Custo: o que o EKS acrescenta à fatura, e o que não acrescenta
A maior parte do que este laboratório introduz não está na fatura da AWS — está na folha de horas. A parte que ESTÁ na fatura é pequena e previsível.
| Dimensão | Cobra por | Cuidado |
|---|---|---|
| Taxa do control plane do EKS | cluster-hora, fixa | cerca de US$ 0,10/hora com a versão do Kubernetes em suporte padrão; cerca de US$ 0,60/hora em suporte estendido — confira o valor atual no AWS Pricing Calculator, porque preço muda e varia por região; o que não muda é ser cobrança POR CLUSTER, com ou sem pod rodando |
| Fargate (ambos orquestradores) | vCPU-hora e GB-hora | mesma tabela de preço nos dois — não é diferenciador de custo de computação |
| ALB | hora + unidade de capacidade (LCU) | igual nos dois; o Terraform cria direto no ECS, o controller cria no EKS, mas o recurso subjacente e o preço dele são os mesmos |
| Add-ons gerenciados (CoreDNS, VPC CNI, kube-proxy) | sem cobrança própria | mas cada nova versão do cluster pede compatibilidade de versão de add-on — é hora de engenharia em upgrade, não linha na fatura |
| Horas de engenharia | não aparece em nenhuma fatura da AWS | é a dimensão que a Prova 5 mede, e é a que mais pesa num piloto pequeno como este |
| Cenário | Volume | O que muda no gasto | Recomendação |
|---|---|---|---|
| A — piloto atual | ECS para 6 serviços, 1 cluster EKS pequeno só para o Aegis | uma taxa de control plane a mais na fatura; horas concentradas no aprendizado inicial | é o cenário deste laboratório |
| B — migração completa | os 7 serviços em EKS | a mesma taxa de control plane pode hospedar todos, mas RBAC e blast radius pedem múltiplos ambientes — e as horas de operação multiplicam sem ganho de capacidade | não recomendado sem um requisito por serviço, não só pelo Aegis |
| C — recusar Kubernetes | negociar outro fornecedor de detecção de fraude | zero de EKS na fatura, mas custo de troca de fornecedor e migração de dados de fraude | legítimo, mas fora do escopo técnico deste módulo |
O custo oculto que só aparece meses depois
Se a versão do Kubernetes do cluster envelhece além da janela de suporte padrão sem upgrade, a AWS move a cobrança automaticamente para o suporte estendido — cerca de seis vezes a taxa padrão, pela MESMA capacidade. Não é aumento de uso: é o preço de adiar um upgrade que já era conhecido. É o equivalente, em custo esquecido, ao Elastic IP órfão do L01 — só que a fatura demora meses para acusar, em vez de dias.
Well-Architected nos seis pilares
| Pilar | Situação ao fim deste laboratório | Risco que fica | Melhoria | Prioridade |
|---|---|---|---|---|
| Excelência operacional | os dois desenhos rodam em paralelo, com horas de operação medidas e registradas | dois painéis, dois caminhos de log — dívida de piloto que precisa de prazo de encerramento | desligar o desenho perdedor ao fim da medição (é a própria decisão deste módulo) | alta |
| Segurança | IRSA escopado por ServiceAccount, endpoint privado, namespace isolado | a IRSA do Load Balancer Controller age em nome do cluster inteiro | revisar o trust policy dela com a mesma régua de menor privilégio do L41 | alta |
| Confiabilidade | o control plane do EKS já é multi-AZ gerenciado pela AWS, como o do ECS | add-ons desatualizados após upgrade de versão do cluster | janela de upgrade planejada, antes do fim do suporte padrão | média |
| Eficiência de performance | mesma capacidade Fargate nos dois desenhos; nenhuma regressão medida | nenhum — a performance não é onde os dois desenhos divergem | não aplicável a este laboratório | baixa |
| Otimização de custos | piloto de escopo pequeno, com control plane medido separadamente da fatura de Fargate | suporte estendido custa cerca de seis vezes mais pela mesma capacidade, se o cluster envelhecer | upgrade de versão antes do fim do suporte padrão | alta |
| Sustentabilidade | nenhuma capacidade computacional a mais que o ECS já usava | um cluster inteiro (com add-ons, control plane) para hospedar um único serviço piloto | consolidar mais serviços com requisito real no mesmo cluster antes de abrir um segundo | baixa |
Evolução em níveis: quando o próximo passo de Kubernetes se justifica
A terceira arquitetura não é um desenho: é a resposta a QUANDO dar o próximo passo. Cada nível resolve um risco e compra outro — e a coluna de custo é a que mais raramente se escreve antes de decidir.
Tudo em ECS Fargate. A decisão de orquestração nem se coloca — é a situação da Cadência antes deste laboratório.Um cluster EKS pequeno, um serviço piloto, IRSA em tudo, horas de operação medidas contra o equivalente ECS.Um segundo, terceiro serviço com requisito PRÓPRIO (não "já que o cluster existe") migra para o mesmo cluster.Separação por ambiente (dev/stage/prod) e por time; `kubectl apply` manual em produção é substituído por GitOps (Argo CD ou Flux).EKS vira a opção padrão para serviço novo que genuinamente precisa do ecossistema (operators, malha de serviço, autoscaling avançado), com um time de plataforma dedicado a add-ons e versão.Cargas de treinamento ou inferência com GPU, que precisam do modelo de agendamento por device plugin do Kubernetes para compartilhar aceleradores entre times de forma multi-tenant.A ordem não é negociável, e o motivo é concreto
RBAC por namespace do Nível 3 depende de já ter uma identidade por ServiceAccount funcionando — que é o Nível 2. GitOps do Nível 4 depende de manifesto estável o suficiente para versionar — que pressupõe o Nível 3 resolvido. Quem tenta plataforma (Nível 5) sem ter operado RBAC multi-time (Nível 3) monta processo de governança sobre um cluster que ninguém testou com dois times dentro.
Onde IA entra nesta arquitetura, e onde não entra
Neste módulo, para o serviço de Notificações, IA não resolve nada — e forçá-la seria o antipadrão que a própria série critica. A pergunta "ECS ou EKS" tem resposta determinística: um requisito escrito existe ou não existe. Um modelo não melhora essa checagem.
Há exatamente um lugar, na evolução em níveis, onde IA muda a resposta: o Nível 6, cargas de treinamento e inferência com GPU. Ali, o motivo de escolher Kubernetes não é "IA é moderna e Kubernetes também" — é que o modelo de agendamento de GPU por device plugin, com múltiplos times compartilhando aceleradores caros, é um problema que o ecossistema Kubernetes resolve com maturidade que o ECS, hoje, não tem no mesmo nível.
| Pergunta | Resposta honesta para este módulo |
|---|---|
| Qual problema a IA resolveria no serviço de Notificações? | nenhum — é envio de e-mail transacional, determinístico, sem ambiguidade a resolver |
| Por que citar IA aqui, então? | porque "vamos de Kubernetes por causa de IA" é uma frase real que aparece em decisão de arquitetura, e o módulo precisa dizer exatamente onde ela é verdadeira e onde não é |
| Onde ela é verdadeira? | agendamento multi-tenant de GPU para treinamento ou inferência — Nível 6 da evolução, não este piloto |
| O Operator do Aegis é "IA na arquitetura"? | não — é um controlador Kubernetes comum que aplica política; o que ele faz por trás (a lógica de detecção de fraude do parceiro) é opaco a este laboratório e não muda a resposta de infraestrutura |
| Por que não migrar já pensando no Nível 6? | porque a Cadência não tem carga de GPU hoje — decidir infraestrutura por uma necessidade hipotética é o mesmo erro que "vamos de Kubernetes" comete, só que projetado para o futuro em vez do presente |
O uso de "IA" que parece justificativa e é hype disfarçado
Ouvir "precisamos de Kubernetes para IA" sem perguntar QUAL carga de IA, com QUAL necessidade de GPU compartilhada, é aceitar a mesma frase vazia de "vamos de Kubernetes" com uma palavra a mais na frente. A pergunta que desfaz o hype é sempre a mesma: qual requisito, escrito, esta peça atende — que nenhuma peça mais simples atende?
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 |
|---|---|---|---|---|---|
| "Vamos de Kubernetes" sem requisito escrito | currículo pessoal, ou "é o padrão do mercado" repetido em painel de conferência | paga o custo do control plane e do RBAC sem nenhum ganho mensurável | cluster com um serviço só, sem ninguém saber apontar por que ele não está em ECS | exigir o requisito na tabela de requisitos antes de abrir o primeiro cluster | nunca — nem quando "todo mundo está migrando" |
| Anexar permissão de negócio ao papel de execução do Fargate Pod | a mensagem de erro (AccessDenied da aplicação) não aparece perto de onde se editou o papel | confused deputy: TODO pod do cluster herdaria a mesma permissão, se o papel de execução concedesse algo — o que ele nem faz | `AccessDenied` da aplicação mesmo com o papel de execução aparentemente correto | IRSA por ServiceAccount, escopo mínimo por serviço | nunca; é sempre o papel errado para esse propósito |
| ClusterRoleBinding com `cluster-admin` para destravar erro de RBAC durante o desenvolvimento | erro de RBAC é confuso de depurar, e a saída rápida é dar acesso total e seguir em frente | qualquer credencial roubada de qualquer pod agora administra o cluster inteiro | ferramenta de auditoria de RBAC aponta binding amplo nunca revisado desde a criação | Role/RoleBinding por namespace, com escopo mínimo, revisado antes de produção | só em ambiente de desenvolvimento local descartável, nunca em cluster compartilhado |
| Migrar todos os serviços para EKS "já que o cluster existe" | parece eficiente reaproveitar infraestrutura que já está de pé e já foi paga | multiplica RBAC/add-ons/versão sem que cada serviço tenha requisito próprio; o piloto vira produção sem plano | sete serviços num cluster pensado e dimensionado para um piloto de um só | mover serviço por serviço, cada um com seu próprio requisito nomeado | quando o serviço TEM requisito próprio — aí não é "já que existe", é decisão nova |
| Instalar o Load Balancer Controller com a política IAM de exemplo, sem revisar | a política oficial é longa, e copiar/colar o `iam_policy.json` funciona na hora | o controller passa a poder criar ou apagar ALB de qualquer serviço na conta, não só o seu | ALB inesperado aparecendo ou desaparecendo fora do namespace de Notificações | revisar a política antes de aplicar; considerar fronteira por cluster ou por conta | em ambiente de teste isolado, sem outros ALBs de produção na mesma conta |
| Comparar ECS e EKS só pela fatura da AWS | a fatura é um número concreto, fácil de defender numa reunião; hora de operação parece subjetivo demais para citar | a taxa do control plane é pequena perto do tempo perdido depurando RBAC e IRSA pela primeira vez — decisão baseada só na fatura erra a comparação inteira | "o EKS é barato" citado sem nenhuma hora de operação registrada ao lado | medir horas de operação como este laboratório mediu, e registrar — é a Prova 5 | nunca; a fatura sozinha nunca captura o custo operacional real |
Quando algo não funciona
| Sintoma | Causa provável | Como investigar | Onde olhar | Correção |
|---|---|---|---|---|
| Pod Running, mas nenhuma chamada AWS funciona | ServiceAccount sem anotação de IRSA, ou trust policy com `sub` errado | confira a variável `AWS_ROLE_ARN` dentro do pod e o evento correspondente no CloudTrail | `kubectl exec ... env`; CloudTrail em `AssumeRoleWithWebIdentity` | corrigir a anotação da ServiceAccount e a condição `sub` do trust policy |
| Ingress sem endereço, rota administrativa inacessível | o Load Balancer Controller sem permissão IAM, ou sub-rede sem a tag esperada | leia os logs do controller à procura de erro de reconciliação | `kubectl logs -n kube-system deploy/aws-load-balancer-controller` | restaurar a permissão faltante, ou marcar a sub-rede com `kubernetes.io/role/elb` |
| Pod preso em Pending indefinidamente | namespace ou rótulo do pod não bate com o `selector` do perfil Fargate | leia o evento de agendamento do pod | `kubectl describe pod` → evento `FailedScheduling` | ajustar namespace/rótulo do Deployment para bater com o perfil Fargate |
| `kubectl apply` aceito, comportamento não muda | add-on gerenciado (CoreDNS, VPC CNI) em versão incompatível com a versão do cluster | compare a versão instalada do add-on com a matriz de compatibilidade da versão do cluster | `aws eks describe-addon-versions` | atualizar o add-on para a versão compatível com o cluster |
| Custo do EKS maior que o esperado com poucos pods | a taxa fixa do control plane corre independente de uso, ou o cluster caiu em suporte estendido | filtre o Cost Explorer pelo serviço EKS e pela linha de suporte estendido | AWS Cost Explorer → Amazon Elastic Kubernetes Service | avaliar se o cluster piloto ainda se justifica; agendar upgrade se caiu em suporte estendido |
| Aplicação funciona no ECS e falha só no EKS | diferença de superfície de configuração entre a task definition e o manifesto YAML — uma variável de ambiente que existia num lado e foi esquecida no outro | compare `environment` da task definition com `env` do Deployment, campo a campo | `aws ecs describe-task-definition` vs. `kubectl get deploy -o yaml` | declarar as mesmas variáveis nos dois manifestos; automatizar essa paridade se o piloto virar produção |
A pergunta que resolve metade destes casos
Antes de mexer em RBAC ou em política IAM, pergunte: o erro apareceu no `kubectl apply` ou só quando o código tentou fazer alguma coisa? A maioria das falhas deste laboratório — IRSA, seletor de Fargate, permissão do controller — é aceita silenciosamente pela API do Kubernetes e só se manifesta depois, no comportamento real. É a mesma lição do "5xx do alvo vs. 5xx do balanceador" do L03, adaptada para uma camada de indireção a mais.
Limpeza: o que o EKS deixa vivo que o ECS nunca deixou
A ordem importa mais aqui do que em qualquer laboratório anterior: o ALB do desenho EKS foi criado pelo controller, não pelo Terraform, e apagar o cluster antes do manifesto deixa esse ALB órfão — cobrando por hora, sem nenhum recurso do Terraform apontando para ele.
#!/usr/bin/env bash
# limpar.sh — o EKS deixa recurso vivo que o ECS nunca teve
set -euo pipefail
PROJETO="${PROJETO:?defina PROJETO}"; REGIAO="${REGIAO:-us-east-1}"
# 1. O ALB do EKS foi criado PELO CONTROLLER, nao pelo Terraform — apague os
# manifestos ANTES do terraform destroy, ou o ALB fica orfao, cobrando por
# hora sem nenhum recurso do Terraform apontando para ele.
kubectl delete ingress -n notificacoes notificacoes-admin
kubectl delete -f notificacoes.yaml
# 2. So entao derrube o que o Terraform administra: cluster, perfil Fargate,
# as tres roles IAM novas, o provedor OIDC.
terraform destroy -auto-approve
# 3. O CLUSTER as vezes demora minutos para deixar de existir de fato — e a
# taxa do control plane corre ate esse instante, nao ate o `destroy` sair.
aws eks describe-cluster --name "${PROJETO}-eks" --region "$REGIAO" 2>&1 \
| grep -q "ResourceNotFoundException" && echo "cluster removido" \
|| echo "cluster ainda existindo — cobrando"
# 4. O provedor OIDC as vezes sobrevive ao cluster se algo falhar no meio.
aws iam list-open-id-connect-providers --query 'OpenIDConnectProviderList[].Arn' --output table
# 5. O grupo de logs do controller tem ciclo proprio, como no L03.
aws logs delete-log-group --log-group-name "/aws/eks/${PROJETO}-eks/cluster" 2>/dev/null || true
# 6. O que veio do L31/L33 e continua cobrando: a fila, a ALB do desenho ECS,
# o RDS. Rode a limpeza deles tambem se nao for seguir para outro laboratorio.
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 |
|---|---|---|---|
| ALB provisionado pelo controller | não, se o Ingress não for apagado antes | sim, por hora + LCU | ele não existe no estado do Terraform — quem o criou foi o controller |
| Cluster EKS | sim, mas leva minutos | sim, até desaparecer de fato | a taxa do control plane corre até o cluster deixar de existir, não até o comando retornar |
| Provedor OIDC | depende — pode sobreviver a uma interrupção no meio do destroy | não diretamente, mas identidades órfãs continuam confiáveis | está registrado separadamente do cluster que o referenciava |
| Perfil Fargate e as três roles IAM novas | sim | não | perfis e roles não cobram parados; ficam de pé só se o destroy falhar no meio |
| Grupo de logs do cluster | depende de configuração | sim, por retenção | tem ciclo próprio, como no L03 — sobrevive ao cluster que o alimentava |
| Fila SQS, ALB do desenho ECS, RDS (herdados do L31) | não fazem parte deste destroy | sim, por hora ou por mensagem | pertencem a laboratórios anteriores; limpe-os à parte se não for continuar a série |
Resumo: problema, peça e motivo
| Problema | Peça | Por que ela, e não outra |
|---|---|---|
| "Vamos de Kubernetes" sem critério | tabela de requisitos com coluna de desenho | transforma preferência em pergunta verificável: existe requisito, ou não existe |
| Parceiro só distribui como Operator | um cluster EKS pequeno, piloto | é o único requisito real encontrado; resolve o bloqueio sem migrar tudo |
| Contêiner sem permissão AWS | IRSA por ServiceAccount, nunca o papel de execução | é a única forma correta de conceder permissão de negócio a um pod |
| ALB que precisa existir sem Terraform criá-lo | AWS Load Balancer Controller | observa Ingress/Service e provisiona o recurso — é o padrão do ecossistema Kubernetes |
| Quem, dentro do cluster, pode o quê | RBAC por namespace | é a segunda camada de permissão que o ECS nunca teve, e que o IAM sozinho não cobre |
| Decidir migrar tudo, nada, ou uma fatia | horas de operação medidas (Prova 5) | é o único critério que não é opinião — a fatura da AWS sozinha some a diferença real |
| Falha | O que a protege | O que ela NÃO protege |
|---|---|---|
| AccessDenied silencioso do contêiner | IRSA corretamente anotado na ServiceAccount | trust policy com `sub` escrito errado — a anotação existir não garante que ela aponta certo |
| ALB nunca provisionado | IRSA do controller com permissão de ELB | sub-rede sem a tag `kubernetes.io/role/elb` — permissão certa não basta se a sub-rede não está marcada |
| Pod nunca agendado | seletor do perfil Fargate batendo com namespace/rótulo | cota de ENI ou de perfil Fargate da conta — isso é limite separado, não erro de seletor |
| Cluster inteiro exposto | endpoint privado do control plane | RBAC mal configurado dentro da própria VPC — acesso privado não é o mesmo que acesso restrito |
| Custo de suporte disparando | upgrade de versão antes do fim do suporte padrão | esquecer de agendar o upgrade — a proteção exige processo, não só saber que ela existe |
- O VP pede Kubernetes; nenhum requisito acompanha o pedido.
- O parceiro Aegis distribui só como Operator: esse é o requisito real.
- A tabela de requisitos separa o que obriga Kubernetes do que só governa o piloto.
- O mesmo binário .NET 8 é implantado em ECS Fargate e em EKS com perfil Fargate.
- No EKS, o papel de execução do pod sobe o contêiner; ele NÃO concede permissão de negócio.
- IRSA, ligado por OIDC a uma ServiceAccount, é quem concede — uma role a mais que o ECS não tinha.
- O AWS Load Balancer Controller observa o Ingress e cria o ALB, com sua própria IRSA.
- As cinco provas confirmam que cada peça faz o que deveria — a quinta mede horas de operação.
- O e-mail que chega ao cliente é idêntico nos dois desenhos: a diferença inteira foi operacional.
- A decisão final usa o número medido, não a preferência de quem pediu Kubernetes primeiro.
Perguntas frequentes
❓ ECS ou EKS: qual devo escolher na AWS?
❓ Quanto custa o plano de controle do EKS?
❓ O que é IRSA no EKS e por que preciso dele com Fargate?
❓ Qual a diferença entre o papel de execução do Fargate Pod e o papel de uma task no ECS?
❓ Preciso do AWS Load Balancer Controller para usar Ingress no EKS?
❓ Kubernetes na AWS é mais caro que ECS?
❓ Quando o EKS realmente se justifica sobre o ECS?
❓ RBAC do Kubernetes substitui o IAM da AWS?
Fixando
Depois de operar o piloto EKS por um mês, a taxa fixa do control plane somou pouco mais de setenta dólares, e o time registrou cerca de três vezes mais horas de engenharia operando o cluster do que operando o serviço equivalente em ECS. Que erro de raciocínio comete quem conclui, só a partir da fatura da AWS, que o EKS foi mais barato?
O time da Cadência quer manter tudo em ECS Fargate, mas o parceiro de detecção de fraude Aegis distribui o produto apenas como Helm chart com um Operator que observa um CRD próprio. Por que isso, sozinho, já é um requisito real para abrir um cluster Kubernetes — sem que seja hype?
Conhecimentos, próximo módulo e documentação
| Item | Conteúdo |
|---|---|
| Conhecimentos anteriores necessários | L30 (limites do serverless) e L33 (descoberta e malha de serviço) lidos; L31 no ar (serviço de Notificações em ECS Fargate); noção básica de pod/deployment/service ajuda |
| Conhecimentos adquiridos | a distinção entre papel de execução do Fargate Pod e IRSA; os três motivos legítimos para escolher EKS sobre ECS; RBAC como segunda camada de permissão; a taxa fixa do control plane e o custo oculto do suporte estendido; como medir custo operacional com horas, não só fatura |
| Limitação que fica | os dois desenhos convivem durante o piloto, dobrando painel e log; e a IRSA do Load Balancer Controller, com o escopo mais largo dos três papéis novos, ainda carece da mesma revisão de menor privilégio que o L41 formaliza |
| Próximo exemplo recomendado | L42 — identidade de workload: task role, execution role, IRSA. Generaliza a distinção de identidade que este módulo tratou só para o caso EKS, cobrindo também IAM Identity Center e a comparação entre role e usuário de longa duração |
| Também habilitado por este módulo | L41 (policy que passa auditoria) tem agora um caso real de IRSA com escopo largo para auditar; qualquer laboratório futuro de plataforma multi-cluster parte do Nível 4 da evolução em níveis daqui |
| Data da última validação técnica | 7 de agosto de 2026 |
Documentação oficial consultada: Amazon EKS Pricing — a taxa por cluster-hora em suporte padrão e estendido; Amazon EKS Pod execution IAM role — o que ela autoriza e a distinção explícita de que o contêiner não a herda; IAM roles for service accounts e Assign IAM roles to Kubernetes service accounts — o mecanismo de IRSA por federação OIDC; Install AWS Load Balancer Controller — manifesto e Helm, e a necessidade de uma role própria. O enquadramento geral de quando cada orquestrador se justifica também se apoiou no artigo oficial "Amazon ECS vs. Amazon EKS: making sense of AWS container services", do blog de contêineres da AWS.
O que não foi verificado, e você deve conferir na sua conta
Os tempos citados — cerca de 12 minutos até o cluster ficar ACTIVE, 40 minutos contra 3h10 de horas de operação — são a medição do ambiente de exemplo da Cadência, e servem como ordem de grandeza, não como referência. Meça o seu: tempo de criação de cluster varia por região e por versão do Kubernetes, e horas de operação caem MUITO depois da primeira vez, à medida que a curva de aprendizado de IRSA e do Load Balancer Controller é paga. O valor da taxa do control plane também deve ser conferido no AWS Pricing Calculator antes de qualquer decisão orçamentária: preços mudam e variam por região.
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…