Lab 01 — App .NET 8 no ECS Fargate com RDS e CloudFront
Ao terminar: Você sobe, em Terraform, uma aplicação de três camadas com banco isolado e segredo rotacionado — e sabe explicar cada decisão.
A API roda na máquina de alguém. O front, no npm run dev de outra pessoa. O banco é um contêiner local que já perdeu os dados duas vezes. Quando o cliente pede para ver, alguém liga um túnel e reza. Este laboratório encerra esse arranjo: no fim dele existe uma URL pública, servida por uma aplicação .NET 8 em contêiner, com banco gerenciado que a internet não alcança, e tudo descrito em Terraform — de modo que a próxima pessoa reproduz sem perguntar nada.
O problema, e a empresa que o tem
Uma equipe de quatro pessoas mantém o sistema de pedidos de uma distribuidora. São cerca de 400 usuários internos e um pico previsível às segundas, quando o time comercial lança as ordens da semana: aproximadamente 12 mil requisições na primeira hora, contra 1.500 numa hora comum. O horário comercial é o que importa; à noite o sistema pode ficar mais lento sem que ninguém reclame.
A dor não é desempenho: é que não existe um ambiente que não seja o laptop de alguém. Publicar significa parar. Um deploy malfeito na sexta custou quatro horas de operação, e a única cópia do banco daquele dia estava no disco de quem fez o deploy. O orçamento é apertado — trata-se de uma equipe pequena, não de uma empresa com time de plataforma —, então a arquitetura precisa ser defensável no custo, não apenas correta.
Os números acima são hipóteses declaradas para dimensionar o laboratório, não medições de um caso real. O que eles determinam é concreto: com esse volume, o gargalo não será CPU de aplicação, e sim conexão de banco e tempo de deploy. Se o seu caso tem outra ordem de grandeza, a seção de escala mostra o que muda.
O que você vai conseguir fazer
Objetivo de laboratório não é reconhecer nome de serviço. Cada linha abaixo é verificável por alguém olhando o seu ambiente:
- Explicar, apontando no desenho, por que o banco não tem rota para a internet e o que o impede de aceitar conexão de fora.
- Provisionar a VPC de duas AZs com Terraform e justificar cada sub-rede pelo que mora nela.
- Publicar uma imagem .NET 8 no ECR e implantar por digest, não por tag móvel.
- Fazer a aplicação ler a senha do banco em execução, sem nenhuma credencial na imagem nem no repositório.
- Distinguir health check raso de profundo e dizer o que quebra ao usar o profundo no balanceador.
- Rodar um deploy e observar o rollout sem devolver erro 5xx a nenhum cliente.
- Medir o custo por componente desta arquitetura com tag de alocação.
- Restaurar o banco em um ponto no tempo e informar o RTO que você mediu, não o que estimou.
- Destruir o ambiente e provar, por comando, que nada ficou cobrando.
O que a certificação cobra disto
Esta arquitetura é o cenário mais frequente das provas de associate, porque ela concentra três decisões que a AWS considera fundamentais: onde o recurso mora, quem alcança quem, e o que acontece quando uma zona cai.
| Conceito | Certificação | Como aparece neste laboratório | O que dominar |
|---|---|---|---|
| Sub-rede pública vs privada | CLF, SAA | ALB público, aplicação e banco privados | Que "privada" significa sem rota para o gateway de internet, não uma etiqueta |
| AZ como domínio de falha | CLF, SAA | Duas AZs, duas tasks, banco Multi-AZ | Que redundância dentro de uma AZ não protege da queda dela |
| Security group por origem | SAA, SOA | SG do banco referencia o SG da task | Que referência a grupo sobrevive a mudança de endereçamento; CIDR não |
| Multi-AZ vs réplica de leitura | SAA | Standby síncrono que não atende consulta | Que Multi-AZ é disponibilidade e réplica é escala de leitura — nunca a mesma coisa |
| Role de execução vs role de task | DVA, SAA | Duas roles distintas na task definition | Quem puxa a imagem é o agente; quem chama a API é a sua aplicação |
| Segredo gerenciado | SAA, SOA | Senha gerada e rotacionada pelo RDS | Que segredo em variável de ambiente não acompanha rotação |
| Health check e drenagem | DVA, SOA | Alvo raso, atraso de remoção reduzido | Que health check profundo no balanceador propaga falha de dependência |
| PITR e retenção | SAA, SOA | Retenção de 7 dias habilitando ponto no tempo | Que retenção zero desliga o recurso, e que RTO só existe se foi ensaiado |
| Custo por hora ligada | CLF, SAA | ALB, NAT e Multi-AZ cobram parados | Distinguir custo por hora de custo por uso é metade das questões de FinOps |
Requisitos, e como cada um muda o desenho
A coluna da direita é a que importa. Requisito que não altera nenhuma peça do desenho é declaração de intenção, e desenho com peça que nenhum requisito pede é adorno. Este laboratório existe para você conseguir traçar essa linha nos dois sentidos.
| Requisito | Valor assumido | O que ele acrescenta ao desenho |
|---|---|---|
| Funcional: consultar e lançar pedidos | API HTTP com persistência relacional | ECS Fargate para a aplicação, RDS PostgreSQL para o estado |
| Funcional: front acessível por navegador | SPA e API no mesmo domínio | CloudFront servindo o estático e roteando a rota de API para o ALB |
| Disponibilidade em horário comercial | Perder uma AZ não para a operação | Duas AZs, no mínimo duas tasks, banco Multi-AZ |
| Latência | p95 abaixo de 400 ms na API interna | Cache na borda para estático, pool de conexão dimensionado na aplicação |
| Durabilidade e recuperação | RPO de 5 min, RTO de 1 h | PITR habilitado com 7 dias de retenção, e um ensaio de restauração |
| Segurança | Nenhuma credencial de longa duração fora do cofre | Secrets Manager com rotação pelo RDS, KMS, task role de duas ações |
| Privacidade | Dado de cliente não vai para log | Log estruturado com campo negado, e revisão do que a aplicação serializa |
| Observabilidade | Diagnosticar erro relatado em minutos | Log, métrica e trace com o mesmo id de correlação |
| Custo | Defensável para equipe de quatro pessoas | VPC endpoint em vez de NAT, classe de banco pequena, tag em tudo |
| Manutenibilidade | Qualquer pessoa reproduz o ambiente | Terraform com variável por ambiente e nenhum passo manual no console |
O requisito que este laboratório NÃO atende, de propósito
Não há recuperação de desastre regional. O RTO de 1 hora é para falha de AZ e para erro humano no dado, não para a perda de uma região inteira. Assumir que Multi-AZ cobre desastre regional é o erro de leitura mais caro desta arquitetura, e é justamente o assunto do laboratório de DR multi-região.
Arquitetura mínima: o que basta para estar no ar
Comece por aqui, e resista à tentação de já colocar WAF, KMS e trace. O desenho mínimo tem uma função pedagógica que a versão completa não tem: ele deixa visível o que cada peça da versão de produção veio resolver. Quem já sobe tudo junto aprende a receita e não a decisão.
- → HTTPS na 443, pelo nome DNS do balanceador
- → encaminha para o IP privado da task sadia
- → consulta SQL na 5432, dentro da VPC
- → imagem puxada quando a task inicia
- Fora da AWS
- Rede e entrega
- Compute
- Banco de dados
Percorra os passos: o que separa este desenho de um contêiner solto na internet é onde cada peça mora. O balanceador é o único endereço público, a aplicação não tem rota de entrada e o banco só responde a um security group. A ligação tracejada é a que derruba o primeiro deploy de quase todo mundo.
- O cliente só conhece o balanceador. O nome público resolve para o ALB, não para a aplicação. Isso não é enfeite de segurança: é o que permite trocar, escalar e derrubar tasks sem que ninguém do lado de fora perceba, porque nenhum cliente jamais soube o endereço delas.
- A aplicação não tem porta de entrada. A task fica em sub-rede privada e aceita tráfego apenas do security group do ALB. Se alguém descobrir o IP dela, não consegue abrir conexão — a regra é por security group de origem, não por faixa de IP.
- A imagem vem de fora da rede privada. O ECR é regional e não fica dentro da VPC. Uma task em sub-rede privada sem NAT Gateway e sem VPC endpoints para ECR, S3 e CloudWatch Logs falha ao puxar a imagem, e o erro aparece como task que morre antes de logar qualquer coisa. É o passo que mais custa tempo em um primeiro deploy.
- O banco aceita um único vizinho. O security group do RDS referencia o security group da task, não um CIDR. Regra por CIDR envelhece: a sub-rede muda, alguém abre para a VPC inteira, e o banco passa a aceitar qualquer coisa que rode ali dentro.
- Onde este desenho quebra. Uma task e uma instância de banco numa AZ só: o desenho inteiro cai com a AZ, e cai também durante todo deploy e toda manutenção do banco. É por isso que ele é o mínimo para aprender e não a arquitetura de produção — a próxima seção mostra exatamente quais peças esse requisito acrescenta.
Ele é implantável e serve tráfego real. Também tem três fraquezas conhecidas, e nomeá-las agora é o que faz a próxima seção parecer óbvia em vez de arbitrária: uma AZ só, senha escolhida por uma pessoa, e nenhuma forma de responder por que está lento além de abrir o log e ler.
Arquitetura para produção
Mesma aplicação, mesmo banco, mesma responsabilidade. O que muda é que cada restrição declarada na seção de requisitos passou a ter uma peça que a atende — e nada além disso entrou.
- → regra gerenciada e limite de taxa por IP
- → só a requisição dinâmica; o estático morre no cache
- → roteia para task sadia e drena a que está saindo
- → escrita e leitura pela 5432
- → replicação síncrona; promove no failover
- → digest imutável no start da task
- → senha lida em execução pela task role
- → chave que cifra o segredo em repouso
- → log com id de correlação e métrica de negócio
- → trace com o mesmo id propagado
- Rede e entrega
- Segurança e identidade
- Compute
- Banco de dados
- Gestão e governança
Cada peça nova aqui rastreia a um requisito não funcional declarado, e nenhuma foi acrescentada por completude. Se você não conseguir apontar o requisito que justifica um componente, ele é adorno — e adorno em arquitetura custa dinheiro e superfície de falha. O tracejado é controle e observação; o contínuo é o caminho do dado.
- A borda decide o que nem chega. Requisito: absorver tráfego automatizado sem escalar computação. WAF e CloudFront respondem ou descartam antes da origem, e o estático em cache não consome task nenhuma. Barrar na borda é mais barato que escalar para aguentar.
- Duas AZs, e o que elas custam. Requisito: sobreviver à perda de uma zona de disponibilidade. ALB e serviço em duas AZs, com no mínimo duas tasks. O custo não é só a segunda task: tráfego entre AZs é cobrado, e é a linha que aparece na fatura sem ninguém ter pedido.
- O segredo não está na imagem. Requisito: nenhuma credencial de longa duração no repositório nem na imagem. O RDS gera e rotaciona a senha no Secrets Manager, e a aplicação a lê em execução com a task role. Injetar como variável de ambiente também funciona e é mais simples, mas a variável não acompanha rotação sem uma nova task.
- O standby não serve leitura. Requisito: reduzir o tempo de indisponibilidade em falha do banco. Multi-AZ mantém uma cópia síncrona que assume no failover e não atende consulta em nenhuma circunstância. Quem espera ganho de leitura aqui está pensando em réplica de leitura, que é outro recurso e outra linha na fatura.
- A imagem é imutável. Requisito: o que subiu em homologação é o que sobe em produção. Referenciar por digest, e não por tag como latest, é o que faz rollback significar voltar ao artefato exato em vez de reconstruir e torcer.
- Sem telemetria correlacionada, nada acima é operável. Requisito: diagnosticar um erro relatado pelo cliente em minutos. Log, métrica e trace compartilhando o mesmo id de correlação transformam a pergunta "por que está lento" em uma consulta. Sem isso, a arquitetura acima é bonita e cega.
| Peça acrescentada | Requisito que a justifica | O que acontece sem ela |
|---|---|---|
| Segunda AZ no ALB e no serviço | Perder uma AZ não para a operação | A queda de uma zona derruba tudo, e o deploy também derruba |
| RDS Multi-AZ | RTO de 1 h em falha de infraestrutura | Falha do banco vira restauração manual de snapshot, em horas |
| CloudFront | Latência do estático e TLS perto do usuário | Cada arquivo estático consome capacidade de task |
| AWS WAF | Tráfego automatizado não escala a computação | Escalar para aguentar bot custa mais que barrar o bot |
| Secrets Manager com rotação | Nenhuma credencial de longa duração | A senha vive no histórico do Terraform e nunca muda |
| KMS com chave própria | Auditoria precisa saber qual chave cifra o quê | Você não consegue revogar acesso ao dado sem apagar o dado |
| CloudWatch e X-Ray correlacionados | Diagnosticar em minutos | Investigação por leitura de log, e latência sem causa identificável |
| Imagem por digest | O que subiu em homologação sobe em produção | Tag móvel faz rollback reconstruir artefato diferente |
O caminho da requisição, ponta a ponta
Um log da etapa 8 precisa carregar o suficiente para reconstruir as sete anteriores. Este é o formato mínimo, e note o que ele não tem: nenhum dado de cliente.
{
"timestamp": "2026-08-06T14:22:31.784Z",
"level": "Warning",
"traceId": "1-68f3a2c7-4b1e9d0f5a2c8e7b6d4f1a3c",
"spanId": "5a2c8e7b6d4f1a3c",
"requestId": "req-9f2c",
"route": "GET /api/itens",
"statusCode": 500,
"durationMs": 4187,
"dbPoolWaitMs": 4102,
"dbPoolSize": 20,
"taskArn": "arn:aws:ecs:us-east-1:111122223333:task/app/9c1f",
"availabilityZone": "us-east-1b",
"imageDigest": "sha256:3f7a",
"message": "timeout aguardando conexao do pool"
}Com esses campos, a pergunta por que deu 500 tem resposta em uma consulta: 4.102 ms dos 4.187 ms foram espera de pool, com pool de 20. Não é lentidão de banco nem de rede — é dimensionamento de pool. Sem o campo dbPoolWaitMs, a mesma investigação vira meia hora de suposição.
As decisões, e o que se perde em cada uma
📋 Aplicação .NET 8 com estado em banco relacional, equipe de quatro pessoas, pico previsível em horário comercial e nenhum time de plataforma para operar infraestrutura.
Fargate remove a camada que a equipe não tem gente para operar — patch de sistema operacional, capacidade de cluster, drenagem de instância — e mantém o modelo de contêiner que o time já usa localmente. O RDS gerenciado dá backup, PITR, failover e rotação de senha sem que ninguém precise escrever um script. A soma é a menor quantidade de operação por unidade de capacidade, que é o critério certo quando a restrição escassa é gente, não dinheiro.
Alt: EC2 com Auto Scaling — Mais barato por unidade de CPU e mais caro em atenção humana: AMI, patch, ciclo de vida e drenagem passam a ser problema seu. Vale quando há compromisso de capacidade de longo prazo ou dependência que exige acesso ao host.
Alt: AWS Lambda — Excelente para o processamento assíncrono desta mesma aplicação, ruim como destino de API com conexão persistente de banco: cada execução concorrente quer sua conexão, e o pool que protege o RDS deixa de existir. Sem RDS Proxy, a concorrência esgota o banco antes de escalar.
Alt: DynamoDB em vez de PostgreSQL — Elimina o gargalo de conexão e escala melhor, ao preço de modelar por padrão de acesso e perder consulta relacional ad hoc. O relatório que o time comercial pede toda semana passa a exigir um segundo caminho.
Alt: AWS App Runner — Menos peças para configurar que o ECS, e menos controle sobre rede, balanceador e integração de VPC. É a escolha certa quando ninguém vai estudar a rede — e este laboratório existe justamente para estudá-la.
| Decisão | Escolha | Alternativa | Motivo | O que se perde |
|---|---|---|---|---|
| Saída da sub-rede privada | VPC endpoints | NAT Gateway | Nenhum caminho precisa da internet; o gateway do S3 não cobra | Chamada a API externa deixa de funcionar até o NAT entrar |
| Front | Estático no CloudFront | SSR na task | HTML pronto na borda, sem consumir capacidade de aplicação | Renderização por usuário exige voltar à origem |
| Referência de imagem | Digest | Tag como latest | Rollback volta ao artefato exato | Um passo a mais no pipeline para capturar o digest |
| Segredo | Lido em execução | Injetado na task definition | Rotação passa a valer sem novo deploy | Código a mais e uma chamada na inicialização |
| Alvo do health check | Raso | Profundo, tocando o banco | Falha de dependência não remove todas as tasks de uma vez | Task com banco inacessível continua recebendo tráfego e falhando rápido |
| Banco | Multi-AZ | Instância única com snapshot | RTO de minutos em vez de horas | Praticamente o dobro do custo do banco, sem ganho de leitura |
Construir: a rede, e a decisão que define a fatura
A rede é o primeiro passo porque tudo depois mora dentro dela. Duas sub-redes públicas, duas privadas, e a escolha entre NAT Gateway e VPC endpoints — que é onde a maior parte do custo evitável deste laboratório se decide.
# Duas AZs porque o requisito é sobreviver à perda de uma. Duas sub-redes
# públicas (só o ALB mora nelas) e duas privadas (aplicação e banco).
data "aws_availability_zones" "disponiveis" {
state = "available"
}
resource "aws_vpc" "principal" {
cidr_block = "10.20.0.0/16"
enable_dns_hostnames = true # o endpoint do RDS é um nome, não um IP
enable_dns_support = true
tags = local.tags
}
resource "aws_subnet" "publica" {
count = 2
vpc_id = aws_vpc.principal.id
cidr_block = cidrsubnet(aws_vpc.principal.cidr_block, 8, count.index)
availability_zone = data.aws_availability_zones.disponiveis.names[count.index]
map_public_ip_on_launch = false # nada nesta sub-rede precisa de IP automático
tags = merge(local.tags, { Name = "publica-${count.index}" })
}
resource "aws_subnet" "privada" {
count = 2
vpc_id = aws_vpc.principal.id
cidr_block = cidrsubnet(aws_vpc.principal.cidr_block, 8, count.index + 10)
availability_zone = data.aws_availability_zones.disponiveis.names[count.index]
tags = merge(local.tags, { Name = "privada-${count.index}" })
}
# ── A parte que decide a fatura deste laboratório ──────────────────────────
# A task precisa alcançar ECR, S3 (onde as camadas da imagem vivem) e
# CloudWatch Logs. Há dois caminhos:
#
# NAT Gateway → simples, e cobra por hora ligada MAIS por GB processado.
# VPC endpoint → o trafego nao sai da rede da AWS; o gateway do S3 e gratuito.
#
# Aqui vão os endpoints, porque o laboratório não precisa de saída para a
# internet em nenhum outro caminho. Se a sua aplicação chamar uma API externa,
# aí o NAT volta a ser necessário — e a decisão passa a ser consciente.
resource "aws_vpc_endpoint" "s3" {
vpc_id = aws_vpc.principal.id
service_name = "com.amazonaws.${var.regiao}.s3"
vpc_endpoint_type = "Gateway"
route_table_ids = [aws_route_table.privada.id]
tags = local.tags
}
resource "aws_vpc_endpoint" "interface" {
for_each = toset(["ecr.api", "ecr.dkr", "logs", "secretsmanager"])
vpc_id = aws_vpc.principal.id
service_name = "com.amazonaws.${var.regiao}.${each.key}"
vpc_endpoint_type = "Interface"
subnet_ids = aws_subnet.privada[*].id
security_group_ids = [aws_security_group.endpoints.id]
private_dns_enabled = true # sem isto o SDK continua tentando o nome público
tags = local.tags
}O erro que faz a task morrer sem log nenhum
Sem private_dns_enabled no endpoint de interface, o SDK dentro do contêiner continua resolvendo o nome público do ECR, a conexão não completa e a task morre antes de escrever a primeira linha de log. O sintoma é uma task que aparece e desaparece no console, e a causa não está em nenhum log de aplicação porque a aplicação nunca subiu.
Construir: o banco, o segredo e a cadeia de security groups
Três security groups em cadeia, e nenhum deles cita faixa de IP. Essa é a diferença entre uma regra que continua correta em um ano e uma que alguém vai alargar para "a VPC inteira" no primeiro incidente.
# Três security groups em cadeia. Nenhum deles cita faixa de IP da VPC: a
# referência é ao GRUPO de origem, e é isso que faz a regra continuar correta
# quando as sub-redes mudarem.
resource "aws_security_group" "alb" {
name = "alb"
vpc_id = aws_vpc.principal.id
ingress {
description = "HTTPS vindo apenas do CloudFront"
from_port = 443
to_port = 443
protocol = "tcp"
prefix_list_ids = [data.aws_ec2_managed_prefix_list.cloudfront.id]
}
egress {
from_port = 8080
to_port = 8080
protocol = "tcp"
security_groups = [aws_security_group.task.id]
}
tags = local.tags
}
resource "aws_security_group" "task" {
name = "task"
vpc_id = aws_vpc.principal.id
ingress {
description = "so o ALB alcanca a aplicacao"
from_port = 8080
to_port = 8080
protocol = "tcp"
security_groups = [aws_security_group.alb.id]
}
egress {
description = "saida para os endpoints e para o banco"
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = [aws_vpc.principal.cidr_block]
}
tags = local.tags
}
resource "aws_security_group" "banco" {
name = "banco"
vpc_id = aws_vpc.principal.id
ingress {
description = "5432 apenas do security group da task"
from_port = 5432
to_port = 5432
protocol = "tcp"
security_groups = [aws_security_group.task.id]
}
tags = local.tags
}
resource "aws_db_instance" "principal" {
identifier = "app-postgres"
engine = "postgres"
instance_class = var.classe_banco
allocated_storage = 20
max_allocated_storage = 100 # crescimento automatico, com teto declarado
db_name = "appdb"
username = "appadmin"
# O RDS gera a senha, guarda no Secrets Manager e rotaciona. Sem esta linha
# alguem escolhe a senha, e ela vaza para o histórico do Terraform.
manage_master_user_password = true
multi_az = true # requisito: falha de AZ nao para a escrita
storage_encrypted = true
kms_key_id = aws_kms_key.dados.arn
db_subnet_group_name = aws_db_subnet_group.privada.name
vpc_security_group_ids = [aws_security_group.banco.id]
backup_retention_period = 7 # habilita PITR; 0 desliga
deletion_protection = true
skip_final_snapshot = false
final_snapshot_identifier = "app-postgres-final"
performance_insights_enabled = true
tags = local.tags
}A linha manage_master_user_password = true merece atenção: com ela, o RDS gera a senha, guarda no Secrets Manager e rotaciona. Sem ela, alguém escolhe a senha, ela entra no estado do Terraform e passa a existir em texto claro em qualquer lugar onde esse estado esteja.
Construir: a imagem .NET 8
Multi-stage, usuário não-root e a variável de porta que mudou no .NET 8. São três detalhes pequenos e cada um deles aparece em revisão de segurança ou em ticket de contêiner que não sobe.
# Multi-stage: o SDK compila, a imagem final só roda. A diferença de tamanho
# não é vaidade — superfície menor é menos CVE para responder depois.
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src
COPY ["Api/Api.csproj", "Api/"]
RUN dotnet restore "Api/Api.csproj"
COPY . .
RUN dotnet publish "Api/Api.csproj" -c Release -o /app/publish /p:UseAppHost=false /p:PublishReadyToRun=true
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS final
WORKDIR /app
COPY --from=build /app/publish .
# As imagens .NET 8 já trazem um usuário não-root pronto. Rodar como root
# dentro do contêiner é gratuito de evitar e caro de justificar em auditoria.
USER $APP_UID
# .NET 8 trocou ASPNETCORE_URLS por ASPNETCORE_HTTP_PORTS neste cenário.
ENV ASPNETCORE_HTTP_PORTS=8080
EXPOSE 8080
ENTRYPOINT ["dotnet", "Api.dll"]E a aplicação. O trecho abaixo concentra as duas decisões que este laboratório quer ensinar em código: de onde vem a senha, e por que existem dois endpoints de saúde em vez de um.
using Amazon.SecretsManager;
using Amazon.SecretsManager.Model;
using Microsoft.EntityFrameworkCore;
using Npgsql;
using System.Text.Json;
var builder = WebApplication.CreateBuilder(args);
// A senha é lida em EXECUÇÃO, com a task role. Nada de credencial na imagem
// nem no repositório. O RDS rotaciona; ler aqui faz a rotação valer sem
// precisar de um novo deploy.
var segredoArn = builder.Configuration["SEGREDO_BANCO_ARN"]!;
var host = builder.Configuration["BANCO_HOST"]!;
using var sm = new AmazonSecretsManagerClient();
var segredo = await sm.GetSecretValueAsync(new GetSecretValueRequest { SecretId = segredoArn });
var credencial = JsonSerializer.Deserialize<Credencial>(segredo.SecretString)!;
var conexao = new NpgsqlConnectionStringBuilder
{
Host = host,
Database = "appdb",
Username = credencial.username,
Password = credencial.password,
SslMode = SslMode.VerifyFull,
RootCertificate = "/app/rds-ca.pem",
// Pool por processo. O padrão do Npgsql é 100, e o RDS deriva
// max_connections da memória da instância — duas tasks com pool cheio
// podem esgotar o banco antes de a CPU dele subir.
Pooling = true,
MaxPoolSize = 20,
Timeout = 5,
CommandTimeout = 10,
}.ToString();
builder.Services.AddDbContext<AppDb>(o => o.UseNpgsql(conexao));
// Dois endpoints DIFERENTES, e é a distinção que evita apagão:
// /health → o processo está vivo? Não toca em dependência nenhuma.
// /ready → consigo atender? Toca no banco.
// O ALB usa /health. Se ele usasse /ready, uma oscilação do banco tiraria
// TODAS as tasks de rotação ao mesmo tempo, e o balanceador transformaria um
// problema do banco numa indisponibilidade total.
builder.Services.AddHealthChecks()
.AddNpgSql(conexao, name: "banco", tags: ["ready"]);
var app = builder.Build();
app.MapGet("/health", () => Results.Ok(new { status = "ok" }));
app.MapHealthChecks("/ready", new()
{
Predicate = c => c.Tags.Contains("ready"),
});
app.MapGet("/api/itens", async (AppDb db) => await db.Itens.AsNoTracking().Take(50).ToListAsync());
app.Run();
record Credencial(string username, string password);Health check profundo no balanceador transforma oscilação em apagão
Se o alvo do ALB fosse /ready, uma oscilação de 30 segundos no banco marcaria TODAS as tasks como insalubres ao mesmo tempo. O balanceador removeria todas de rotação e a resposta ao cliente passaria de erro intermitente de banco para 503 total — o balanceador amplificaria a falha em vez de contê-la. Health check do balanceador responde "este processo está vivo"; profundidade de dependência é assunto de alarme, não de rotação de tráfego.
Construir: o serviço no ECS e o balanceador
Aqui aparecem as duas roles que mais se confundem, o desired_count que atende ao requisito de AZ, e dois parâmetros que fazem o deploy parecer travado quando ficam no padrão.
# execution role: quem PUXA a imagem e escreve log. É do agente do ECS.
resource "aws_iam_role" "execucao" {
name = "app-execucao"
assume_role_policy = data.aws_iam_policy_document.confia_ecs_tasks.json
}
resource "aws_iam_role_policy_attachment" "execucao_base" {
role = aws_iam_role.execucao.name
policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy"
}
# task role: quem a SUA APLICAÇÃO é. Trocar as duas é o erro mais comum aqui.
resource "aws_iam_role" "task" {
name = "app-task"
assume_role_policy = data.aws_iam_policy_document.confia_ecs_tasks.json
}
# Menor privilégio de verdade: um segredo, uma chave, duas ações.
data "aws_iam_policy_document" "task" {
statement {
actions = ["secretsmanager:GetSecretValue"]
resources = [aws_db_instance.principal.master_user_secret[0].secret_arn]
}
statement {
actions = ["kms:Decrypt"]
resources = [aws_kms_key.dados.arn]
condition {
test = "StringEquals"
variable = "kms:ViaService"
values = ["secretsmanager.${var.regiao}.amazonaws.com"]
}
}
}
resource "aws_ecs_task_definition" "app" {
family = "app"
requires_compatibilities = ["FARGATE"]
network_mode = "awsvpc"
cpu = 512 # combinações de CPU e memória são fixas no Fargate
memory = 1024
execution_role_arn = aws_iam_role.execucao.arn
task_role_arn = aws_iam_role.task.arn
container_definitions = jsonencode([{
name = "api"
image = "${aws_ecr_repository.app.repository_url}@${var.digest_imagem}"
essential = true
portMappings = [{ containerPort = 8080, protocol = "tcp" }]
environment = [
{ name = "ASPNETCORE_HTTP_PORTS", value = "8080" },
{ name = "SEGREDO_BANCO_ARN", value = aws_db_instance.principal.master_user_secret[0].secret_arn },
{ name = "BANCO_HOST", value = aws_db_instance.principal.address },
]
logConfiguration = {
logDriver = "awslogs"
options = {
"awslogs-group" = aws_cloudwatch_log_group.app.name
"awslogs-region" = var.regiao
"awslogs-stream-prefix" = "api"
}
}
# O health check do CONTÊINER pode ser mais fundo que o do balanceador.
healthCheck = {
command = ["CMD-SHELL", "wget -q -O- http://localhost:8080/health || exit 1"]
interval = 15
timeout = 3
retries = 3
startPeriod = 20
}
}])
tags = local.tags
}
resource "aws_ecs_service" "app" {
name = "app"
cluster = aws_ecs_cluster.principal.id
task_definition = aws_ecs_task_definition.app.arn
desired_count = 2 # uma por AZ: perder uma AZ nao zera capacidade
launch_type = "FARGATE"
network_configuration {
subnets = aws_subnet.privada[*].id
security_groups = [aws_security_group.task.id]
assign_public_ip = false # a task nao tem endereco publico
}
load_balancer {
target_group_arn = aws_lb_target_group.app.arn
container_name = "api"
container_port = 8080
}
# Deploy ruim volta sozinho em vez de esperar alguem perceber.
deployment_circuit_breaker {
enable = true
rollback = true
}
# Espera o novo alvo ficar sadio antes de contar como pronto.
health_check_grace_period_seconds = 30
enable_execute_command = true
depends_on = [aws_lb_listener.https]
tags = local.tags
}
resource "aws_lb_target_group" "app" {
name = "app"
port = 8080
protocol = "HTTP"
vpc_id = aws_vpc.principal.id
target_type = "ip" # obrigatorio com awsvpc
health_check {
path = "/health" # raso de propósito: ver a seção de armadilhas
matcher = "200"
interval = 15
timeout = 5
healthy_threshold = 2
unhealthy_threshold = 3
}
# Padrao e 300s. Uma API HTTP curta nao precisa de 5 minutos para drenar, e
# esse valor faz cada deploy parecer travado.
deregistration_delay = 20
tags = local.tags
}Note a policy da task role: um segredo, uma chave, duas ações, e uma condição que restringe o uso da chave ao Secrets Manager. É a forma que o laboratório de menor privilégio vai generalizar — derivar a permissão do uso real em vez de partir do amplo e nunca voltar.
Implantar, e provar que está no ar
# 1. Construir e publicar a imagem, capturando o DIGEST (não a tag).
aws ecr get-login-password --region "$REGIAO" \
| docker login --username AWS --password-stdin "$CONTA.dkr.ecr.$REGIAO.amazonaws.com"
docker build -t app:local .
docker tag app:local "$REPO:$(git rev-parse --short HEAD)"
docker push "$REPO:$(git rev-parse --short HEAD)"
DIGEST=$(aws ecr describe-images --repository-name app \
--image-ids imageTag="$(git rev-parse --short HEAD)" \
--query 'imageDetails[0].imageDigest' --output text)
# 2. Aplicar a infraestrutura amarrada àquele digest exato.
terraform apply -var="digest_imagem=$DIGEST"
# 3. Acompanhar o rollout. O circuit breaker reverte sozinho se falhar.
aws ecs wait services-stable --cluster app --services app
# 4. Provar que está no ar, e que o front e a API respondem.
curl -fsS "https://$DOMINIO/health"
curl -fsS "https://$DOMINIO/api/itens" | head -c 200O que confirma o sucesso não é o Terraform terminar sem erro. É esta sequência: o serviço estabilizar, as duas rotas responderem, e o log do CloudWatch mostrar duas tasks em AZs diferentes atendendo. Se o rollout reverteu, o circuit breaker agiu e a causa está no evento do serviço, não no log da aplicação.
- aws ecs describe-services devolve runningCount igual a desiredCount e nenhum evento de falha recente.
- As duas tasks aparecem em zonas de disponibilidade diferentes — se as duas estão na mesma, a distribuição não está funcionando e a queda de uma AZ ainda derruba tudo.
- O alvo do target group está healthy para as duas.
- A rota de estático responde do cache da borda e a de API chega à origem.
- Nenhum 5xx durante o rollout, verificado no log de acesso do balanceador.
Quebrar de propósito: três falhas e o diagnóstico de cada uma
Laboratório que só testa o caminho felizes ensina metade. As três falhas abaixo são as que aparecem de verdade nesta arquitetura, e cada uma tem um sintoma que aponta para o lugar errado se você não souber o que procurar.
| Falha injetada | Como injetar | Sintoma enganoso | Diagnóstico correto |
|---|---|---|---|
| Banco inalcançável | Remover a regra de 5432 do security group | Aplicação devolve 500 e o ALB mantém as tasks em rotação | O alvo continua sadio porque o health check é raso — de propósito. O sinal está no alarme de erro, não na rotação |
| Pool esgotado | Baixar MaxPoolSize para 2 e gerar carga | Timeout que parece lentidão de banco, com CPU do RDS baixa | dbPoolWaitMs alto com CPU de banco baixa é dimensionamento de pool, não capacidade de banco |
| Imagem inacessível | Apagar um dos endpoints de interface do ECR | Task aparece e desaparece sem nenhum log de aplicação | A causa está no evento de paragem da task e no motivo do stopped, não em log — a aplicação nunca subiu |
| Queda de AZ | Parar as tasks de uma zona e observar | Latência sobe e a capacidade cai pela metade por alguns segundos | Prova que desired_count de 2 é o mínimo, não o confortável: durante a recomposição você opera com uma task |
| Deploy ruim | Publicar imagem que falha no start | O serviço parece travado em "in progress" | O circuit breaker reverte e registra no evento do serviço; sem ele, o serviço tentaria indefinidamente |
A task do seu serviço está em sub-rede privada, sem NAT Gateway. Ela aparece no console, desaparece em segundos e não escreve nenhuma linha de log da aplicação. Qual é a causa MAIS provável?
Segurança: o risco, o controle e a detecção
Responsabilidade compartilhada, aplicada a este desenho: a AWS responde pelo hipervisor, pelo patch do sistema operacional do Fargate e pelo motor do RDS. Você responde por tudo na tabela abaixo — e a linha mais provável não é ataque externo, é permissão ampla que alguém deixou passar.
| Risco | Probabilidade | Impacto | Prevenção | Detecção | Resposta |
|---|---|---|---|---|---|
| Credencial no repositório | Alta | Alto | Senha gerenciada pelo RDS; nenhuma no código | Varredura de segredo no CI | Rotacionar pelo Secrets Manager e auditar uso no CloudTrail |
| Policy ampla na task role | Alta | Alto | Duas ações e recurso específico | IAM Access Analyzer sobre uso real | Reduzir para o que foi usado em 30 dias e reaplicar |
| Banco exposto | Média | Crítico | Sub-rede privada e SG por grupo de origem | AWS Config sobre acesso público | Remover a regra e verificar log de conexão |
| Dado pessoal em log | Média | Alto | Log estruturado com campo negado | Consulta periódica por padrão de documento | Expurgar o stream e corrigir o serializador |
| Imagem com CVE crítica | Média | Médio | Base slim e reconstrução periódica | Scan on push no ECR | Reconstruir e implantar por digest novo |
| Tráfego automatizado | Alta | Médio | Regra gerenciada e limite de taxa no WAF | Métrica de bloqueio do WAF | Endurecer a regra para o padrão observado |
| Exclusão acidental do banco | Baixa | Crítico | deletion_protection e snapshot final | CloudTrail sobre DeleteDBInstance | Restaurar por PITR e revisar quem tinha a permissão |
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "LerApenasOSegredoDesteBanco",
"Effect": "Allow",
"Action": "secretsmanager:GetSecretValue",
"Resource": "arn:aws:secretsmanager:us-east-1:111122223333:secret:rds!db-9c1f-*"
},
{
"Sid": "DecifrarApenasViaSecretsManager",
"Effect": "Allow",
"Action": "kms:Decrypt",
"Resource": "arn:aws:kms:us-east-1:111122223333:key/8b2e",
"Condition": {
"StringEquals": { "kms:ViaService": "secretsmanager.us-east-1.amazonaws.com" }
}
}
]
}Nenhum * em Resource. A condição em kms:ViaService é o detalhe que separa permissão apertada de permissão que parece apertada: sem ela, a task poderia decifrar qualquer coisa cifrada com aquela chave, e não apenas o segredo do banco.
Observabilidade: as perguntas que o painel tem de responder
Painel se justifica por pergunta respondida, não por gráfico exibido. Se um gráfico não responde a nenhuma das perguntas abaixo, ele está ocupando espaço que alguém vai precisar às três da manhã.
- A aplicação está no ar? Contagem de alvos sadios por AZ, não média.
- Está lenta? Latência p95 do balanceador separada da latência da aplicação — a diferença entre as duas é rede e fila.
- O erro é nosso ou do cliente? 5xx e 4xx em séries separadas; juntá-las esconde regressão atrás de erro de integração.
- O banco é o gargalo? Conexões ativas e espera de pool ao lado da CPU do RDS.
- Estamos perto de escalar? Utilização de CPU e memória das tasks contra o alvo da política.
- O custo mudou de patamar? Custo diário por tag, com a linha da semana anterior.
| Alarme | Limiar inicial | Por que este limiar | Ação de plantão |
|---|---|---|---|
| Alvos sadios abaixo de 2 | Por 2 minutos | Abaixo de 2 já não há tolerância a falha de AZ | Investigar antes de o segundo cair |
| 5xx acima de 1% das requisições | 5 minutos | Abaixo disso é ruído de borda e de cliente | Correlacionar por traceId no X-Ray |
| p95 acima de 400 ms | 10 minutos | É o requisito declarado; alarmar no requisito e não em número redondo | Ver espera de pool antes de olhar o banco |
| Espera de pool acima de 100 ms | 5 minutos | É o sintoma que precede o timeout | Avaliar MaxPoolSize antes de escalar task |
| Falha de failover do RDS | Evento único | Evento raro; qualquer ocorrência é notícia | Confirmar promoção e medir o tempo real |
| Custo diário 30% acima da média | Diário | Pega recurso esquecido no dia seguinte, não no fim do mês | Comparar por tag e procurar o que subiu |
Escala: 10, 10 mil, 1 milhão
A mesma arquitetura em três ordens de grandeza. O que muda primeiro não é a aplicação: é conexão de banco, e depois a fronteira entre leitura e escrita.
| Ordem de grandeza | O que aguenta sem mudar | O que quebra primeiro | O que fazer |
|---|---|---|---|
| 10 usuários | Tudo, com folga grande | Nada; o custo por usuário é absurdo | Reduzir a classe do banco e considerar AZ única em ambiente de estudo |
| 400 usuários (este caso) | A arquitetura como desenhada | Deploy sem cuidado de drenagem | É o ponto de operação alvo; medir antes de mexer |
| 10 mil usuários | ALB e Fargate, escalando horizontalmente | Conexões do banco, muito antes da CPU dele | Pool por task, cache de leitura e réplica de leitura para o que só lê |
| 1 milhão de requisições por hora | A borda absorve o estático | Escrita concentrada na primária | Separar leitura em réplica, extrair escrita assíncrona para fila |
| Pico repentino de 10× | CloudFront e WAF na frente | Tempo de partida de task nova, que não é instantâneo | Escalar por métrica antecedente e manter capacidade mínima acima do vale |
| Falha de uma AZ | Metade da capacidade e o failover do banco | A capacidade restante, se desired_count era 2 | Mínimo de 4 tasks quando perder metade não pode doer |
A quota que ninguém confere antes do pico
Antes de contar com escala horizontal, confira em Service Quotas o limite de tasks do Fargate por região na sua conta e o limite de conexões que a classe do seu RDS permite. O segundo deriva da memória da instância, e não é um número redondo. Descobrir isso durante o pico é a forma mais cara de aprender que quota existe.
Custo: onde o dinheiro vai nesta arquitetura
Duas dimensões explicam quase toda a fatura deste desenho, e nenhuma delas é requisição: hora ligada e byte que atravessa fronteira. ALB, NAT, Multi-AZ e endpoint de interface cobram parados. Tráfego entre AZs cobra por GB. É por isso que um ambiente de homologação esquecido ligado custa quase o mesmo que produção com uso baixo.
| Cenário | Volume | Componentes que dominam | Tendência | Otimização mais eficaz |
|---|---|---|---|---|
| Protótipo | 10 usuários, horário comercial | ALB e banco, ambos por hora ligada | Quase fixa, independente de uso | AZ única, sem Multi-AZ, e desligar fora do horário |
| Produção pequena (este caso) | 400 usuários, 12 mil requisições no pico | Multi-AZ do banco, ALB, endpoints de interface | Fixa com pequena parcela variável | Trocar NAT por endpoints, dimensionar a classe do banco pelo uso medido |
| Alta escala | Milhões de requisições por hora | Fargate, tráfego entre AZs, saída de dados | Variável e dominante | Cache na borda, Savings Plans para a base, Spot para o assíncrono |
Não decore preço; entenda a dimensão
Valor por hora e por GB muda, e número decorado envelhece antes de você usar. O que não muda é a estrutura: o que cobra por existir, o que cobra por atravessar fronteira e o que cobra por uso. Calcule o seu caso no AWS Pricing Calculator com as dimensões acima e confirme a cobrança real no Cost Explorer no terceiro dia, com a tag que você aplicou em todo recurso.
Well-Architected nos seis pilares
Revisão honesta do que este laboratório entrega. As linhas de prioridade alta são dívida real, e cada uma tem um laboratório da série que a resolve — porque apontar risco sem apontar o caminho é auditoria, não ensino.
| Pilar | Situação após o laboratório | Risco que permanece | Melhoria | Prioridade |
|---|---|---|---|---|
| Excelência operacional | Infraestrutura em Terraform, deploy por digest, rollback automático | Deploy ainda parte da máquina de alguém | Pipeline com credencial federada | Alta |
| Segurança | Sub-rede privada, SG por origem, segredo rotacionado, task role de duas ações | Sem detecção contínua nem revisão de permissão efetiva | GuardDuty, Config e Access Analyzer | Alta |
| Confiabilidade | Duas AZs, Multi-AZ, circuit breaker, PITR habilitado | RTO não foi ensaiado, e não há DR regional | Ensaio de restauração cronometrado e DR | Alta |
| Eficiência de performance | Cache na borda, pool dimensionado, escala por métrica | Nenhum teste de carga; o gargalo é hipótese | Curva de carga até a quebra | Média |
| Otimização de custos | Tag em tudo, endpoints em vez de NAT, budget com alerta | Sem rightsizing medido e sem compromisso de capacidade | Compute Optimizer antes de Savings Plans | Média |
| Sustentabilidade | Fargate dimensionado ao uso, sem instância ociosa | Ambiente de estudo ligado fora do horário | Agendar redução para zero à noite | Baixa |
Evolução em níveis: o que muda, e o que passa a doer
Uma task, banco em AZ única, sem borda. Serve para aprender e para validar a ideia.Domínio próprio, TLS, segredo no cofre, log estruturado. É o mínimo defensável com usuário real.Duas AZs, Multi-AZ, alarme, PITR, rollback automático. É onde este laboratório termina.Réplica de leitura, cache, escrita assíncrona em fila, escala por métrica antecedente.Conta por ambiente, SCP, pipeline com aprovação, DR multi-região, FinOps com rateio.Extração para o lake, e assistente sobre o acervo de pedidos quando houver pergunta em linguagem natural para responder.Onde IA entra nesta arquitetura, e onde não entra
Aqui, IA não resolve nada — e dizer isso é parte do conteúdo
Este laboratório trata de onde o recurso mora e de quem alcança quem. Não existe problema de linguagem, de classificação nem de extração no caminho, e nenhuma decisão do desenho melhora com um modelo. Prever tráfego para escalar seria o exemplo clássico de IA decorativa: o CloudWatch já escala por métrica observada, com latência de segundos e custo próximo de zero, enquanto um modelo acrescentaria treino, deriva e uma nova dependência para operar. IA entra nesta série quando existe um problema que regra não resolve — busca em linguagem natural sobre o acervo, extração de dado preso em documento, assistente com ferramenta —, e ela entra depois de dado, segurança e observabilidade porque é sobre essas três que ela se sustenta em produção.
Anti-padrões desta arquitetura
| Anti-padrão | Por que alguém faz | Sintoma em produção | Forma correta | Quando é aceitável |
|---|---|---|---|---|
| Banco com acesso público | Conectar o cliente SQL do laptop direto | Tentativa de login de origem desconhecida no log | Sub-rede privada e túnel por SSM | Nunca em produção; em estudo, por horas contadas |
| SG com 0.0.0.0/0 na 5432 | Destravar depressa durante depuração | Funciona, e ninguém volta atrás | Referenciar o SG de origem | Nunca; use SG temporário e apague |
| Tag latest na task definition | Menos um passo no deploy | Rollback traz uma imagem diferente da esperada | Referenciar por digest | Em ambiente descartável de estudo |
| Health check tocando o banco | Parece mais correto verificar tudo | Oscilação de banco vira 503 total | Raso no balanceador, profundo no alarme | Quando task sem banco é inútil E há capacidade de sobra |
| Uma task só, com Multi-AZ no banco | Economizar metade da computação | Todo deploy e toda falha de task é indisponibilidade | Mínimo de duas, uma por AZ | Ambiente interno com janela de indisponibilidade acordada |
| Senha em variável de ambiente do Terraform | É o caminho mais curto | A senha vive no estado e nunca é rotacionada | Senha gerenciada pelo RDS | Nunca; o custo de fazer certo aqui é uma linha |
| NAT Gateway sem precisar | É o que todo tutorial mostra | Linha fixa na fatura por tráfego que não sairia | VPC endpoints para os serviços usados | Quando há chamada real a API externa |
Quando algo não funciona
| Sintoma | Causa provável | Como investigar | Onde olhar | Correção |
|---|---|---|---|---|
| Task inicia e para, sem log | Não alcançou ECR, S3 ou Logs | Ler o motivo de paragem da task, não o log | stoppedReason em describe-tasks | Criar os endpoints faltantes com DNS privado, ou NAT |
| ALB devolve 503 | Nenhum alvo sadio | Conferir estado do alvo antes de olhar a aplicação | describe-target-health | Ver se a porta do health check e a do contêiner coincidem |
| ALB devolve 502 | Aplicação fechou a conexão ou respondeu inválido | Comparar horário do 502 com o log da aplicação | Log de acesso do ALB e log da task | Conferir a porta em ASPNETCORE_HTTP_PORTS e o protocolo do target group |
| Timeout ao conectar no banco | SG, sub-rede ou pool | Descartar rede antes de culpar o banco | Espera de pool no log, CPU do RDS | Se a espera de pool é alta e a CPU baixa, ajuste o pool |
| Erro de TLS ao conectar no RDS | Certificado da AWS ausente na imagem | Testar com SslMode reduzido, em ambiente de estudo apenas | Exceção do Npgsql | Embutir o pacote de certificados do RDS e manter VerifyFull |
| Deploy fica em progresso e reverte | Novo alvo nunca ficou sadio | Ler os eventos do serviço em ordem | Eventos do serviço ECS | Aumentar o período de graça ou corrigir o start da aplicação |
| Segredo não encontrado | ARN de outro segredo, ou falta o sufixo | Conferir o ARN resolvido em execução | CloudTrail em GetSecretValue | O ARN do segredo gerenciado termina em sufixo; use o do atributo do recurso |
| Terraform recusa destruir o banco | Proteção de deleção ligada | Ler a mensagem inteira, que nomeia o recurso | Saída do destroy | Desligar a proteção em um apply antes de destruir |
Limpeza: o que o destroy não leva
Esta seção não é formalidade. Laboratório de arquitetura deixa recurso cobrando depois que a aula termina, e o Terraform não remove o que ele foi instruído a preservar.
# A ordem importa: o Terraform recusa remover o banco com proteção ligada, e
# recusa remover a VPC enquanto houver endpoint ou ENI de task pendurada.
terraform apply -var="digest_imagem=$DIGEST" \
-var="protecao_delecao=false" -var="desired_count=0"
aws ecs wait services-inactive --cluster app --services app || true
terraform destroy -var="digest_imagem=$DIGEST" -var="protecao_delecao=false"
# ── O que o destroy NÃO leva, e continua cobrando ──────────────────────────
aws rds describe-db-snapshots --snapshot-type manual \
--query 'DBSnapshots[].DBSnapshotIdentifier' # snapshot final fica
aws logs describe-log-groups \
--query 'logGroups[?retentionInDays==null].logGroupName' # log sem retenção
aws ecr list-images --repository-name app --query 'imageIds[].imageDigest'
aws ec2 describe-addresses --query 'Addresses[?AssociationId==null]' # EIP órfão
aws kms describe-key --key-id "$CHAVE" --query 'KeyMetadata.KeyState' # em espera
# ── Endpoint de VPC — o que este laboratório cria em vez do NAT ──
#
# O de GATEWAY (S3, DynamoDB) não cobra por hora: ele é uma rota, e some com a VPC.
# O de INTERFACE (ECR, Secrets Manager, CloudWatch Logs) cobra POR HORA E POR AZ,
# e é o que precisa ser conferido. O destroy leva os declarados aqui; um criado à
# mão numa tentativa anterior continua cobrando.
aws ec2 describe-vpc-endpoints \
--query 'VpcEndpoints[*].[VpcEndpointId,ServiceName,VpcEndpointType,State]' \
--output table
# aws ec2 delete-vpc-endpoints --vpc-endpoint-ids <id>O que continua na fatura depois de um destroy bem-sucedido
Snapshot final do RDS, que a própria configuração pediu para criar. Log group sem retenção definida, que guarda para sempre. Imagens no ECR, cobradas por armazenamento. Elastic IP não associado, que cobra justamente por estar parado. Chave do KMS em espera de exclusão, que cobra durante toda a janela. E, se você criou o bucket do estado do Terraform à mão, ele fica. Rode os comandos acima e confira a fatura no dia seguinte: é o único jeito de saber. E confira os endpoints de VPC: o de interface (ECR, Secrets Manager) cobra por hora e por zona de disponibilidade, então três endpoints em duas AZs são seis cobranças por hora — a mesma ordem de grandeza do NAT Gateway que eles substituem.
Resumo: problema, serviço e motivo
| Problema | Serviço | Por que este e não outro |
|---|---|---|
| Rodar a aplicação sem operar servidor | ECS Fargate | Mantém o modelo de contêiner e remove patch e capacidade de cluster |
| Guardar estado relacional com backup | RDS PostgreSQL | PITR, failover e rotação de senha sem script próprio |
| Receber tráfego e distribuir | Application Load Balancer | Roteamento por caminho e health check por alvo, que o NLB não faz |
| Servir estático e encerrar TLS perto do usuário | CloudFront | Tira do contêiner o trabalho que não precisa de contêiner |
| Barrar tráfego automatizado | AWS WAF | Bloquear é mais barato que escalar para aguentar |
| Guardar a senha e rotacioná-la | Secrets Manager | Rotação gerenciada, que o Parameter Store não oferece |
| Cifrar dado em repouso com chave própria | KMS | Permite revogar acesso ao dado sem apagar o dado |
| Alcançar serviço regional sem sair da rede | VPC endpoints | Remove o NAT e a cobrança por GB de um tráfego que não precisava sair |
| Diagnosticar em minutos | CloudWatch e X-Ray | Log, métrica e trace com o mesmo id de correlação |
Desafio — sem roteiro
O requisito
O time de relatórios quer rodar consultas pesadas sobre os mesmos dados, sem competir por conexão com a aplicação de produção. Adicione uma read replica do RDS e aponte um segundo endpoint da aplicação — só leitura — para ela.
Critério de aceite — executável, não "verifique se funciona"
Com a aplicação de produção sob carga simulada (um script disparando escrita continuamente), a consulta de relatório na replica continua respondendo em menos de 200ms. Comando: medir a latência da rota de relatório com `curl -w "%{time_total}"` durante a carga.
- Dica 1: A replica se cria com `aws_db_instance` apontando `replicate_source_db` para a instância primária — não precisa de outro `aws_db_subnet_group`.
- Dica 2: A aplicação precisa de uma SEGUNDA connection string, não de trocar a existente — a rota de escrita continua na primária.
- Dica 3: A replica está sempre um pouco atrás da primária (replicação assíncrona) — se o teste ler um dado escrito há menos de 1 segundo, ele pode não estar lá ainda. Isso não é bug, é o trade-off que o requisito aceitou.
Custo estimado do que este desafio acrescenta
Uma segunda instância RDS do mesmo tamanho da primária — dobra o custo de compute do banco enquanto o desafio estiver de pé.
Lembrete de limpeza
Todo recurso que este desafio criar entra no mesmo `terraform destroy` do laboratório — nada fica para trás cobrando sozinho.
Perguntas frequentes
❓ Preciso de NAT Gateway para a task no Fargate puxar imagem do ECR?
❓ Onde a senha do banco deve ficar numa aplicação em ECS Fargate?
❓ O health check do ALB deve consultar o banco de dados?
❓ RDS Multi-AZ aumenta a capacidade de leitura da aplicação?
❓ Preciso de CloudFront na frente do ALB ou o ALB basta?
❓ Quantas tasks o serviço precisa para sobreviver à queda de uma AZ?
❓ O que o terraform destroy deixa cobrando neste laboratório?
Fixando
Sua equipe configurou o health check do target group para uma rota que executa SELECT 1 no PostgreSQL. O RDS oscila por 40 segundos. O que os clientes observam?
O time quer reduzir a fatura sem degradar o requisito de "perder uma AZ não para a operação". Qual mudança atende ao objetivo SEM violar o requisito?
Próximo passo — Laboratório 02
Você tem a aplicação no ar e a rede funcionando, mas provavelmente aceitou o plano de endereçamento sem discutir. O laboratório 02 abre a VPC: por que o banco não tem rota para a internet, o que diferencia sub-rede pública de privada além do nome, e quando NAT Gateway volta a ser necessário. É o pré-requisito dos laboratórios de rede privada e de rede híbrida. Última validação técnica deste módulo: 06/ago/2026, com os nomes de serviço e os atributos de Terraform conferidos na documentação oficial da AWS.
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…