Lab 02 — A rede por baixo: por que o banco não tem rota para a internet
O laboratório 01 colocou a aplicação no ar. Este responde a pergunta que ficou de fora: por que o banco de dados não pode ter endereço na internet — e por que fechar a porta no security group não é a mesma coisa que não haver caminho.
O problema, e a empresa que o tem
Uma equipe de quatro pessoas subiu a API do laboratório anterior numa tarde. Para rodar as migrations, alguém marcou o banco como publicamente acessível e liberou a porta 5432 para o próprio IP. Funcionou, e ficou.
Três meses depois há 40 clientes pagantes, o IP de casa de quem configurou mudou duas vezes, e a regra do security group foi alargada para 0.0.0.0/0 numa sexta-feira em que a migration precisava subir. Ninguém desfez. O banco de produção está alcançável da internet e a única coisa entre ele e o mundo é uma linha de configuração que já foi alargada uma vez.
O que acontece se nada for feito
Não é "um dia alguém invade". É que a auditoria do segundo contrato pede o desenho da rede, e a resposta honesta reprova. A correção sob prazo de auditoria é feita em produção, com o time inteiro parado — e reconstruir rede com banco em pé exige janela, snapshot e ensaio, que é exatamente o que este laboratório permite fazer antes.
O que você vai conseguir fazer
- Explicar, sem consultar nada, por que uma sub-rede é pública ou privada — e apontar a linha exata que decide.
- Escrever o plano de endereçamento de uma VPC de duas AZs e justificar cada máscara escolhida.
- Distinguir Internet Gateway de NAT Gateway pelo comportamento, não pelo nome.
- Provar por medição que o banco deixou de ser alcançável de fora, sem depender do security group.
- Dizer quanto o NAT custa na sua conta e qual das duas dimensões domina.
- Diagnosticar os três sintomas clássicos: task que não baixa imagem, NAT na sub-rede errada e sub-rede sem associação de tabela.
- Decidir entre um NAT e um por AZ com o critério de custo contra domínio de falha.
Entregável ao final
Uma VPC de duas AZs com plano de endereçamento documentado, quatro sub-redes com tabelas de rotas explícitas, o banco sem endereço público, e uma medição que prova a inalcançabilidade — tudo em Terraform, com o `destroy` conferido.
O que a certificação cobra disto
| Conceito | Certificação | Como aparece no projeto | O que dominar |
|---|---|---|---|
| Sub-rede pública vs privada | SAA-C03 · CLF-C02 | duas tabelas de rotas, uma com IGW e outra com NAT | que a diferença é o destino da rota padrão, não um atributo da sub-rede |
| Internet Gateway | SAA-C03 | única entrada do desenho, servindo as duas AZs | que ele é bidirecional e que um recurso sem IP público não é alcançável mesmo com rota |
| NAT Gateway | SAA-C03 · SOA-C02 | saída da sub-rede privada, na pública, com Elastic IP | que ele é só de saída, mora do lado público, e é zonal — não regional |
| Plano de endereçamento (CIDR) | SAA-C03 · SAP-C03 | /16 na VPC e /20 por sub-rede, com espaço reservado | por que sobredimensionar sub-rede é mais barato que redimensionar depois |
| AZ como domínio de falha | SAA-C03 | sub-redes pareadas por AZ e a decisão de um NAT por AZ | que recurso zonal replicado é o que sustenta disponibilidade, e o custo disso |
| Custo de transferência de dados | SAP-C03 · FinOps | as duas dimensões de cobrança do NAT | que tráfego entre AZs cobra, e que o endpoint do L44 elimina parte da conta |
A questão que este desenho responde
A forma mais comum é: "uma instância em sub-rede privada precisa baixar atualizações; qual a solução mais adequada?". A resposta é NAT Gateway na sub-rede pública. O distrator que mais funciona é "Internet Gateway na sub-rede privada" — e quem entendeu que o IGW dá caminho de entrada sabe por que isso mudaria a natureza da sub-rede.
Requisitos, e como cada um muda o desenho
| Requisito | Tipo | O que ele acrescenta ao desenho |
|---|---|---|
| Banco não alcançável publicamente | conformidade | sub-rede sem rota para o IGW e `publicly_accessible = false` — duas condições, não uma |
| Aplicação baixa imagem e lê segredo | funcional | NAT Gateway, porque a sub-rede privada não tem saída própria |
| 99,9% no horário comercial | disponibilidade | sub-redes em duas AZs e a pergunta de um NAT por AZ ou compartilhado |
| Até 150 dólares por mês | custo | um NAT em vez de dois, com a dependência de AZ escrita e aceita |
| Auditoria pede o desenho | conformidade | plano de endereçamento documentado e tabelas de rotas explícitas, não a principal |
| Dev roda migration | operacional | caminho alternativo pela sessão do SSM em vez de rota pública — senão a rota volta |
O último requisito é o que decide se a correção sobrevive. Fechar a rota sem dar ao time um jeito de alcançar o banco garante que alguém a reabra na próxima urgência. É o mesmo raciocínio de qualquer controle: controle que impede o trabalho é controle que se desliga.
Arquitetura mínima: o desenho que "funcionou"
Este primeiro diagrama não é uma etapa a construir — é o estado atual, desenhado para que o defeito fique visível. Ele funciona, atende os 40 clientes e passaria por qualquer teste funcional.
- → psql pelo IP público
- → tentativa em 5432
- → entrada roteada pela tabela
- → alcança a task
- → alcança o banco — o defeito
- → consulta SQL na porta 5432
- Fora da AWS
- Rede e entrega
- Compute
- Banco de dados
Tudo numa sub-rede pública, tudo com IP público. Funciona, e o banco fica alcançável da internet — a diferença entre "não expus a porta" e "não há rota" é o que este laboratório constrói. Percorra os passos: o defeito não está numa configuração errada, está na ausência de uma.
- Uma tabela de rotas para tudo. Uma VPC nova vem com uma tabela de rotas principal, e toda sub-rede que não tenha associação explícita usa ela. Acrescentar 0.0.0.0/0 → IGW nessa tabela torna TODAS as sub-redes públicas de uma vez. Ninguém decidiu isso; é o efeito de não decidir.
- O IGW é bidirecional, e é isso que muda. Ele não é "a saída para a internet". Ele é o caminho nos dois sentidos: recurso com IP público numa sub-rede roteada para o IGW é alcançável DE FORA. É a propriedade que a próxima arquitetura remove.
- O banco recebe DNS público. `publicly_accessible = true` faz o RDS resolver para um IP público. A porta continua protegida pelo security group — e um security group errado é uma linha de Terraform de distância, enquanto a ausência de rota exige reconstruir a rede.
- A varredura chega antes do primeiro cliente. Endereços públicos da AWS são varridos continuamente. O que separa o banco de uma tentativa de autenticação é o security group, e é só ele. Defesa em uma camada é defesa que uma linha errada desliga.
- O hábito do dev fixa o desenho. Rodar migration da máquina local exige o banco alcançável. Enquanto esse for o fluxo de trabalho, ninguém vai fechar a rota — e é por isso que a correção começa por dar um caminho melhor ao dev, não por bloquear.
O defeito não é uma configuração errada
Nada aqui está mal configurado. A tabela de rotas principal faz o que faz por padrão, o IGW faz o que promete, e o security group está fazendo seu trabalho. O defeito é a AUSÊNCIA de uma segunda camada de sub-rede — e ausência não aparece em revisão de código, porque não há linha para revisar.
Arquitetura para produção
A evolução não acrescenta uma caixa: ela parte a rede em duas camadas e cria uma segunda tabela de rotas. A topologia muda, e é a mudança de topologia que remove a propriedade indesejada.
- → HTTPS na porta 443
- → entrada roteada para a sub-rede pública
- → alcança o balanceador
- → encaminha para o IP privado da task
- → consulta SQL, tráfego interno à VPC
- → saída consulta a tabela privada
- → 0.0.0.0/0 aponta para o NAT
- → traduz para o endereço fixo de saída
- → pull da imagem, por GB processado
- → leitura do segredo
- Fora da AWS
- Rede e entrega
- Compute
- Banco de dados
- Segurança e identidade
A sub-rede privada é privada porque a tabela dela NÃO tem rota para o IGW — e é só isso. O NAT existe para o tráfego de SAÍDA que a aplicação precisa (imagem, atualização, segredo) e não devolve caminho de entrada. Percorra os passos e repare que o banco deixa de ter endereço alcançável.
- A entrada tem um único ponto. Só o ALB vive numa sub-rede com rota para o IGW, e só ele tem endereço público. Reduzir a superfície de entrada a uma peça é o que torna a auditoria possível: existe um lugar para olhar.
- Duas tabelas, e a diferença é uma linha. A pública tem 0.0.0.0/0 → IGW. A privada tem 0.0.0.0/0 → NAT. Nada mais distingue as duas sub-redes: mesmo tipo de recurso, mesma VPC, mesma AZ. É a pergunta de prova que mais gente erra, e a resposta é literalmente qual destino a linha aponta.
- O NAT mora do lado público. Ele precisa de rota para o IGW para funcionar, então vive na sub-rede pública e é referenciado pela tabela da privada. Colocá-lo na privada produz um recurso que sobe, cobra e não roteia nada — falha silenciosa, sem erro de criação.
- O NAT é de saída, e só. Ele traduz endereço de dentro para fora e mantém a tabela de conexões para devolver a resposta. Não há como iniciar conexão de fora através dele. É a propriedade que o IGW não tem, e a razão de existirem dois recursos diferentes em vez de um.
- O banco perde o endereço, não só a porta. Com `publicly_accessible = false` e sem rota de entrada, não existe pacote da internet que chegue ao RDS — independentemente do security group. Duas camadas erradas seriam necessárias para expô-lo, em vez de uma.
- A conta do NAT é por hora E por GB. Ele cobra por hora ligado e por gigabyte processado, nas duas direções. Um NAT por AZ dobra a parte fixa e elimina o tráfego entre AZs; um NAT só reduz a parte fixa e cria dependência de uma AZ. É a decisão de custo deste laboratório, e o L44 mostra como não pagar por parte disso.
65.536 endereços4.096 endereços4.096 endereços4.096 endereços4.096 endereçosde 10.0.64.0 em dianteCinco endereços por sub-rede não são seus
A AWS reserva os cinco primeiros de cada sub-rede: o de rede, o do roteador, o do DNS, um reservado para uso futuro e o de broadcast. Numa sub-rede /28 você tem 11 endereços úteis, não 16 — e é por isso que sub-rede apertada esgota antes do que a conta de cabeça sugere.
O caminho de um pacote, ponta a ponta
O passo que mais ensina é o último. Não há regra bloqueando a entrada — há ausência de caminho. Um security group mal escrito não muda esse fato, e é justamente essa independência entre as camadas que o desenho compra.
{
"version": 2,
"account-id": "111122223333",
"interface-id": "eni-0abc1234def567890",
"srcaddr": "203.0.113.47",
"dstaddr": "10.0.32.14",
"srcport": 54321,
"dstport": 5432,
"protocol": 6,
"packets": 1,
"bytes": 44,
"start": 1754524800,
"end": 1754524860,
"action": "REJECT",
"log-status": "OK"
}Repare no `dstaddr`: 10.0.32.14 é endereço privado. Um registro assim aparece quando algo DENTRO da VPC tenta alcançar o banco sem permissão — nunca da internet, porque de fora esse endereço não é roteável. Ver `REJECT` com origem pública em porta de banco é o sinal de que a sub-rede não é o que se pensa.
As decisões, e o que se perde em cada uma
📋 Quantos NAT Gateways, com orçamento de 150 dólares por mês e requisito de 99,9% em horário comercial
A parte fixa do NAT é o item mais caro deste desenho, e dobrá-la para cobrir uma falha de AZ que o requisito de 99,9% em horário comercial tolera é comprar disponibilidade que o contrato não pede. A decisão vem do requisito, não do que é tecnicamente melhor — e ela está escrita, para ser revisada quando o requisito mudar.
Alt: Um NAT por AZ — dobra a parte fixa e é o desenho correto acima de 99,9% ou fora de horário comercial
Alt: Instância NAT em EC2 — mais barata na fatura e mais caro no total: você passa a operar patch, monitoramento e failover
Alt: Sem NAT, com endpoint de VPC — elimina a conta para S3 e ECR mas não cobre tráfego arbitrário de saída; é o L44
Alt: Sem NAT, task em sub-rede pública — devolve o IP público à aplicação e desfaz o motivo deste laboratório
| Decisão | Escolha | Alternativas | Motivo | O que se perde |
|---|---|---|---|---|
| Tabelas de rotas | Explícitas, uma por camada | Usar a principal | sub-rede sem associação herda a principal em silêncio | duas linhas de Terraform a mais por sub-rede |
| Máscara das sub-redes | /20 | /24 por sub-rede | espaço não custa; redimensionar exige recriar | a tabela de endereços fica menos compacta de ler |
| Sub-rede do banco | A mesma privada da aplicação | Uma terceira camada isolada | a separação extra só ajuda com regras de rede entre camadas, que este desenho ainda não tem | a segmentação por camada fica para quando houver mais de um consumidor |
| Acesso do dev ao banco | Sessão do SSM pela task | VPN ou bastion host | não acrescenta recurso nem endereço público, e o rastro fica no CloudTrail | exige o agente e a permissão configurados, e é mais lento que psql direto |
Construir: a rede, com as duas camadas explícitas
A árvore do projeto continua a do laboratório 01. O arquivo de rede é o que muda por inteiro.
lab02-rede/
├── rede.tf # VPC, sub-redes, IGW, NAT, tabelas de rotas
├── banco.tf # RDS sem endereço público
├── ecs.tf # serviço nas sub-redes privadas
├── variaveis.tf # região, CIDR base, quantidade de NAT
├── saidas.tf # ids que os outros arquivos e os testes consomem
└── README.md # o plano de endereçamento, por escrito# Plano de endereçamento: /16 na VPC, /20 por sub-rede, /18 reservado.
# A escolha está no README porque quem lê o Terraform daqui a um ano precisa saber
# o que NÃO usar, e isso não está em nenhuma linha de código.
locals {
azs = ["${var.regiao}a", "${var.regiao}b"]
# Índices fixos, não calculados a partir de `count`. Reordenar a lista de AZs
# com CIDR derivado do índice REATRIBUI sub-rede existente — o plano do
# Terraform mostra destroy/create de sub-rede com recurso dentro.
publicas = {
"${var.regiao}a" = "10.0.0.0/20"
"${var.regiao}b" = "10.0.16.0/20"
}
privadas = {
"${var.regiao}a" = "10.0.32.0/20"
"${var.regiao}b" = "10.0.48.0/20"
}
}
resource "aws_vpc" "principal" {
cidr_block = "10.0.0.0/16"
# Os dois são necessários para o nome DNS do RDS resolver dentro da VPC.
# Sem `enable_dns_hostnames`, a aplicação recebe o endpoint e não resolve —
# e o sintoma é timeout de conexão, que manda todo mundo olhar security group.
enable_dns_support = true
enable_dns_hostnames = true
tags = { Name = "lab02", Projeto = "lab02" }
}
resource "aws_internet_gateway" "igw" {
vpc_id = aws_vpc.principal.id
tags = { Name = "lab02-igw" }
}
resource "aws_subnet" "publica" {
for_each = local.publicas
vpc_id = aws_vpc.principal.id
cidr_block = each.value
availability_zone = each.key
# `false` de propósito, e é o oposto do padrão da AWS quando a sub-rede é
# criada pelo console. IP público automático é como recurso ganha endereço
# sem ninguém pedir; aqui o ALB recebe o dele porque é `internet-facing`.
map_public_ip_on_launch = false
tags = { Name = "lab02-publica-${each.key}", Camada = "publica" }
}
resource "aws_subnet" "privada" {
for_each = local.privadas
vpc_id = aws_vpc.principal.id
cidr_block = each.value
availability_zone = each.key
tags = { Name = "lab02-privada-${each.key}", Camada = "privada" }
}
# ─── O NAT, e as duas coisas que ele exige ──────────────────────────────────
#
# 1. Ele mora na sub-rede PÚBLICA. Criá-lo na privada produz um recurso que sobe
# sem erro, cobra por hora e não roteia nada — o sintoma é timeout na saída.
# 2. Ele precisa do IGW existindo. `depends_on` explícito porque o Terraform não
# infere essa ordem: não há referência de atributo entre os dois.
resource "aws_eip" "nat" {
domain = "vpc"
tags = { Name = "lab02-nat-eip" }
}
resource "aws_nat_gateway" "nat" {
allocation_id = aws_eip.nat.id
subnet_id = aws_subnet.publica["${var.regiao}a"].id
depends_on = [aws_internet_gateway.igw]
tags = { Name = "lab02-nat-aza" }
}
# ─── As duas tabelas: a diferença entre elas é UMA linha ────────────────────
resource "aws_route_table" "publica" {
vpc_id = aws_vpc.principal.id
route {
cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.igw.id # ← esta linha faz a sub-rede ser pública
}
tags = { Name = "lab02-rt-publica" }
}
resource "aws_route_table" "privada" {
vpc_id = aws_vpc.principal.id
route {
cidr_block = "0.0.0.0/0"
nat_gateway_id = aws_nat_gateway.nat.id # ← e esta faz ser privada
}
tags = { Name = "lab02-rt-privada" }
}
# Associação EXPLÍCITA de cada sub-rede. Sem isto, a sub-rede usa a tabela
# principal da VPC — e "esqueci de associar" é indistinguível de "está associada
# ao lugar certo" quando se olha só a lista de sub-redes no console.
resource "aws_route_table_association" "publica" {
for_each = aws_subnet.publica
subnet_id = each.value.id
route_table_id = aws_route_table.publica.id
}
resource "aws_route_table_association" "privada" {
for_each = aws_subnet.privada
subnet_id = each.value.id
route_table_id = aws_route_table.privada.id
}
# A tabela principal fica SEM rota para o IGW. Assim, sub-rede criada no futuro
# sem associação explícita nasce sem saída — falha visível, em vez de sub-rede
# acidentalmente pública.
resource "aws_default_route_table" "principal" {
default_route_table_id = aws_vpc.principal.default_route_table_id
tags = { Name = "lab02-rt-principal-sem-saida" }
}
# ─── Registro do tráfego, para PROVAR o que o desenho afirma ────────────────
resource "aws_flow_log" "vpc" {
vpc_id = aws_vpc.principal.id
traffic_type = "ALL"
log_destination_type = "cloud-watch-logs"
log_destination = aws_cloudwatch_log_group.flow.arn
iam_role_arn = aws_iam_role.flow.arn
}
resource "aws_cloudwatch_log_group" "flow" {
name = "/lab02/vpc/flow"
retention_in_days = 7 # o destroy NÃO apaga log group sem isto; ver a limpeza
}A ordem das AZs é uma armadilha do Terraform
Derivar o CIDR de um índice (`cidrsubnet(..., count.index)`) parece elegante e é frágil: a lista de AZs pode vir em ordem diferente, e o CIDR de cada sub-rede muda de dono. O plano mostra `destroy` e `create` de sub-rede com o banco dentro. Mapa com a AZ como chave amarra cada CIDR à sua zona.
Construir: o banco sem endereço, e o caminho do dev
resource "aws_db_subnet_group" "privada" {
name = "lab02-privada"
subnet_ids = [for s in aws_subnet.privada : s.id]
# Duas AZs são exigência do grupo, mesmo sem Multi-AZ ligado: sem isso o RDS
# recusa a criação. É o motivo de a sub-rede privada da AZ b existir desde já.
}
resource "aws_db_instance" "pg" {
identifier = "lab02-pg"
engine = "postgres"
instance_class = "db.t4g.micro"
allocated_storage = 20
storage_encrypted = true
db_subnet_group_name = aws_db_subnet_group.privada.name
vpc_security_group_ids = [aws_security_group.banco.id]
# A metade da correção que aparece aqui. A outra metade é a ausência de rota,
# e é ela que sustenta: com `false` mas em sub-rede pública, o banco continuaria
# com caminho de entrada esperando um security group errado.
publicly_accessible = false
# Senha gerenciada: ela nunca passa pelo estado do Terraform.
manage_master_user_password = true
username = "lab02admin"
backup_retention_period = 7
skip_final_snapshot = true # laboratório. Em produção, `false` — ver a limpeza
tags = { Name = "lab02-pg", Projeto = "lab02" }
}
# Security group que referencia OUTRO security group, não faixa de IP. A regra
# passa a ser "quem tem este papel", e continua valendo quando a task troca de IP.
resource "aws_security_group" "banco" {
name = "lab02-banco"
vpc_id = aws_vpc.principal.id
}
resource "aws_vpc_security_group_ingress_rule" "banco_da_app" {
security_group_id = aws_security_group.banco.id
referenced_security_group_id = aws_security_group.app.id
from_port = 5432
to_port = 5432
ip_protocol = "tcp"
description = "PostgreSQL somente das tasks da aplicacao"
}Falta o caminho do dev. Sem ele, a rota pública volta na próxima urgência — e o laboratório teria trocado um problema de rede por um problema de processo.
# Sessão pela task que já roda na sub-rede privada. Não cria recurso, não abre
# porta, e cada sessão fica registrada no CloudTrail com o nome de quem abriu.
TASK=$(aws ecs list-tasks --cluster lab02 --service-name api \
--query 'taskArns[0]' --output text)
# Encaminhamento de porta: a 5432 local passa a falar com o banco pela task.
aws ssm start-session \
--target "ecs:lab02_${TASK##*/}_$(aws ecs describe-tasks --cluster lab02 \
--tasks "$TASK" --query 'tasks[0].containers[0].runtimeId' --output text)" \
--document-name AWS-StartPortForwardingSessionToRemoteHost \
--parameters "host=$(terraform output -raw endereco_banco),portNumber=5432,localPortNumber=5432"
# Em outro terminal, o psql fala com localhost e chega no banco privado.
psql -h localhost -p 5432 -U lab02admin -d postgres
# Exige `enableExecuteCommand = true` na task e a permissão de SSM na task role.
# É a troca deste desenho: uma permissão a mais para não ter um endereço a mais.Implantar, e provar que o banco não é alcançável
terraform init
terraform plan -out=plano # confira: 4 sub-redes, 2 tabelas, 4 associações
terraform apply plano # NAT e RDS levam alguns minutos
# ── Prova 1: a tabela da sub-rede privada não tem rota para o IGW ──
aws ec2 describe-route-tables \
--filters "Name=tag:Name,Values=lab02-rt-privada" \
--query 'RouteTables[0].Routes[*].[DestinationCidrBlock,GatewayId,NatGatewayId]' \
--output table
# Esperado: 0.0.0.0/0 com NatGatewayId preenchido e GatewayId vazio.
# Se aparecer `igw-` aqui, a sub-rede não é privada, independentemente do nome.
# ── Prova 2: TODA sub-rede tem associação explícita ──
aws ec2 describe-route-tables \
--filters "Name=vpc-id,Values=$(terraform output -raw vpc_id)" \
--query 'RouteTables[*].Associations[?Main==`false`].SubnetId' --output text | wc -w
# Esperado: 4. Menos que isso significa sub-rede herdando a tabela principal.
# ── Prova 3: o banco não resolve para endereço público ──
dig +short "$(terraform output -raw endereco_banco)"
# Esperado: um 10.0.x.x. Qualquer endereço público aqui reprova o laboratório.
# ── Prova 4: a saída funciona, medida de dentro ──
aws ecs execute-command --cluster lab02 --task "$TASK" --container api \
--interactive --command "curl -s -o /dev/null -w '%{http_code}' https://api.ecr.us-east-1.amazonaws.com"
# Esperado: 200 ou 400 — qualquer resposta HTTP prova que o NAT roteia.
# Timeout aqui é o sintoma de NAT na sub-rede errada.A prova 3 é a que vale para a auditoria
Ela não pergunta se a porta está fechada — pergunta se existe endereço. É a diferença entre "configuramos o firewall" e "não há caminho", e é a segunda que sobrevive a uma sexta-feira com migration atrasada.
Quebrar de propósito: três falhas e o diagnóstico
| O que fazer | Sintoma | Como diagnosticar | O que a falha ensina |
|---|---|---|---|
| Mover o NAT para a sub-rede privada | task não inicia: `CannotPullContainerError` depois de ~2 minutos | `describe-nat-gateways` mostra o NAT `available` na sub-rede errada; o Flow Log não registra saída | que recurso que sobe sem erro pode não funcionar — e que o NAT precisa de rota para o IGW |
| Remover a associação da sub-rede privada | mesmo sintoma, causa diferente | a query da prova 2 devolve 3 em vez de 4; a sub-rede usa a tabela principal, que não tem saída | por que a tabela principal fica deliberadamente sem rota: a falha aparece |
| Apagar a rota 0.0.0.0/0 da tabela privada | a aplicação responde e para de baixar imagem nova | `describe-route-tables` mostra só a rota local; o tráfego interno continua, o externo não | que a rota local é implícita e nunca se apaga — só o tráfego para fora depende da linha |
| Marcar `publicly_accessible = true` de volta | nada quebra, e o `dig` passa a devolver IP público | a prova 3 reprova; o Flow Log passa a registrar tentativas com origem pública | que o teste tem de afirmar a ausência de endereço, não a presença de resposta |
A quarta é a mais importante e a única que não produz sintoma. Um desenho que volta a ser inseguro sem quebrar nada é exatamente o que precisa de teste automatizado — as provas 1 a 3 existem para virar checagem de esteira.
Segurança: o risco, o controle e a detecção
| Risco | Probabilidade | Impacto | Controle preventivo | Detecção | Resposta |
|---|---|---|---|---|---|
| Banco alcançável da internet | era certa antes; baixa depois | crítico | sub-rede sem rota para IGW + `publicly_accessible = false` | prova 3 na esteira; regra do AWS Config para RDS público | reverter o Terraform e girar a senha, porque não se sabe quem chegou |
| Sub-rede nova nasce pública | média | alto | tabela principal sem rota de saída | prova 2 conta associações; Flow Log mostra saída inesperada | associar à tabela certa; a ausência de saída trava o deploy antes do dano |
| Regra alargada sob urgência | alta | alto | caminho do dev pelo SSM, para a urgência não exigir a regra | CloudTrail em `AuthorizeSecurityGroupIngress` com 0.0.0.0/0 | reverter e registrar; se o caminho alternativo falhou, o problema é ele |
| Elastic IP órfão | média | baixo, e permanente | EIP declarado no mesmo Terraform do NAT | Cost Explorer mostra cobrança de endereço não associado | liberar o endereço; ver a seção de limpeza |
| Segredo do banco em variável de ambiente | média | alto | `manage_master_user_password` e leitura em runtime | varrer definição de task por `PASSWORD` | girar a senha e mover para leitura em runtime |
Duas camadas, e por que isso é o ponto
Antes, o security group era a única coisa entre o banco e a internet. Agora são duas condições independentes — ausência de rota e ausência de endereço público — e as duas teriam de estar erradas ao mesmo tempo. Não é redundância por excesso: é o que faz um erro de configuração deixar de ser um incidente.
Uma sub-rede chamada "privada-az-a" tem uma tabela de rotas associada com 0.0.0.0/0 apontando para um Internet Gateway. Ela é pública ou privada?
Observabilidade: as perguntas que o painel tem de responder
| Pergunta | Onde ela é respondida | Alarme inicial |
|---|---|---|
| Alguém está tentando alcançar o banco de fora? | Flow Log, filtro por `dstport=5432` com `action=REJECT` e origem pública | qualquer registro assim: em desenho correto, o número é zero |
| O NAT está roteando? | métrica de bytes processados pelo NAT no CloudWatch | zero bytes por 15 minutos com a aplicação em pé |
| Quanto o NAT está processando? | a mesma métrica, acumulada por dia | acima de 60 GB/mês projetado — o limiar em que o endpoint do L44 se paga |
| Alguma sub-rede está sem endereço livre? | a diferença entre endereços da sub-rede e interfaces em uso | abaixo de 15% livre: task nova falha a partir daí |
| A tabela de rotas mudou? | CloudTrail em `CreateRoute`, `DeleteRoute` e `ReplaceRoute` | qualquer chamada fora de janela de deploy |
O primeiro alarme é o único cujo limiar é zero, e é de propósito. Num desenho em que o banco não tem endereço público, tentativa de fora não é ruído a tolerar — é sinal de que o desenho não é o que se pensa.
Escala: 10, 10 mil, 1 milhão
| Volume | O que aguenta | O que quebra primeiro | O que fazer |
|---|---|---|---|
| 10 usuários | tudo, com folga | nada — o custo fixo domina | nada; o NAT já é o item mais caro e não depende do volume |
| 10 mil por dia | o desenho como está | nada estrutural | ligar o Multi-AZ do banco quando a janela de manutenção passar a doer |
| 1 milhão de eventos | o roteamento; o NAT escala sozinho até dezenas de Gbps | a conta do NAT, muito antes da capacidade dele | endpoint de VPC para S3 e ECR (L44) — corta a maior parte do tráfego de saída |
| Pico repentino | o NAT absorve sem configuração | endereços livres na sub-rede, se as tasks escalarem muito | foi por isso que a sub-rede é /20 e não /24 |
| Falha da AZ a | a AZ b continua servindo pelo ALB | a saída para a internet: o NAT está na AZ a | o segundo NAT, que é a decisão adiada com o motivo escrito |
| Falha regional | nada | tudo; o desenho é de uma região | está fora do escopo deste laboratório e é o L58 |
A falha de AZ tem um efeito que surpreende
Com um NAT só na AZ a, uma falha dessa zona deixa a aplicação da AZ b servindo requisições — e sem conseguir baixar imagem nova nem ler segredo. Ela funciona até o próximo deploy ou reinício de task. É uma degradação parcial, e a pior de diagnosticar: o serviço responde.
Custo: onde o dinheiro vai nesta arquitetura
| Cenário | Volume | O que domina | Tendência | Otimização |
|---|---|---|---|---|
| Protótipo | um dev, algumas horas por dia | a parte fixa do NAT — pagando por 730 horas para usar 20 | quase todo o custo é ocioso | apagar o NAT ao fim do dia, ou não ter sub-rede privada em ambiente de estudo |
| Produção pequena (este laboratório) | 40 clientes, 12 mil req/dia | a parte fixa do NAT, seguida do RDS | estável e previsível | um NAT em vez de dois; endpoint de gateway para S3, que não cobra por hora |
| Alta escala | milhões de eventos, deploy frequente | os GB processados, que passam a parte fixa | cresce com o número de deploys, não só com o tráfego de usuário | endpoint de interface para ECR e um por AZ para não pagar tráfego entre zonas |
O custo oculto deste desenho é o deploy
Cada task nova baixa a imagem pelo NAT. Uma imagem de 300 MB, com 10 tasks e 20 deploys por mês, são 60 GB processados só de pull — mais que o tráfego dos 40 clientes. É a medição que faz o endpoint do L44 se pagar, e ela não aparece se você olhar só o tráfego de usuário.
Well-Architected nos seis pilares
| Pilar | Situação depois deste laboratório | Risco que fica | Prioridade |
|---|---|---|---|
| Excelência operacional | rede em Terraform, plano de endereçamento escrito, provas em comandos | as provas não estão na esteira ainda — e a regressão silenciosa é a que importa | alta |
| Segurança | duas camadas independentes protegendo o banco; Flow Log ligado | sem regra automática que reprove RDS público; a checagem é manual | alta |
| Confiabilidade | sub-redes em duas AZs; ALB e banco preparados | o NAT é ponto único: falha da AZ a degrada sem derrubar | média |
| Eficiência de performance | tráfego do banco não sai da VPC | nenhum relevante nesta escala | baixa |
| Otimização de custos | um NAT, com o motivo escrito; tags de projeto | o pull de imagem paga NAT sem necessidade — o endpoint resolve | média |
| Sustentabilidade | nada ocioso além do NAT | o NAT de ambiente de estudo roda 730 horas para ser usado em 20 | baixa |
Evolução em níveis
| Nível | O que muda | Novo risco | Impacto de custo |
|---|---|---|---|
| 1 — Protótipo | uma sub-rede pública, tudo com IP público | o banco alcançável; é o ponto de partida deste módulo | o mais baixo: sem NAT |
| 2 — Este laboratório | duas camadas, duas tabelas, NAT, banco sem endereço | o NAT como ponto único de saída | + a parte fixa do NAT, que passa a dominar |
| 3 — Produção | um NAT por AZ; provas na esteira; regra do Config | nenhum novo; o custo fixo dobra | + a segunda parte fixa |
| 4 — Alta escala | endpoint de gateway e de interface (L44) | endpoint de interface tem custo por hora próprio | − a maior parte dos GB do NAT |
| 5 — Avançado | VPC por ambiente, conta por ambiente, rede compartilhada | complexidade de roteamento entre contas | sobe com a duplicação de recursos fixos |
| 6 — Dados e IA | endpoint privado para serviços de IA, tráfego sem sair da AWS | nenhum de rede; o risco muda de lugar | o endpoint troca o custo de NAT pelo dele |
Onde IA entra nesta arquitetura, e onde não entra
Aqui, IA não resolve o problema — e dizer isso é parte da resposta
O problema deste laboratório é uma linha de tabela de rotas. Não há ambiguidade para um modelo desfazer, não há linguagem natural para interpretar, e não há volume de decisões que justifique inferência. Colocar IA aqui seria enfeite, e o padrão desta escola é dizer isso em vez de forçar.
Existe uma extensão legítima, e ela é de OPERAÇÃO, não de arquitetura: quando o volume de registros do Flow Log passa do que alguém lê, a pergunta "que tráfego aqui é anormal" deixa de ter resposta manual. Aí entra detecção sobre o próprio registro — e é o GuardDuty, que consome Flow Log sem você construir nada. É aprendizado de máquina aplicado a um problema que de fato é de padrão, e ele aparece no L48.
A distinção que vale levar: IA entra quando o problema é de PADRÃO em volume que excede a leitura humana. Rede é um problema de topologia, e topologia se prova, não se estima.
Anti-padrões desta arquitetura
| Anti-padrão | Por que alguém faz | Por que é problema | Forma correta |
|---|---|---|---|
| Chamar de "privada" a sub-rede pelo nome da tag | a tag é o que se vê na lista do console | o nome não muda o roteamento; sub-rede chamada "privada" com rota para o IGW é pública | conferir a tabela associada, que é o que decide — a prova 1 faz isso |
| NAT na sub-rede privada | parece o lugar dele, já que serve a privada | sobe sem erro, cobra por hora e não roteia; o sintoma aparece no deploy seguinte | NAT na pública, referenciado pela tabela da privada |
| Usar a tabela principal para tudo | é o padrão e economiza duas linhas | sub-rede nova herda em silêncio; "esqueci de associar" fica indistinguível de "está certo" | associação explícita, e a principal sem rota de saída |
| Sub-rede /28 para economizar endereço | endereço parece um recurso escasso | sobram 11 endereços úteis, e cada task Fargate consome um; a falha só aparece sob escala | /20 e espaço reservado — endereço privado não custa nada |
| Abrir 0.0.0.0/0 no security group "só por hoje" | a migration precisa subir e o IP de casa mudou | "hoje" não tem data de fim, e a regra fica; foi assim que este laboratório nasceu | caminho pelo SSM, que não depende de IP nem de regra nova |
| Derivar CIDR de índice numérico | parece elegante e evita repetir endereço | reordenar a lista de AZs reatribui sub-rede; o plano mostra destroy de sub-rede com banco dentro | mapa com a AZ como chave, amarrando cada CIDR à sua zona |
Quando algo não funciona
| Sintoma | Causa provável | Como investigar | Correção |
|---|---|---|---|
| `CannotPullContainerError` na task | a sub-rede privada não tem saída | `describe-route-tables` na tabela associada; procure 0.0.0.0/0 | associar a tabela correta, ou mover o NAT para a sub-rede pública |
| Timeout ao conectar no banco, de dentro da VPC | security group, ou DNS não resolvendo | `dig` no endpoint pela task; se não resolver, é `enable_dns_hostnames` | ligar as duas opções de DNS na VPC; conferir a regra que referencia o SG da app |
| O `dig` no endpoint devolve IP público | `publicly_accessible = true` ficou | a prova 3; e `describe-db-instances` no campo `PubliclyAccessible` | mudar para `false` e aplicar; a mudança exige reinício da instância |
| `terraform apply` falha criando o NAT | o IGW ainda não existe | a mensagem cita dependência de gateway | `depends_on` no IGW — o Terraform não infere essa ordem |
| O plano mostra destroy de sub-rede | CIDR derivado de índice e a ordem das AZs mudou | compare o CIDR de cada sub-rede no estado com o do plano | trocar para mapa com a AZ como chave; NÃO aplicar antes |
| Cobrança de Elastic IP depois do destroy | o EIP foi criado fora do Terraform e ficou órfão | `describe-addresses` procurando `AssociationId` vazio | liberar o endereço; endereço não associado cobra por hora |
Limpeza: o que o destroy não leva
terraform destroy
# ── O que o destroy NÃO leva, e cada um cobra ou atrapalha ──
# 1. Elastic IP órfão — cobra por hora quando NÃO está associado.
aws ec2 describe-addresses \
--query 'Addresses[?AssociationId==`null`].[PublicIp,AllocationId]' --output table
# aws ec2 release-address --allocation-id <id>
# 2. Snapshot final do RDS. Aqui `skip_final_snapshot = true`, então não há —
# mas em produção o padrão é `false`, e o snapshot fica cobrando armazenamento.
aws rds describe-db-snapshots --snapshot-type manual \
--query 'DBSnapshots[?starts_with(DBInstanceIdentifier,`lab02`)].DBSnapshotIdentifier'
# 3. Log group do Flow Log. Ele TEM `retention_in_days`, então é destruído —
# sem esse atributo, o Terraform não o gerencia e o log fica para sempre.
aws logs describe-log-groups --log-group-name-prefix /lab02
# 4. Backups automáticos do RDS. Sobrevivem à instância pelo período de retenção.
aws rds describe-db-instance-automated-backups \
--query 'DBInstanceAutomatedBackups[?starts_with(DBInstanceIdentifier,`lab02`)]'
# 5. Interfaces de rede órfãs. Elas impedem a VPC de ser apagada, e o erro do
# destroy é "DependencyViolation" sem dizer qual dependência.
aws ec2 describe-network-interfaces \
--filters "Name=vpc-id,Values=<vpc-id>" \
--query 'NetworkInterfaces[*].[NetworkInterfaceId,Description,Status]' --output table
# 6. O segredo criado pelo `manage_master_user_password` fica em exclusão
# agendada — não é apagado na hora, e ainda aparece na lista.
aws secretsmanager list-secrets --include-planned-deletion \
--query 'SecretList[?starts_with(Name,`rds!`)].[Name,DeletedDate]' --output table
# 7. Application Load Balancer e target group. O destroy leva os dois quando estão
# no mesmo Terraform — mas um Application Load Balancer criado à mão numa
# tentativa anterior cobra por hora e por unidade de capacidade, igual ao NAT.
aws elbv2 describe-load-balancers \
--query 'LoadBalancers[?starts_with(LoadBalancerName,`lab02`)].[LoadBalancerName,State.Code]' \
--output table
# 8. NAT Gateway e Elastic IP, os dois mais caros de esquecer.
aws ec2 describe-nat-gateways --filter "Name=state,Values=available" \
--query 'NatGateways[*].[NatGatewayId,SubnetId]' --output table| Recurso | O `destroy` leva? | Se ficar, o que acontece |
|---|---|---|
| NAT Gateway | sim | cobra por hora com ou sem tráfego — o esquecimento mais caro deste laboratório |
| Elastic IP | sim, o do NAT | endereço NÃO associado cobra por hora; um criado à mão fica órfão e invisível |
| Application Load Balancer | sim | cobra por hora e por unidade de capacidade, na mesma ordem de grandeza do NAT |
| Log group do Flow Log | sim, porque tem `retention_in_days` | sem esse atributo o Terraform não o gerencia, e o log fica para sempre |
| Backup automático do RDS | não | sobrevive à instância pelo período de retenção configurado |
| Snapshot final | não, se existir | aqui `skip_final_snapshot = true`; em produção o padrão é o oposto |
| Segredo da senha gerenciada | exclusão agendada | continua na lista alguns dias antes de sair de fato |
| Interface de rede órfã | não | impede a VPC de ser apagada, e o erro não diz qual é a dependência |
O NAT é o que mais dói esquecer
Ele cobra por hora, com ou sem tráfego, e não aparece em nenhuma lista de "recursos em uso" que se olhe por hábito. Um NAT esquecido em ambiente de estudo é a fatura surpresa mais comum de quem está aprendendo VPC. Confira `describe-nat-gateways` antes de fechar o terminal.
Resumo: problema, serviço e motivo
| Problema | Peça | Por que ela, e não outra |
|---|---|---|
| Banco alcançável da internet | sub-rede privada + tabela sem IGW | remove o caminho, em vez de depender de regra que já foi alargada uma vez |
| Aplicação precisa sair | NAT Gateway na sub-rede pública | dá saída sem dar entrada — é a propriedade que o IGW não tem |
| Sub-rede nova nascer pública | tabela principal sem rota de saída | transforma o esquecimento em falha visível no deploy |
| Endereço esgotar sob escala | sub-rede /20 com /18 reservado | endereço privado não custa; redimensionar exige recriar a VPC |
| Dev precisa do banco | sessão do SSM pela task | não cria endereço nem regra, e deixa rastro no CloudTrail |
| Provar o que o desenho afirma | Flow Log + as quatro provas | auditoria pede evidência, e "configuramos" não é evidência |
| Falha | O que protege |
|---|---|
| Security group alargado por engano | a ausência de rota, que é independente dele |
| Sub-rede criada sem associação | a tabela principal sem saída — o deploy falha |
| Falha da AZ a | o ALB e as tasks na AZ b continuam servindo (a saída, não) |
| NAT na sub-rede errada | as provas 1 e 4, que medem em vez de supor |
| `publicly_accessible` voltando a `true` | a prova 3 — e nada mais, hoje: é a lacuna |
Desafio — sem roteiro
O requisito
A aplicação privada passou a chamar o S3 diretamente (para ler um arquivo de configuração), e o tráfego está saindo pelo NAT Gateway — que cobra por GB processado. Adicione um Gateway VPC Endpoint para o S3.
Critério de aceite — executável, não "verifique se funciona"
Depois do endpoint criado, o CloudWatch do NAT Gateway (`BytesOutToDestination`) para de crescer quando a aplicação lê do S3 — o tráfego passou a ir pela rota do endpoint, não pelo NAT.
- Dica 1: Gateway endpoint (não Interface) é o tipo certo para S3 — ele é de graça e funciona por rota na tabela de rotas, não por ENI.
- Dica 2: A tabela de rotas da sub-rede PRIVADA precisa da rota nova para o prefixo do S3 — o Terraform associa isso automaticamente se você usar `route_table_ids` no recurso do endpoint.
- Dica 3: Prova que funcionou: descreva a tabela de rotas com `aws ec2 describe-route-tables` e confira a rota `pl-...` (prefix list do S3) apontando para o endpoint, não para o NAT.
Custo estimado do que este desafio acrescenta
Gateway VPC Endpoint para S3 não tem custo de hora nem de processamento — é a mudança rara que só economiza.
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
❓ O que exatamente faz uma sub-rede ser privada?
❓ Por que o NAT Gateway fica na sub-rede pública?
❓ Qual a diferença entre Internet Gateway e NAT Gateway?
❓ Preciso de um NAT Gateway por zona de disponibilidade?
❓ Fechar a porta no security group não resolve o mesmo problema?
❓ Por que sub-rede /20 e não /24?
❓ Quanto custa um NAT Gateway?
❓ Como o time acessa o banco depois de fechar a rota?
Fixando
Uma task numa sub-rede privada falha com `CannotPullContainerError`. O NAT Gateway existe e está `available`. Qual é a primeira coisa a verificar?
Você marcou `publicly_accessible = false` no RDS, mas ele está numa sub-rede cuja tabela tem rota para o Internet Gateway. O banco está protegido?
Próximo passo — Laboratório 03
A rede está correta e o banco não tem endereço. O próximo laboratório ataca o que ainda dói: trocar a versão da aplicação sem indisponibilidade. Ele reaproveita esta VPC inteira e acrescenta o registro de imagem, a atualização gradual e a drenagem de conexão — e usa o NAT que você acabou de criar, o que torna visível a conta de pull de imagem que o laboratório 44 vai eliminar.
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…