Lab 05 — Domínio, TLS e o estático na borda
O problema, e a empresa que o tem
A Cadência é a mesma equipe de duas pessoas do L01 e do L03: uma API de pedidos em .NET 8, no ECS Fargate, com PostgreSQL em sub-rede privada e balanceador na frente, em São Paulo. Este ano ela ganhou oito lojas em Portugal e Angola, e é de lá que vêm as reclamações — não de erro, de lentidão.
O endereço que os lojistas usam é o nome do balanceador, aquele que termina em elb.amazonaws.com, em HTTP. O navegador escreve "Não seguro" ao lado dele, e já houve pergunta de lojista sobre isso. A resposta interna sempre foi "está na nossa lista" — porque conseguir HTTPS ali não é questão de configurar melhor: é impossível, e a próxima seção mostra por quê.
A página de pedidos são 42 arquivos e 3,1 MB, servidos pelo mesmo contêiner da API a partir de wwwroot. Medido de Lisboa, o primeiro carregamento levou 4,8 s; de São Paulo, 0,9 s. A diferença não é a velocidade da task: é que cada um dos 42 arquivos atravessa o Atlântico. Quem olha o painel do ECS não vê problema nenhum, porque não existe problema nenhum lá.
Há um terceiro sintoma, e é o que mais custa por semana: trocar uma palavra na tela exige construir imagem, publicar no registro e fazer rollout da API. O front pegou carona no ciclo de vida do backend, e mudança de texto passou a carregar o risco de reimplantar o sistema de pedidos.
O que este laboratório NÃO é
Não é proteção de borda. WAF, limite de taxa e Shield entram no L47, que depende deste — e a razão da ordem é concreta: WAF só é obrigatório se ninguém puder alcançar a origem por fora, o que só passa a valer depois de a borda ser o único caminho. Também não é decisão entre SPA na borda e renderização no servidor: isso é o L20, e este módulo é o pré-requisito dele.
O que você vai conseguir fazer
Cada objetivo se prova com um comando na seção de implantação. Nenhum deles se prova com a sensação de ter entendido.
- Explicar por que nenhum certificado público pode cobrir o nome de um balanceador da AWS.
- Dizer em que região o certificado de uma distribuição tem de estar, e por que a região da aplicação é irrelevante para isso.
- Derivar por que o ápice de um domínio precisa de registro ALIAS, e o que muda na fatura.
- Servir front e API no mesmo domínio com dois comportamentos, e justificar a ordem deles.
- Compor uma chave de cache e prever o efeito de incluir cookie ou parâmetro de consulta.
- Dizer quem decide o tempo de vida real de um objeto na borda, entre a origem e a política.
- Publicar o front por cópia de arquivo, sem invalidação e sem rollout da API.
- Medir taxa de acerto por caminho, e explicar por que a métrica única da distribuição mente.
- Diagnosticar 502, 403 e 404 na borda distinguindo qual das três camadas os gerou.
O que a certificação cobra disto
| Conceito | Certificação | Como aparece aqui | O que dominar |
|---|---|---|---|
| Certificado gerenciado e região | SAA-C03, DVA-C02, SOA-C02 | dois certificados: us-east-1 para a borda, região da aplicação para o ALB | que a exigência de us-east-1 é da distribuição, não da conta nem da aplicação |
| Validação por DNS e renovação | SAA-C03, SOA-C02 | CNAME na zona, e o que acontece se ele for apagado depois | que a renovação automática depende do registro CONTINUAR na zona |
| ALIAS vs CNAME no Route 53 | SAA-C03, CLF-C02 | registro no ápice apontando para a distribuição | que CNAME no ápice não existe, e que ALIAS para recurso da AWS não cobra consulta |
| Comportamento de cache por caminho | SAA-C03, DVA-C02 | um domínio com política oposta em `/assets/*` e em `/api/*` | que o primeiro padrão que casa vence e o coringa é sempre o último |
| Chave de cache | SAA-C03, DVA-C02 | política sem cookie no estático, e encaminhamento total na API | a diferença entre política de cache e política de requisição de origem |
| Origem e acesso restrito | SAA-C03, SOA-C02 | bucket privado com OAC e ALB atrás de lista de prefixo mais cabeçalho | que bucket como ponto de site é origem personalizada e não aceita OAC |
| TTL e cabeçalho da origem | SAA-C03, DVA-C02 | `Cache-Control` decidindo, com TTL máximo e padrão como limites | qual dos três TTL se aplica em cada combinação de cabeçalho |
| Invalidação vs versionamento | DVA-C02, DOP-C02 | publicação por nome com hash, invalidando só o `index.html` | por que versionar é mais previsível e mais barato que invalidar |
| Origem regional vs global | SAA-C03 | S3 e CloudFront fora da VPC; ALB e task dentro | que alcançar S3 de dentro da VPC é endpoint, não pertencimento |
Onde isto costuma ser cobrado errado
A pergunta clássica descreve uma aplicação em Frankfurt ou São Paulo e pergunta onde pedir o certificado da distribuição. Quem responde "na região da aplicação" está aplicando a regra do balanceador — que é correta para o balanceador. Para a distribuição, a resposta é us-east-1 em qualquer cenário, e não há exceção regional. A segunda pegadinha frequente dá um domínio no ápice e oferece CNAME como alternativa ao ALIAS: não é uma escolha entre duas opções, porque uma delas não existe.
Requisitos, e como cada um muda o desenho
Requisito que não aparece numa linha de configuração é intenção. A coluna da direita é onde cada um deixou marca — e a arquitetura de produção não tem nenhuma peça que não esteja nela.
| Requisito | Valor declarado | O que ele decide no desenho |
|---|---|---|
| HTTPS confiável no domínio próprio | obrigatório | obriga zona hospedada, certificado do ACM validado por DNS e — para a distribuição — certificado em us-east-1 |
| Endereço sem `www` | cadencia.com.br responde | obriga registro ALIAS tipo A e AAAA no ápice; CNAME ali não é permitido |
| Front e API no mesmo nome | sem CORS, sem cookie de terceiro | obriga duas origens numa distribuição, com dois comportamentos, em vez de dois domínios |
| Primeiro carregamento em Lisboa | abaixo de 1,5 s | obriga cache na borda com validade longa no estático, porque o gargalo é número de idas |
| Taxa de acerto no estático | ≥ 90% | obriga chave de cache sem cookie, sem cabeçalho e sem parâmetro de consulta |
| Publicar o front sem rollout da API | obrigatório | obriga o estático fora da imagem, num bucket, e a remoção de `UseStaticFiles` |
| Resposta de API nunca reutilizada | zero tolerância | obriga política de armazenamento desligado em `/api/*` e `no-store` na origem |
| Estático inalcançável por fora da borda | obrigatório | obriga bucket privado com OAC e política que exige o ARN desta distribuição |
| Origem inalcançável por fora da borda | obrigatório | obriga lista de prefixo gerenciada no grupo de segurança e cabeçalho secreto conferido na aplicação |
| Cabeçalhos de segurança sem tocar no código | HSTS e afins | obriga política de cabeçalhos de resposta na distribuição, não middleware |
| Custo de publicação previsível | sem invalidação em massa | obriga hash no nome do arquivo e validade de um ano nos ativos |
O requisito que quase todo mundo descobre tarde
Front e API no mesmo nome parece preferência estética e é decisão técnica com três consequências. Domínios separados fazem toda chamada com cabeçalho customizado disparar uma requisição de verificação prévia antes da real, dobrando a latência das primeiras chamadas; transformam o cookie de sessão em cookie de terceiro, que os navegadores bloqueiam por padrão; e acrescentam um certificado e um nome a administrar. Nada disso aparece em ambiente local, onde tudo é `localhost`.
Arquitetura mínima: o contêiner entregando tudo
Este é o desenho que a Cadência tem hoje, e ele é legítimo como ponto de partida: publica, funciona e cabe em uma linha de código. O laboratório começa medindo, porque "o site está lento em Portugal" não é discutível e um número é.
- → HTTP 80: a página e mais 41 arquivos
- → encaminha o .js e o /api/pedidos pela mesma regra
- → SQL do pedido, atrás da fila de estático
- → wwwroot embutido na camada da imagem
- Fora da AWS
- Compute
- Rede e entrega
- Banco de dados
Duas coisas somem deste desenho ao mesmo tempo, e é por isso que ele é o ponto de partida: não existe HTTPS confiável, porque o nome não é seu; e não existe cache, porque não há nada entre o cliente e a origem. Percorra os passos e repare que o problema de latência não é distância — é distância multiplicada pelo número de arquivos.
- Nenhum certificado é possível neste nome. O certificado público exige provar controle do domínio, e o nome do balanceador termina em `elb.amazonaws.com` — que não é seu. O resultado não é "HTTPS mal configurado": é HTTPS impossível. A consequência prática vai além do cadeado, porque recurso de navegador que exige contexto seguro (service worker, instalação como aplicativo, geolocalização) não liga.
- A distância é cobrada 42 vezes, não uma. A ida e volta entre Lisboa e São Paulo é o custo fixo de cada requisição. Com 42 arquivos, o HTTP/2 multiplexa e ainda assim a página só fica pronta depois do último byte do último arquivo. Latência de rede não se resolve com mais vCPU na origem: só se resolve não indo até a origem.
- A vCPU do contêiner é gasta em byte que não muda. Cada visita faz a task ler e transmitir os mesmos megabytes. É computação paga para entregar conteúdo idêntico ao da visita anterior — e é o único item de custo desta arquitetura que cresce com o número de visitas sem gerar nada novo.
- O estático competindo com o dinâmico é competição por thread. Quando o pico chega, as requisições de arquivo e as de pedido disputam o mesmo pool de threads e a mesma banda da task. Escalar o serviço resolve — pagando task nova para servir CSS. O gargalo é real, e a peça errada está sendo escalada.
- O ciclo de vida do front virou o ciclo de vida da API. Com o `wwwroot` dentro da imagem, trocar uma palavra na tela exige construir imagem, publicar no registro e fazer rollout — todo o L03 para um texto. E o inverso também vale: quem só quer corrigir o front assume o risco de reimplantar a API.
- Por que quase todo projeto começa exatamente assim. Porque `app.UseStaticFiles()` é uma linha, e porque um domínio a menos é um domínio a menos para administrar. Não é ignorância: é o menor caminho que funciona. Ele para de funcionar quando o público deixa de ser local, e é esse o momento em que este laboratório entra.
A medição abaixo separa as duas parcelas que costumam ser confundidas: o tempo até o primeiro byte, que é dominado pela distância, e o tempo total, que é dominado pelo número de arquivos. Rode de onde o cliente está — de dentro da própria região o resultado é bonito e inútil.
# Rode ANTES de mudar nada, e de fora da regiao da aplicacao.
# `curl` de um host em Lisboa, ou de uma instancia em eu-west-3, serve.
ALVO="http://ffv-lab-alb-1234567890.sa-east-1.elb.amazonaws.com"
# 1. Tempo ate o primeiro byte de UM arquivo. Isto mede distancia.
for i in $(seq 1 10); do
curl -s -o /dev/null -w "%{time_connect} %{time_starttransfer} %{time_total}\n" \
"$ALVO/assets/app.js"
done
# 2. A pagina inteira: os 42 arquivos, em sequencia de dependencia.
# Sem navegador nao ha como reproduzir a cascata fielmente; o que este
# laco mede e o piso, e o piso ja basta para a decisao.
curl -s "$ALVO/" -o /tmp/index.html
grep -o -E '(src|href)="[^"]+"' /tmp/index.html | cut -d\" -f2 | sort -u \
| while read -r caminho; do
curl -s -o /dev/null -w "%{time_total} $caminho\n" "$ALVO$caminho"
done | sort -rn | head -10
# Na Cadencia, de Lisboa: conexao 0,19 s · primeiro byte 0,21 s · total da
# pagina 4,8 s. De Sao Paulo: 0,02 / 0,03 / 0,9 s. A conta fecha: a diferenca
# de 3,9 s e aproximadamente 0,19 s de ida e volta multiplicado pelas idas que
# a cascata de dependencia nao consegue paralelizar.
# Estes numeros sao da aplicacao de exemplo. Meca os seus: a sua cascata tem
# outra forma, e e a forma dela que decide o multiplicador.Por que HTTPS aqui não é questão de configurar melhor
Um certificado público existe para provar que você controla um nome. A autoridade certificadora só emite depois dessa prova — no ACM, publicando um CNAME na zona do domínio. O nome `ffv-lab-alb-1234567890.sa-east-1.elb.amazonaws.com` pertence à AWS, e você não tem como publicar registro na zona `amazonaws.com`. Não existe caminho: enquanto o endereço for o do balanceador, HTTPS confiável está fora de alcance. É esse fato, e não uma preferência por domínio bonito, que faz este laboratório começar pelo DNS.
Arquitetura para produção
Cada peça abaixo rastreia a uma linha da tabela de requisitos. Se você não conseguir apontar o requisito, a peça é adorno — e vale checar: a política de cabeçalhos de resposta está lá por causa da linha de HSTS, o cabeçalho secreto por causa da linha de origem inalcançável, e a assinatura de métricas por causa da linha de taxa de acerto.
- → sync do build: nome com hash + Cache-Control por tipo
- → consulta A e AAAA de cadencia.com.br
- → ALIAS devolve endereço de POP, sem salto extra
- → nome alternativo autorizado pelo certificado
- → HTTPS 443 terminado no ponto de presença
- → só no miss de /assets/*: GET assinado em SigV4
- → /api/* sempre passa, com cookie e Authorization
- → origem.cadencia.com.br, o nome que o certificado cobre
- → só o que é único por usuário chega ao contêiner
- → SQL do pedido, sem fila de bundle na frente
- → taxa de acerto e latência de origem por minuto
- → classificação de cada requisição, com o caminho
- Fora da AWS
- Rede e entrega
- Segurança e identidade
- Armazenamento
- Compute
- Banco de dados
- Gestão e governança
A mudança estrutural não é "acrescentar CloudFront": é a origem deixar de ser uma. O mesmo nome passa a ter dois caminhos com políticas opostas — o estático existe para ser reutilizado, a rota de API existe para nunca ser. Percorra os passos: cada peça rastreia a uma linha da tabela de requisitos.
- O ápice só resolve porque ALIAS não é CNAME. Não existe CNAME no ápice de uma zona: a especificação de DNS não permite, e o Route 53 também não. O registro ALIAS é extensão da AWS que responde com os endereços do recurso, mantendo o tipo A ou AAAA. Além de ser o único caminho para `cadencia.com.br` sem `www`, a consulta a recurso da AWS por ALIAS não é cobrada, enquanto a de CNAME é — e um CNAME que aponta para outro registro na zona é cobrado como duas consultas.
- Dois certificados, duas regiões, dois papéis. O certificado que o navegador vê tem de estar em us-east-1, porque é lá que a distribuição o busca — independentemente de a aplicação rodar em São Paulo. O certificado do ALB fica na região da aplicação, e serve à conexão entre a borda e a origem. Eles não são o mesmo certificado, e o segundo tem uma exigência que derruba quem improvisa: um dos nomes dele precisa casar com o nome de domínio configurado como origem, senão a resposta ao visitante é 502.
- Um domínio, dois comportamentos, e a ordem decide. O comportamento de `/api/*` é declarado antes do coringa `*`, e o primeiro padrão que casa é o que vale — o coringa é sempre processado por último. Se a ordem se invertesse, toda requisição de API cairia na política do estático e passaria a ser armazenada. É por isso que ordem de comportamento é decisão de segurança, não de arrumação.
- A chave de cache é onde a taxa de acerto se ganha ou se perde. No comportamento do estático, a chave não inclui cookie, cabeçalho nem string de consulta: o mesmo arquivo tem uma entrada só. Incluir o cookie de sessão transformaria um objeto em uma cópia por usuário, e a taxa de acerto cairia para perto de zero sem nenhuma mensagem de erro. No comportamento da API, o armazenamento é desligado e todo valor do visitante é encaminhado, porque ali a resposta depende de quem pergunta.
- O TTL real vem da origem, não da distribuição. O `Cache-Control` que o S3 devolve é o que decide quanto tempo o objeto vive na borda. O TTL máximo da política só limita esse valor; o TTL padrão só é usado quando a origem não manda cabeçalho nenhum. Quem configura a política e esquece o cabeçalho na publicação fica com o padrão de um dia sem saber por quê — e quem manda o cabeçalho certo praticamente não precisa da política.
- Versão no nome do arquivo, em vez de invalidação. Cada arquivo é publicado com o hash do conteúdo no nome e um ano de validade; só o `index.html`, que aponta para eles, tem validade curta. Publicar passa a ser "subir arquivo novo e trocar uma linha de HTML", sem apagar nada de lugar nenhum. É a recomendação explícita da documentação, e também a mais barata: invalidação tem franquia mensal de caminhos e cobra acima dela.
- O bucket é privado, e a origem também deveria ser. O controle de acesso de origem faz o CloudFront assinar cada requisição ao S3, e a política do bucket só aceita o principal do serviço com o ARN desta distribuição — nem o público, nem outra distribuição. Para o ALB não há assinatura equivalente: o que existe é a lista de prefixo gerenciada do CloudFront no grupo de segurança, mais um cabeçalho secreto que a origem exige. As duas medidas juntas, e é a segunda que prova que a borda é a SUA.
A diferença estrutural em relação ao desenho anterior não é uma caixa a mais na frente. É que a origem deixou de ser uma: o mesmo nome passou a ter dois caminhos com regras opostas, e o estático saiu da imagem da aplicação. Tudo o mais — política de cache, política de requisição, OAC, cabeçalho secreto — existe para administrar essa separação.
O fator com maior efeito por linha alterada
Dos quatro fatores, o único que pode variar por três ordens de grandeza por causa de uma linha é o de variantes. Uma política de cache que inclui todos os cookies transforma 42 objetos em 42 por usuário, e a taxa de acerto vai a perto de zero sem erro nenhum, sem alarme e sem mudança visível na página — só a fatura de origem e a latência mudam. É por isso que a chave de cache é o objeto de estudo deste módulo, e não o CloudFront.
O caminho de uma requisição, ponta a ponta
Os passos abaixo não são jargão: cada um é um lugar onde a requisição pode parar, e saber qual deles parou é a diferença entre corrigir em minutos e mexer em configuração no escuro. Duas camadas de cache aparecem aqui, e a segunda é a que quase nunca é desenhada.
A camada que falta em quase todo desenho
Entre o ponto de presença e a origem existe o cache regional. Ele importa por um motivo aritmético: sem ele, cada ponto de presença do mundo buscaria o mesmo arquivo na sua origem, e o fator de pontos de presença da fórmula seria multiplicado inteiro. Com ele, o primeiro acesso de uma região aquece um cache que serve os pontos de presença vizinhos. O cabeçalho `Server-Timing` distingue os dois casos: `cdn-hit-layer;desc="EDGE"` é acerto no ponto de presença, `desc="REC"` é acerto no cache regional.
O que a API do CloudFront devolve é o lugar mais honesto para conferir a configuração, porque nele a ordem dos comportamentos é literal. Os campos abaixo são os que decidem; os demais foram omitidos.
// O que a API do CloudFront devolve, resumido nos campos que decidem.
// `aws cloudfront get-distribution-config --id E1PDQ2EXAMPLE`
//
// Este e o payload em que o defeito mais grave deste laboratorio e VISIVEL: a
// ordem dentro de CacheBehaviors.Items.
{
"DistributionConfig": {
"Aliases": { "Quantity": 2, "Items": ["cadencia.com.br", "www.cadencia.com.br"] },
"DefaultRootObject": "index.html",
"Origins": { "Quantity": 2, "Items": [
{
"Id": "estatico",
"DomainName": "ffv-lab-front-111122223333.s3.sa-east-1.amazonaws.com",
// OAC presente e OAI vazio: e assim que se confirma que a assinatura
// esta ativa. Os dois preenchidos ao mesmo tempo e migracao pela metade.
"OriginAccessControlId": "E2ABCDEXAMPLE",
"S3OriginConfig": { "OriginAccessIdentity": "" }
},
{
"Id": "api",
// NAO e o nome *.elb.amazonaws.com: o certificado do ALB nao o cobre, e
// o resultado seria 502 em toda chamada de API.
"DomainName": "origem.cadencia.com.br",
"CustomOriginConfig": {
"OriginProtocolPolicy": "https-only",
"OriginSslProtocols": { "Quantity": 1, "Items": ["TLSv1.2"] }
},
"CustomHeaders": { "Quantity": 1, "Items": [
{ "HeaderName": "X-Origem-Autorizada", "HeaderValue": "<vem do Secrets Manager>" }
]}
}
]},
// O coringa. Ele e sempre o ULTIMO a ser avaliado, e nao aparece na lista
// ordenada abaixo — tem campo proprio.
"DefaultCacheBehavior": {
"TargetOriginId": "estatico",
"ViewerProtocolPolicy": "redirect-to-https",
"CachePolicyId": "8a1b2c3d-0000-4444-8888-ffv-lab-estatico",
"Compress": true
},
// A ORDEM desta lista e a regra de roteamento. O primeiro padrao que casa
// vence. Mover /api/* para depois de um padrao mais amplo faria a API ser
// armazenada na borda — e resposta de um usuario chegaria a outro.
"CacheBehaviors": { "Quantity": 1, "Items": [
{
"PathPattern": "/api/*",
"TargetOriginId": "api",
// Managed-CachingDisabled: TTL minimo, maximo e padrao em zero.
"CachePolicyId": "4135ea2d-6df8-44a3-9df3-4b5a84be39ad",
// Managed-AllViewer: encaminha tudo do visitante, inclusive o Host.
"OriginRequestPolicyId": "216adef6-5c7f-47e4-b989-5492eafa07d3",
"AllowedMethods": { "Quantity": 7, "Items": [
"GET", "HEAD", "OPTIONS", "PUT", "POST", "PATCH", "DELETE"
]}
}
]},
"ViewerCertificate": {
// Regiao embutida no ARN. Qualquer coisa diferente de us-east-1 aqui
// significa que o apply falhou e alguem editou no console.
"ACMCertificateArn": "arn:aws:acm:us-east-1:111122223333:certificate/abcd-1234",
"SSLSupportMethod": "sni-only",
"MinimumProtocolVersion": "TLSv1.2_2021"
},
// Ausente de proposito: pagina de erro customizada e configuracao da
// DISTRIBUICAO, nao do comportamento. Mapear 404 para /index.html faria o
// 404 legitimo da API responder HTML com status 200.
"CustomErrorResponses": { "Quantity": 0 }
}
}
O 502 que não vem da sua aplicação
Quando a distribuição fala HTTPS com a origem, um dos nomes do certificado da origem tem de casar com o nome de domínio configurado como origem — e a documentação é explícita: se nenhum casar, o CloudFront devolve 502 ao visitante. Apontar a origem para o nome `*.elb.amazonaws.com` com um certificado emitido para o seu domínio é exatamente esse caso. O log da aplicação fica limpo, porque nenhuma requisição chegou a ela, e o campo `x-edge-detailed-result-type` do registro de acesso é onde a causa aparece.
As decisões, e o que se perde em cada uma
📋 Publicar o front e a API da Cadência sob um domínio próprio em HTTPS, com lojas em dois continentes, equipe de duas pessoas e a aplicação rodando em São Paulo.
Manter front e API no MESMO nome elimina de uma vez o CORS, a requisição de verificação prévia e o cookie de terceiro — três problemas que só existem porque alguém separou os domínios. Tirar o estático do contêiner separa os ciclos de vida: o front passa a ser publicado por cópia de arquivo, sem rollout da API. E o cache na borda ataca a latência internacional onde ela realmente está, que é no número de idas até São Paulo, não na velocidade da task. As três decisões se sustentam com uma distribuição e um bucket.
Alt: ALB servindo o estático, com certificado do ACM regional — Resolve o HTTPS e o domínio, e não resolve nada do resto: cada visita continua atravessando o oceano por arquivo, e a task continua gastando vCPU com byte que não muda. É um passo legítimo se o público for local e o estático, irrelevante — e aí o CloudFront seria adorno.
Alt: Subdomínio separado para a API (api.cadencia.com.br) — Parece mais organizado e cobra em outra moeda: requisição de verificação prévia antes de cada chamada com cabeçalho, cookie que passa a ser de terceiro e é bloqueado por padrão nos navegadores, e um certificado a mais. Faz sentido quando front e API têm times e ciclos separados; não faz para duas pessoas.
Alt: Bucket S3 configurado como site estático, público — É o caminho de menos linhas, e desliga o controle de acesso de origem: um bucket como ponto de site precisa ser origem personalizada e não aceita OAC nem OAI. Ninguém consegue mais garantir que o conteúdo só é servido pela borda, o que inviabiliza o WAF do L47 como camada obrigatória.
Alt: CDN de terceiro na frente do ALB — Funciona, e é a escolha certa quando a empresa já tem contrato e time treinado. Aqui ela troca duas integrações nativas (certificado gerenciado sem cópia de chave privada, e OAC assinando o acesso ao bucket) por um segredo compartilhado e um segundo painel — sem nenhum ganho no problema declarado.
| Decisão | Escolha | Alternativas | Motivo | O que se perde |
|---|---|---|---|---|
| Onde mora o estático | bucket privado com OAC | dentro da imagem; bucket como ponto de site público; EFS | separa o ciclo de vida do front do da API e tira computação do caminho | um recurso a mais para administrar, e a publicação deixa de ser um único artefato |
| Domínio da API | mesmo nome, comportamento `/api/*` | subdomínio `api.`; porta diferente | elimina verificação prévia, cookie de terceiro e um certificado | front e API passam a compartilhar a distribuição: mudança nela afeta os dois |
| Chave de cache do estático | só o caminho | incluir cookie; incluir consulta inteira; incluir `Accept-Language` | uma entrada por objeto é o teto teórico de taxa de acerto | nenhuma variação por usuário na borda — quem precisa disso precisa de outro desenho |
| Quem decide o TTL | `Cache-Control` da origem | TTL padrão da política; TTL mínimo forçado | a validade viaja junto com o arquivo e é revisada onde o arquivo é publicado | exige disciplina na publicação: esquecer o cabeçalho faz cair no TTL padrão |
| Atualizar conteúdo | hash no nome do arquivo | invalidar `/*` a cada publicação; TTL curto em tudo | é a recomendação oficial, dá controle de versão e não cobra por caminho | o nome do arquivo deixa de ser legível, e o build precisa gerar o hash |
| Protocolo até a origem | somente HTTPS | HTTP; `match-viewer` | o trecho borda-origem atravessa a internet pública e não deveria ir em claro | exige certificado e nome próprio na origem, com o 502 como punição do descuido |
| Fechar a origem | lista de prefixo + cabeçalho secreto | só lista de prefixo; só cabeçalho; deixar aberta | a lista prova que veio do CloudFront, o cabeçalho prova que veio da SUA distribuição | um segredo a rotacionar, e ele fica no estado do Terraform |
| Página de erro customizada | nenhuma | mapear 404 e 403 para `/index.html` com 200 | é configuração da distribuição, e transformaria o 404 da API em HTML com 200 | link direto para rota de SPA quebra — e é por isso que o L20 existe |
| Classe de preço | todas as localizações | restringir a classe para reduzir custo de transferência | o público em dois continentes é justamente o motivo do módulo | transferência mais cara do que se o público fosse local |
A dívida que este desenho cria, e que ele não paga
Com a borda como único caminho, ela passa a ser um ponto onde regra de segurança e de roteamento se concentram — e este módulo não coloca nada lá além de TLS e cache. Requisição automatizada continua chegando, agora barata para o atacante e ainda paga por você em requisições da distribuição. Barrar antes de custar computação é o L47, e ele só passa a ser possível depois deste, porque WAF na borda não serve para nada enquanto a origem for alcançável por fora.
Construir: o nome e os dois certificados
Este é o arquivo onde mais gente trava, e quase sempre pelo mesmo motivo: um certificado pedido na região errada não dá erro de validação — ele fica pronto, válido, e a distribuição não o aceita. Dois provedores no mesmo módulo resolvem isso de forma explícita.
# dns-tls.tf — o nome, e os DOIS certificados que esta arquitetura exige
# Duas regioes no mesmo modulo, de proposito. A distribuicao do CloudFront so
# aceita certificado de us-east-1; o ALB so aceita certificado da regiao dele.
# Nao e redundancia: sao dois pontos de terminacao de TLS diferentes.
provider "aws" {
region = var.regiao # sa-east-1 neste laboratorio
}
provider "aws" {
alias = "borda"
region = "us-east-1"
}
variable "dominio" {
type = string
description = "Dominio registrado, ja delegado a uma zona do Route 53. Ex.: cadencia.com.br"
}
variable "regiao" {
type = string
default = "sa-east-1"
}
locals {
tags = {
Projeto = var.projeto
Ambiente = var.ambiente
Modulo = "L05-borda"
CentroDeCusto = var.centro_de_custo # o rateio do L09 depende desta tag
}
}
# A zona ja existe: quem registra o dominio delega o NS para ela. Se o
# registrador aponta para outro provedor de DNS, nada abaixo funciona — e o
# sintoma e "o site nao resolve", sem erro em log nenhum.
data "aws_route53_zone" "raiz" {
name = var.dominio
private_zone = false
}
# ── Certificado da BORDA: us-east-1, sempre ─────────────────────────────────
resource "aws_acm_certificate" "borda" {
provider = aws.borda
domain_name = var.dominio
validation_method = "DNS"
# O "www" entra como nome alternativo para o redirecionamento funcionar no
# mesmo certificado. Cada nome alternativo tem de estar no certificado: a
# distribuicao recusa nome alternativo que o certificado nao cobre.
subject_alternative_names = ["www.${var.dominio}"]
# Certificado em uso nao pode ser destruido. Sem isto, qualquer mudanca de
# nome vira um apply que tenta apagar antes de criar, e falha na metade.
lifecycle {
create_before_destroy = true
}
tags = local.tags
}
# Validacao por DNS: o ACM entrega um par nome/valor de CNAME e observa a zona.
#
# O `for_each` e indexado por `domain_name` por um motivo documentado: para um
# dominio curinga e sua base, o ACM gera o MESMO par nome/valor. Indexar pela
# lista produziria duas entradas identicas e o Terraform reclamaria de recurso
# duplicado.
resource "aws_route53_record" "validacao_borda" {
for_each = {
for o in aws_acm_certificate.borda.domain_validation_options : o.domain_name => {
nome = o.resource_record_name
tipo = o.resource_record_type
valor = o.resource_record_value
}
}
zone_id = data.aws_route53_zone.raiz.zone_id
name = each.value.nome
type = each.value.tipo
records = [each.value.valor]
ttl = 60
# Reexecutar depois de recriar o certificado sobrescreve o registro antigo em
# vez de falhar. Sem isto, o segundo apply para em "registro ja existe".
allow_overwrite = true
}
# Este recurso nao cria nada: ele ESPERA a emissao. Sem ele, a distribuicao
# tenta usar um ARN de certificado ainda pendente e o apply falha.
resource "aws_acm_certificate_validation" "borda" {
provider = aws.borda
certificate_arn = aws_acm_certificate.borda.arn
validation_record_fqdns = [for r in aws_route53_record.validacao_borda : r.fqdn]
timeouts {
# O ACM desiste em 72 h e marca o certificado como "Validation timed out".
# 15 min e paciencia suficiente para erro de zona errada aparecer aqui, em
# vez de tres dias depois.
create = "15m"
}
}
# ── Certificado da ORIGEM: regiao da aplicacao ──────────────────────────────
#
# Por que um nome proprio para a origem, em vez de apontar a distribuicao para
# o nome *.elb.amazonaws.com do balanceador: com HTTPS entre borda e origem, um
# dos nomes do certificado do ALB tem de casar com o nome de dominio
# configurado como origem. O nome da AWS nao esta no seu certificado — e o
# visitante recebe 502.
resource "aws_acm_certificate" "origem" {
domain_name = "origem.${var.dominio}"
validation_method = "DNS"
lifecycle {
create_before_destroy = true
}
tags = local.tags
}
resource "aws_route53_record" "validacao_origem" {
for_each = {
for o in aws_acm_certificate.origem.domain_validation_options : o.domain_name => {
nome = o.resource_record_name
tipo = o.resource_record_type
valor = o.resource_record_value
}
}
zone_id = data.aws_route53_zone.raiz.zone_id
name = each.value.nome
type = each.value.tipo
records = [each.value.valor]
ttl = 60
allow_overwrite = true
}
resource "aws_acm_certificate_validation" "origem" {
certificate_arn = aws_acm_certificate.origem.arn
validation_record_fqdns = [for r in aws_route53_record.validacao_origem : r.fqdn]
}
# O nome da origem tambem e ALIAS: aponta para o ALB do L01 e nao cobra
# consulta. Ele e publico de proposito — o que impede acesso direto nao e
# esconder o nome (seguranca por obscuridade), e sim o grupo de seguranca mais
# o cabecalho secreto que a aplicacao exige.
resource "aws_route53_record" "origem" {
zone_id = data.aws_route53_zone.raiz.zone_id
name = "origem.${var.dominio}"
type = "A"
alias {
name = aws_lb.publico.dns_name
zone_id = aws_lb.publico.zone_id
evaluate_target_health = false
}
}
# O listener 443 do ALB, agora com certificado de verdade. A politica de
# seguranca fecha em TLS 1.2 para cima.
resource "aws_lb_listener" "https" {
load_balancer_arn = aws_lb.publico.arn
port = 443
protocol = "HTTPS"
ssl_policy = "ELBSecurityPolicy-TLS13-1-2-2021-06"
certificate_arn = aws_acm_certificate_validation.origem.certificate_arn
default_action {
type = "forward"
target_group_arn = aws_lb_target_group.api.arn
}
}
Duas coisas que dão errado em silêncio aqui
A primeira: se o registrador do domínio não delegou os servidores de nome para a zona do Route 53, tudo neste arquivo aplica com sucesso e nada resolve. Confira com `dig NS seu-dominio` que os quatro nomes devolvidos são os da zona, e não os do registrador. A segunda: apagar o CNAME de validação depois da emissão não invalida o certificado atual, mas interrompe a renovação automática — a documentação é explícita em dizer que a renovação depende do registro CONTINUAR na zona. O sintoma aparece meses depois, na expiração, quando ninguém lembra de ter apagado nada.
| Onde | Região do certificado | Nome que ele cobre | Por que ali |
|---|---|---|---|
| Distribuição do CloudFront | us-east-1, sempre | `cadencia.com.br` e `www.cadencia.com.br` | exigência da distribuição, documentada e sem exceção regional |
| Listener HTTPS do ALB | região da aplicação (sa-east-1) | `origem.cadencia.com.br` | um dos nomes tem de casar com o nome configurado como origem, senão o visitante recebe 502 |
| Conexão navegador → borda | usa o de us-east-1 | o nome que o visitante digitou | nome alternativo não coberto pelo certificado faz a distribuição recusar a configuração |
| Conexão borda → origem | usa o regional | o nome de domínio da origem | aqui o CloudFront age como cliente TLS e valida o certificado como qualquer cliente |
Construir: o bucket privado, as duas políticas e a distribuição
A ordem de leitura deste arquivo é a ordem das decisões: primeiro o bucket que ninguém alcança, depois a assinatura que faz a borda alcançá-lo, depois as duas políticas de cache — e só então a distribuição, que é onde tudo se amarra.
# borda.tf — o bucket privado, as duas politicas de cache e a distribuicao
#
# `aws_vpc.principal`, `aws_lb.publico` e `aws_lb_target_group.api` vem do L01 e
# do L02: este modulo nao os recria, referencia. Se o seu L01 usa outro nome,
# ajuste as referencias ou exponha-os por `terraform_remote_state`.
data "aws_caller_identity" "atual" {}
# ── Origem do estatico ──────────────────────────────────────────────────────
resource "aws_s3_bucket" "estatico" {
bucket = "${var.projeto}-front-${data.aws_caller_identity.atual.account_id}"
# Sem isto, `terraform destroy` falha com o bucket cheio e voce descobre no
# fim da aula. Em producao, deixe FALSE: apagar bucket com objeto e
# exatamente o acidente que a flag protege.
force_destroy = var.ambiente != "prod"
tags = local.tags
}
# O bucket nunca e publico. Quem serve e a borda; o bucket so responde a ela.
resource "aws_s3_bucket_public_access_block" "estatico" {
bucket = aws_s3_bucket.estatico.id
block_public_acls = true
block_public_policy = true
ignore_public_acls = true
restrict_public_buckets = true
}
# O OAC exige propriedade imposta pelo dono do bucket — e desligar ACL e o
# padrao de bucket novo desde 2023.
resource "aws_s3_bucket_ownership_controls" "estatico" {
bucket = aws_s3_bucket.estatico.id
rule {
object_ownership = "BucketOwnerEnforced"
}
}
resource "aws_s3_bucket_versioning" "estatico" {
bucket = aws_s3_bucket.estatico.id
versioning_configuration {
status = "Enabled"
}
}
# Versionamento sem expiracao cresce sem teto: cada publicacao deixa a versao
# anterior de todo arquivo trocado.
resource "aws_s3_bucket_lifecycle_configuration" "estatico" {
bucket = aws_s3_bucket.estatico.id
rule {
id = "expira-versoes-antigas"
status = "Enabled"
filter {}
noncurrent_version_expiration {
noncurrent_days = 30
}
}
}
# ── Bucket do registro de acesso ────────────────────────────────────────────
#
# ATENCAO, e esta e uma incerteza declarada: a entrega do registro padrao ao S3
# tem exigencia propria de permissao no bucket, e ela mudou entre as versoes de
# registro padrao do CloudFront. Confira a pagina de registro padrao antes de
# assumir que criar o bucket basta — se o log nao aparecer em algumas horas, e
# aqui que a causa esta, nao na distribuicao.
resource "aws_s3_bucket" "logs" {
bucket = "${var.projeto}-logs-${data.aws_caller_identity.atual.account_id}"
force_destroy = var.ambiente != "prod"
tags = local.tags
}
resource "aws_s3_bucket_public_access_block" "logs" {
bucket = aws_s3_bucket.logs.id
block_public_acls = true
block_public_policy = true
ignore_public_acls = true
restrict_public_buckets = true
}
# Registro de acesso guarda endereco IP e caminho: e dado pessoal com prazo, nao
# arquivo eterno. Sem esta regra, e a linha de custo que ninguem ve crescer.
resource "aws_s3_bucket_lifecycle_configuration" "logs" {
bucket = aws_s3_bucket.logs.id
rule {
id = "retencao-de-log"
status = "Enabled"
filter {}
expiration {
days = 90
}
}
}
# ── Controle de acesso de origem: a borda ASSINA cada leitura ───────────────
resource "aws_cloudfront_origin_access_control" "estatico" {
name = "${var.projeto}-front"
origin_access_control_origin_type = "s3"
signing_behavior = "always"
signing_protocol = "sigv4"
}
# A politica do bucket nao autoriza "o CloudFront": autoriza ESTA distribuicao.
# Sem a condicao de ARN de origem, qualquer distribuicao de qualquer conta
# poderia usar seu bucket como origem.
data "aws_iam_policy_document" "estatico" {
statement {
sid = "LeituraSomenteDestaDistribuicao"
effect = "Allow"
principals {
type = "Service"
identifiers = ["cloudfront.amazonaws.com"]
}
actions = ["s3:GetObject"]
# Objeto, nao bucket: `s3:GetObject` opera sobre chave. Nao ha `ListBucket`
# aqui de proposito — a borda nao precisa listar, e sem listagem um caminho
# inexistente devolve 403 em vez de revelar o que existe.
resources = ["${aws_s3_bucket.estatico.arn}/*"]
condition {
test = "StringEquals"
variable = "AWS:SourceArn"
values = [aws_cloudfront_distribution.site.arn]
}
}
}
resource "aws_s3_bucket_policy" "estatico" {
bucket = aws_s3_bucket.estatico.id
policy = data.aws_iam_policy_document.estatico.json
}
# ── Politica de cache do ESTATICO: a chave mais estreita possivel ───────────
resource "aws_cloudfront_cache_policy" "estatico" {
name = "${var.projeto}-estatico"
comment = "Chave so com o caminho. Cookie ou consulta aqui multiplicaria o cache."
# min_ttl = 0 e decisao, nao descuido. Com min_ttl > 0 o CloudFront guarda o
# objeto por aquele tempo MESMO que a origem responda no-cache, no-store ou
# private — a documentacao avisa isso em letras miudas, e e a causa da falha
# mais dificil de diagnosticar deste laboratorio.
min_ttl = 0
# Vale apenas quando a origem NAO manda Cache-Control nem Expires.
default_ttl = 3600
# Teto do que o Cache-Control da origem pode pedir. Um ano cobre o arquivo
# com hash no nome, que e o unico que pede tanto.
max_ttl = 31536000
parameters_in_cache_key_and_forwarded_to_origin {
# Habilitar as duas compressoes normaliza o Accept-Encoding do visitante
# para uma forma canonica antes de entrar na chave. Sem isso, a variedade
# de Accept-Encoding dos navegadores fragmentaria o cache.
enable_accept_encoding_gzip = true
enable_accept_encoding_brotli = true
cookies_config {
cookie_behavior = "none"
}
headers_config {
header_behavior = "none"
}
query_strings_config {
query_string_behavior = "none"
}
}
}
# Politicas gerenciadas, por ID, para a rota dinamica. Nao ha por que manter
# copia local de politica que a AWS mantem.
data "aws_cloudfront_cache_policy" "desligado" {
name = "Managed-CachingDisabled"
}
# AllViewer encaminha TUDO do visitante, inclusive o cabecalho Host. Para uma
# origem ALB isso e o que se quer: a aplicacao ve o nome publico e monta
# redirecionamento e link absoluto com ele. Origem de API Gateway ou URL de
# funcao Lambda exigiria AllViewerExceptHostHeader, porque essas esperam o Host
# da propria origem.
data "aws_cloudfront_origin_request_policy" "todo_visitante" {
name = "Managed-AllViewer"
}
resource "aws_cloudfront_response_headers_policy" "seguranca" {
name = "${var.projeto}-seguranca"
security_headers_config {
strict_transport_security {
access_control_max_age_sec = 31536000
include_subdomains = true
preload = false # so ligue depois de ter certeza de TODO subdominio em HTTPS
override = true
}
content_type_options {
override = true
}
frame_options {
frame_option = "DENY"
override = true
}
referrer_policy {
referrer_policy = "strict-origin-when-cross-origin"
override = true
}
}
# Server-Timing e o instrumento de medicao deste laboratorio: ele diz se a
# resposta veio do cache e de qual camada. Amostragem baixa em producao; a
# prova usa o cabecalho `Pragma: server-timing`, que forca a inclusao em
# qualquer taxa.
server_timing_headers_config {
enabled = true
sampling_rate = 1
}
}
# ── A distribuicao ──────────────────────────────────────────────────────────
resource "aws_cloudfront_distribution" "site" {
enabled = true
is_ipv6_enabled = true
comment = "${var.projeto} — front e API no mesmo dominio"
aliases = [var.dominio, "www.${var.dominio}"]
# Sem isto, a raiz do site responde 403: o CloudFront pediria o objeto de
# chave vazia ao bucket.
default_root_object = "index.html"
# A classe de preco restringe de quais pontos de presenca o conteudo e
# servido. Restringir reduz o custo de transferencia e AUMENTA a latencia de
# quem esta fora das regioes mantidas — e o publico de Lisboa e Luanda e
# justamente o motivo deste laboratorio.
price_class = "PriceClass_All"
origin {
origin_id = "estatico"
domain_name = aws_s3_bucket.estatico.bucket_regional_domain_name
origin_access_control_id = aws_cloudfront_origin_access_control.estatico.id
}
origin {
origin_id = "api"
domain_name = "origem.${var.dominio}" # NAO o nome *.elb.amazonaws.com: veja dns-tls.tf
custom_origin_config {
http_port = 80
https_port = 443
origin_protocol_policy = "https-only"
origin_ssl_protocols = ["TLSv1.2"]
}
# Prova de que a requisicao veio DESTA distribuicao. A lista de prefixo
# gerenciada do CloudFront no grupo de seguranca do ALB barra o que nao e
# CloudFront; ela nao distingue a sua borda da borda de outra pessoa. Este
# cabecalho distingue.
custom_header {
name = "X-Origem-Autorizada"
value = data.aws_secretsmanager_secret_version.origem.secret_string
}
}
# Comportamento CORINGA: o estatico. Ele e sempre o ultimo a ser avaliado,
# independentemente da ordem no arquivo.
default_cache_behavior {
target_origin_id = "estatico"
viewer_protocol_policy = "redirect-to-https"
allowed_methods = ["GET", "HEAD", "OPTIONS"]
cached_methods = ["GET", "HEAD"]
compress = true
cache_policy_id = aws_cloudfront_cache_policy.estatico.id
response_headers_policy_id = aws_cloudfront_response_headers_policy.seguranca.id
}
# Comportamento ORDENADO: o primeiro padrao que casa vence, e este e avaliado
# antes do coringa. Inverter a ordem faria a API ser armazenada na borda —
# que e como resposta de um usuario chega a outro.
ordered_cache_behavior {
path_pattern = "/api/*"
target_origin_id = "api"
viewer_protocol_policy = "https-only"
allowed_methods = ["GET", "HEAD", "OPTIONS", "PUT", "POST", "PATCH", "DELETE"]
cached_methods = ["GET", "HEAD"]
compress = true
cache_policy_id = data.aws_cloudfront_cache_policy.desligado.id
origin_request_policy_id = data.aws_cloudfront_origin_request_policy.todo_visitante.id
response_headers_policy_id = aws_cloudfront_response_headers_policy.seguranca.id
}
viewer_certificate {
acm_certificate_arn = aws_acm_certificate_validation.borda.certificate_arn
ssl_support_method = "sni-only"
# Corta TLS 1.0 e 1.1. O padrao historico e mais permissivo, e nao ha
# motivo para aceita-lo num site novo.
minimum_protocol_version = "TLSv1.2_2021"
}
restrictions {
geo_restriction {
restriction_type = "none"
}
}
# NAO ha custom_error_response aqui, e isso e decisao com motivo: pagina de
# erro customizada e configuracao da DISTRIBUICAO, nao do comportamento.
# Mapear 404 para /index.html com 200 — a receita comum de SPA — faria o 404
# legitimo de /api/pedidos/999 virar HTML com status 200. O roteamento de SPA
# na borda e o L20, e la o assunto ganha secao propria.
logging_config {
bucket = aws_s3_bucket.logs.bucket_domain_name
prefix = "cloudfront/"
include_cookies = false # cookie em log de acesso e dado pessoal a mais para guardar
}
tags = local.tags
}
# Metricas de taxa de acerto e latencia de origem NAO vem de graca: e uma
# assinatura por distribuicao, cobrada por metrica-mes. Sem ela, a pergunta
# "qual e minha taxa de acerto?" so tem resposta pelo registro de acesso.
resource "aws_cloudfront_monitoring_subscription" "site" {
distribution_id = aws_cloudfront_distribution.site.id
monitoring_subscription {
realtime_metrics_subscription_config {
realtime_metrics_subscription_status = "Enabled"
}
}
}
# ── O nome no apice, e o www ────────────────────────────────────────────────
#
# ALIAS, nao CNAME: nao existe CNAME no apice de uma zona. E consulta por ALIAS
# a recurso da AWS nao e cobrada, enquanto a de CNAME e.
resource "aws_route53_record" "apice" {
for_each = toset(["A", "AAAA"])
zone_id = data.aws_route53_zone.raiz.zone_id
name = var.dominio
type = each.value
alias {
name = aws_cloudfront_distribution.site.domain_name
zone_id = aws_cloudfront_distribution.site.hosted_zone_id
evaluate_target_health = false
}
}
resource "aws_route53_record" "www" {
for_each = toset(["A", "AAAA"])
zone_id = data.aws_route53_zone.raiz.zone_id
name = "www.${var.dominio}"
type = each.value
alias {
name = aws_cloudfront_distribution.site.domain_name
zone_id = aws_cloudfront_distribution.site.hosted_zone_id
evaluate_target_health = false
}
}
output "dominio_publico" {
value = "https://${var.dominio}"
}
output "distribuicao_id" {
value = aws_cloudfront_distribution.site.id
}
output "bucket_estatico" {
value = aws_s3_bucket.estatico.id
}
output "regiao" {
value = var.regiao
}
Por que a política do estático tem TTL mínimo em zero
Parece contraintuitivo num comportamento cujo objetivo é guardar por muito tempo. O motivo está numa advertência da documentação: com TTL mínimo maior que zero, o CloudFront guarda o objeto por pelo menos aquele tempo MESMO que a origem responda `no-cache`, `no-store` ou `private`. Ou seja, o TTL mínimo é um mecanismo para ignorar a origem. Quem precisa de validade longa consegue isso pelo `Cache-Control` da origem, que é revisável onde o arquivo é publicado; quem usa TTL mínimo para o mesmo fim ganha, de brinde, a impossibilidade de tirar algo do ar rapidamente.
| Comportamento | Política de cache | Chave de cache | Política de requisição | Por quê |
|---|---|---|---|---|
| `*` (coringa, estático) | própria, TTL máximo de um ano | só o caminho, com `Accept-Encoding` normalizado | nenhuma | uma entrada por objeto é o teto de taxa de acerto; o S3 não precisa de nada do visitante |
| `/api/*` | Managed-CachingDisabled | irrelevante: nada é guardado | Managed-AllViewer | a resposta depende de quem pergunta, e a aplicação precisa do cookie, do `Authorization` e do `Host` público |
O aviso da documentação que explica por que `/api/*` não pode ter cache
A página de cabeçalhos customizados de origem traz um alerta que vale reproduzir com precisão: se você encaminha o cabeçalho `Authorization` à origem SEM incluí-lo na chave de cache, o CloudFront pode servir a mesma resposta armazenada tanto a visitante autorizado quanto a não autorizado. As duas saídas legítimas são incluir o `Authorization` na chave — o que cria uma entrada por token e destrói a taxa de acerto — ou desligar o armazenamento onde a autorização é processada na origem. Este laboratório escolhe a segunda, e é por isso que a política de `/api/*` é `Managed-CachingDisabled` e não uma política com validade curta: validade curta ainda é validade, e um segundo de reuso já é vazamento.
A diferença entre as duas políticas, na frase mais curta possível
A política de cache define o que entra na CHAVE — e tudo o que entra na chave é também encaminhado à origem. A política de requisição de origem define o que é encaminhado SEM entrar na chave. É por isso que a segunda existe: a aplicação precisa do cookie de sessão para responder, e o cookie de sessão não pode fazer parte da identidade do objeto armazenado. Confundir as duas produz os dois piores resultados possíveis — taxa de acerto destruída, se você põe na chave o que devia só encaminhar; ou aplicação sem os dados de que precisa, se você não encaminha nada.
Construir: fechar a origem para tudo que não é a borda
Duas medidas, e elas provam coisas diferentes. A lista de prefixo gerenciada prova que a requisição veio de um servidor de origem do CloudFront; o cabeçalho secreto prova que veio da SUA distribuição. Só a primeira deixaria a origem acessível por qualquer pessoa capaz de criar uma distribuição apontando para o seu nome de origem.
# origem-fechada.tf — a origem so aceita quem veio da borda
# O segredo do cabecalho vem do Secrets Manager, nunca do codigo nem de
# `variable` com default. Sinceridade necessaria: o valor lido aqui FICA no
# estado do Terraform. Estado em S3 com criptografia e acesso restrito nao e
# opcional a partir do momento em que voce le segredo em `data`.
data "aws_secretsmanager_secret" "origem" {
name = "${var.projeto}/borda/cabecalho-de-origem"
}
data "aws_secretsmanager_secret_version" "origem" {
secret_id = data.aws_secretsmanager_secret.origem.id
}
# A lista de prefixo gerenciada do CloudFront contem os enderecos dos
# servidores da borda que falam com origem. Ela e mantida pela AWS: nao ha
# lista de IP para atualizar a mao.
data "aws_ec2_managed_prefix_list" "cloudfront" {
name = "com.amazonaws.global.cloudfront.origin-facing"
}
data "aws_ec2_managed_prefix_list" "cloudfront_ipv6" {
name = "com.amazonaws.global.ipv6.cloudfront.origin-facing"
}
resource "aws_security_group" "alb" {
name = "${var.projeto}-alb"
description = "Entrada apenas da borda do CloudFront"
vpc_id = aws_vpc.principal.id
tags = local.tags
}
resource "aws_vpc_security_group_ingress_rule" "https_da_borda" {
security_group_id = aws_security_group.alb.id
from_port = 443
to_port = 443
ip_protocol = "tcp"
prefix_list_id = data.aws_ec2_managed_prefix_list.cloudfront.id
description = "HTTPS somente dos servidores de origem do CloudFront (IPv4)"
}
resource "aws_vpc_security_group_ingress_rule" "https_da_borda_v6" {
security_group_id = aws_security_group.alb.id
from_port = 443
to_port = 443
ip_protocol = "tcp"
prefix_list_id = data.aws_ec2_managed_prefix_list.cloudfront_ipv6.id
description = "HTTPS somente dos servidores de origem do CloudFront (IPv6)"
}
# Nao ha regra para a porta 80: o redirecionamento acontece na borda, e o ALB
# nao precisa ouvir HTTP. Uma porta que ninguem escuta e uma porta que ninguem
# testa por engano.
Por que o cabeçalho não é falsificável pelo cliente
A dúvida é legítima: se o cabeçalho viaja numa requisição HTTP, por que não basta o atacante enviá-lo? Porque ele não passa. A documentação é explícita: se o cabeçalho já está presente na requisição do visitante, o CloudFront SOBRESCREVE o valor antes de encaminhar à origem. Quem tenta forjar consegue apenas fazer a distribuição substituir o valor forjado pelo verdadeiro — e quem não passa pela distribuição não chega ao ALB, porque o grupo de segurança só aceita as faixas de origem do CloudFront. Controlar acesso por cabeçalho customizado é, aliás, um dos usos que a própria documentação lista para o recurso.
Onde o segredo aparece, e por que isso precisa ser dito
Ler segredo em `data` no Terraform o coloca no arquivo de estado, em texto claro. Isso não é defeito da abordagem, é característica dela — e a resposta não é evitar o `data`, é tratar o estado como material sensível: bucket com criptografia, versionamento, acesso restrito por política e trilha de auditoria. Quem não faz isso tem o segredo do cabeçalho de origem, e provavelmente todos os outros, num arquivo que qualquer pessoa com acesso de leitura ao bucket de estado lê.
Construir: o que muda na aplicação .NET 8
A mudança mais importante no código é uma remoção: UseStaticFiles sai, e a pasta wwwroot deixa de existir na imagem. O que entra é menos do que se imagina — dois middlewares e uma configuração de proxy.
// Program.cs — o que muda na aplicacao quando o estatico sai dela
using System.Security.Cryptography;
using System.Text;
using Microsoft.AspNetCore.HttpOverrides;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
builder.Services.AddHealthChecks();
// Dois saltos de proxy na frente: o CloudFront acrescenta o endereco do
// visitante em X-Forwarded-For, e o ALB acrescenta o do proprio ponto de
// presenca. Sem ForwardLimit = 2, o ASP.NET Core para no primeiro salto e
// registra a borda como se fosse o cliente.
//
// Este numero e derivado da topologia, nao lido em documentacao: confira uma
// vez, registrando o valor bruto do cabecalho, antes de confiar nele.
builder.Services.Configure<ForwardedHeadersOptions>(o =>
{
o.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
o.ForwardLimit = 2;
// A rede da VPC e o ALB nao tem endereco fixo conhecido; limpar as listas e
// aceitar o cabecalho e seguro AQUI porque o grupo de seguranca so aceita
// trafego da borda. Numa origem exposta a internet, isto seria falsificavel.
o.KnownNetworks.Clear();
o.KnownProxies.Clear();
});
var app = builder.Build();
app.UseForwardedHeaders();
// ATENCAO ao que NAO esta aqui: nao ha `app.UseStaticFiles()` e nao existe
// mais pasta wwwroot na imagem. O estatico e publicado no S3 por copia de
// arquivo, sem construir imagem e sem rollout. Foi este apagamento que
// separou o ciclo de vida do front do ciclo de vida da API.
// O cabecalho secreto e conferido antes de qualquer rota de negocio. Ele nao
// substitui o grupo de seguranca: a lista de prefixo prova que a requisicao
// veio do CloudFront, e este cabecalho prova que veio da NOSSA distribuicao.
var segredoEsperado = builder.Configuration["Borda:CabecalhoDeOrigem"]
?? throw new InvalidOperationException(
"Borda:CabecalhoDeOrigem ausente. Ele vem do Secrets Manager pela chave "
+ "`secrets` da definicao de container, nunca por `environment`.");
app.Use(async (ctx, next) =>
{
// A verificacao de saude vem do ALB, nao da borda — e o ALB nao manda o
// cabecalho. Exigi-lo aqui derrubaria todas as tasks saudaveis, que e o
// jeito mais rapido de transformar um controle de seguranca num incidente.
var caminho = ctx.Request.Path.Value ?? string.Empty;
if (caminho.StartsWith("/health", StringComparison.Ordinal))
{
await next();
return;
}
var recebido = ctx.Request.Headers["X-Origem-Autorizada"].ToString();
// Comparacao em tempo fixo: comparar segredo com == vaza o tamanho do
// prefixo correto pelo tempo de resposta.
var iguais = CryptographicOperations.FixedTimeEquals(
Encoding.UTF8.GetBytes(recebido),
Encoding.UTF8.GetBytes(segredoEsperado));
if (!iguais)
{
ctx.Response.StatusCode = StatusCodes.Status403Forbidden;
return; // sem corpo: nao ha o que informar a quem nao deveria estar aqui
}
await next();
});
// Toda resposta de /api sai marcada como nao armazenavel. Isto e cinto duplo:
// o comportamento de /api/* ja usa a politica Managed-CachingDisabled. O
// cabecalho protege o dia em que alguem trocar a politica sem ler este arquivo
// — e protege tambem o cache do NAVEGADOR e o de qualquer proxy no caminho,
// que a politica do CloudFront nao alcanca.
app.Use(async (ctx, next) =>
{
if (ctx.Request.Path.StartsWithSegments("/api"))
{
ctx.Response.OnStarting(() =>
{
ctx.Response.Headers.CacheControl = "no-store, private";
return Task.CompletedTask;
});
}
await next();
});
// Vivacidade sem dependencia; prontidao com dependencia. Vem do L03 e continua
// valendo: sao perguntas diferentes.
app.MapHealthChecks("/health/live");
app.MapHealthChecks("/health/ready");
app.MapControllers();
app.Run();
No controlador, o ponto que mais ensina é o contraexemplo: existe uma rota de API que MERECERIA cache na borda, e reconhecer isso desmonta a regra preguiçosa de que "API não se cacheia".
// PedidosController.cs — a resposta que a borda pode reutilizar, e a que nao pode
using Microsoft.AspNetCore.Mvc;
[ApiController]
[Route("api/[controller]")]
public sealed class PedidosController : ControllerBase
{
private readonly IPedidoRepositorio _repo;
public PedidosController(IPedidoRepositorio repo) => _repo = repo;
// Resposta por usuario: NUNCA armazenavel em cache compartilhado. O
// middleware ja marca no-store; a anotacao deixa a intencao visivel a quem
// le so o controlador.
[HttpGet("{id:guid}")]
[ResponseCache(NoStore = true, Location = ResponseCacheLocation.None)]
public async Task<IActionResult> Obter(Guid id, CancellationToken ct)
{
var pedido = await _repo.ObterAsync(id, ct);
return pedido is null ? NotFound() : Ok(pedido);
}
// Contraexemplo deliberado: catalogo publico e igual para todo mundo. Esta
// e a unica rota de API deste laboratorio que MERECERIA um comportamento
// com cache — e ela mostra que "API nao se cacheia" e regra preguicosa.
//
// Para aproveitar de fato, seria preciso um terceiro comportamento em
// /api/catalogo* com politica propria. Nao foi criado aqui porque o
// requisito declarado nao pede: acrescentar seria adorno, e a secao de
// evolucao registra isso como o passo do nivel 4.
[HttpGet("/api/catalogo")]
public async Task<IActionResult> Catalogo(CancellationToken ct)
{
var itens = await _repo.CatalogoAsync(ct);
// s-maxage fala com o cache COMPARTILHADO (a borda); max-age fala com o
// navegador. Poder dar tempos diferentes aos dois e o motivo de a
// diretiva existir. stale-while-revalidate deixa a borda servir o valor
// vencido enquanto busca o novo, o que remove o pico de latencia da
// expiracao.
Response.Headers.CacheControl = "public, max-age=60, s-maxage=300, stale-while-revalidate=60";
return Ok(itens);
}
}
Por que a rota de saúde está isenta do cabeçalho secreto
A verificação de saúde do grupo de destino sai do balanceador, não da borda — então ela nunca traz o cabeçalho que a distribuição injeta. Exigir o segredo em `/health` faria todas as tasks serem marcadas insalubres e substituídas em laço, o que transforma um controle de segurança em indisponibilidade total em poucos minutos. É o tipo de detalhe que só aparece na primeira implantação, e sempre em produção.
Construir: publicar o front sem invalidar nada
A publicação inteira é cópia de arquivo, e a ordem das duas cópias importa. O que dispensa a invalidação é o hash no nome: um arquivo cujo nome muda quando o conteúdo muda nunca precisa ser removido do cache, porque o nome antigo continua correto para o conteúdo antigo.
#!/usr/bin/env bash
# publicar-front.sh — a publicacao que dispensa invalidacao
set -euo pipefail
PROJETO="${PROJETO:-ffv-lab}"
BUCKET=$(terraform output -raw bucket_estatico)
DIST=$(terraform output -raw distribuicao_id)
# 1. O build grava o hash do conteudo no nome de cada arquivo:
# dist/assets/app.4f2a91c8.js
# dist/assets/estilo.9b17ee02.css
# Vite, webpack e esbuild fazem isso por padrao. Se o seu build nao faz,
# ligar essa opcao e o pre-requisito de todo o resto deste roteiro.
npm run build
# 2. Primeiro os ARQUIVOS COM HASH, com um ano de validade e `immutable`.
# A ordem importa: se o index.html subisse primeiro, ele apontaria por
# alguns segundos para arquivos que ainda nao existem — e quem visitasse
# nesse intervalo veria a pagina quebrada.
aws s3 sync dist/assets/ "s3://${BUCKET}/assets/" \
--cache-control "public, max-age=31536000, immutable" \
--delete=false
# 3. Depois o index.html, com validade curta. Ele e o unico arquivo cujo NOME
# nao muda, entao e o unico que precisa expirar rapido.
aws s3 cp dist/index.html "s3://${BUCKET}/index.html" \
--cache-control "public, max-age=60, s-maxage=60" \
--content-type "text/html; charset=utf-8"
# 4. Invalidacao de UM caminho, nao de `/*`. O `/*` apaga tudo, inclusive os
# arquivos com hash que jamais mudarao, e obriga a borda a rebuscar o build
# inteiro em todos os pontos de presenca. Ha franquia mensal de caminhos e
# cobranca acima dela; e o desperdicio maior nao esta na fatura, esta no
# pico de latencia que todo visitante paga depois.
aws cloudfront create-invalidation \
--distribution-id "$DIST" \
--paths "/index.html" "/" \
--query "Invalidation.{Id:Id,Status:Status}" --output table
# 5. Nao remova o build anterior no mesmo instante: quem carregou o index.html
# antigo nos ultimos 60 s ainda vai pedir os arquivos antigos. Expire por
# ciclo de vida, com folga, em vez de apagar na publicacao.
echo "Publicado. Os arquivos da versao anterior expiram pela regra de 30 dias."
| Arquivo | Nome muda com o conteúdo? | `Cache-Control` publicado | Precisa de invalidação? |
|---|---|---|---|
| `/assets/app.4f2a91c8.js` | sim, o hash é do conteúdo | `public, max-age=31536000, immutable` | nunca |
| `/assets/estilo.9b17ee02.css` | sim | `public, max-age=31536000, immutable` | nunca |
| `/index.html` | não, o nome é fixo | `public, max-age=60, s-maxage=60` | só ele, e só quando a pressa justifica os 60 s |
| `/api/pedidos/{id}` | não se aplica | `no-store, private` | não, porque nada foi armazenado |
| `/api/catalogo` | não se aplica | `public, max-age=60, s-maxage=300, stale-while-revalidate=60` | não: a validade curta já resolve, e invalidar rota dinâmica é sinal de desenho errado |
Os 60 s do index.html não são desperdício, são orçamento
Com validade de um minuto, uma publicação chega a todo mundo em no máximo um minuto sem nenhuma invalidação — e o `index.html` é um único arquivo pequeno, então a origem é consultada uma vez por minuto por ponto de presença, não uma vez por visita. O número sai da pergunta "quanto tempo você aceita esperar por uma correção urgente?". Um minuto é confortável; cinco também; um dia não, e é o que o TTL padrão da política daria se o cabeçalho fosse esquecido.
Implantar, e provar que a borda está fazendo o trabalho
Cinco provas, cada uma com um resultado que aprova e uma leitura do que a reprovação significa. A terceira e a quarta são as que separam "tem CloudFront na frente" de "tem cache funcionando", que são situações diferentes e parecem idênticas de fora.
# provas.sh — cinco medicoes. Nenhuma conclusao vem de "abriu no navegador"
set -euo pipefail
DOMINIO="cadencia.com.br"
DIST=$(terraform output -raw distribuicao_id)
echo "== Prova 1 — o certificado da borda esta em us-east-1 e foi emitido =="
# A distribuicao recusa ARN de certificado de outra regiao no proprio apply.
# Se este comando nao encontra nada, o certificado foi pedido na regiao errada.
aws acm list-certificates --region us-east-1 \
--query "CertificateSummaryList[?DomainName=='${DOMINIO}'].[DomainName,Status]" \
--output table
# APROVA: uma linha com ISSUED.
# REPROVA com PENDING_VALIDATION: o CNAME de validacao nao esta na zona. O ACM
# desiste em 72 h e o certificado vira VALIDATION_TIMED_OUT — a partir dai nao
# ha o que consertar, e sim um certificado novo a pedir.
echo "== Prova 2 — o apice resolve para a borda, e o registro e ALIAS =="
dig +short "${DOMINIO}" A | head -3
# APROVA: enderecos IP. Se aparecesse um nome, seria CNAME — e CNAME no apice
# nao existe.
ZONA=$(aws route53 list-hosted-zones-by-name --dns-name "${DOMINIO}" \
--query "HostedZones[0].Id" --output text)
aws route53 list-resource-record-sets --hosted-zone-id "$ZONA" \
--query "ResourceRecordSets[?Name=='${DOMINIO}.'].[Type,AliasTarget.DNSName]" \
--output table
# APROVA: A e AAAA com AliasTarget apontando para *.cloudfront.net.
# REPROVA com AliasTarget nulo: e registro comum, e voce esta pagando consulta
# que o ALIAS nao cobraria.
echo "== Prova 3 — o mesmo arquivo, duas vezes: miss e depois hit =="
ARQ=$(curl -s "https://${DOMINIO}/" | grep -o '/assets/[a-z]*\.[a-z0-9]*\.js' | head -1)
for i in 1 2; do
curl -s -o /dev/null -D - -H "Pragma: server-timing" "https://${DOMINIO}${ARQ}" \
| grep -i -E "^(server-timing|x-cache|age):"
done
# APROVA: a primeira execucao traz `cdn-cache-miss`; a segunda, `cdn-cache-hit`
# com `cdn-hit-layer;desc="EDGE"` e um cabecalho Age crescente.
# O `Pragma: server-timing` forca o cabecalho mesmo com amostragem em 1%.
# REPROVA se as duas derem miss: ou a origem esta mandando no-store no
# estatico, ou a chave de cache inclui algo que muda entre as duas chamadas.
echo "== Prova 4 — a rota de API NUNCA e reutilizada entre usuarios =="
for cookie in "sessao=aaa" "sessao=bbb"; do
curl -s -o /dev/null -D - -H "Pragma: server-timing" -H "Cookie: ${cookie}" \
"https://${DOMINIO}/api/pedidos/00000000-0000-0000-0000-000000000000" \
| grep -i -E "^(HTTP|server-timing|cache-control):"
done
# APROVA: as duas com `cdn-cache-miss` e `cache-control: no-store, private`.
# Dois cookies diferentes, de proposito: repetir a MESMA chamada nao provaria
# nada, porque o que se quer descartar e reuso ENTRE usuarios.
# REPROVA com um `cdn-cache-hit`: a ordem dos comportamentos esta invertida e
# resposta de um usuario pode chegar a outro. Pare tudo e corrija.
#
# Repare no que este teste NAO consegue fazer: forjar o cabecalho de origem.
# Se voce mandar `-H "X-Origem-Autorizada: qualquer-coisa"`, a distribuicao
# SOBRESCREVE o valor antes de encaminhar — a documentacao diz que, quando o
# cabecalho ja esta na requisicao do visitante, o CloudFront reescreve o valor.
# E justamente essa sobrescrita que torna o cabecalho um controle util.
echo "== Prova 5 — nem o bucket nem a origem sao alcancaveis por fora =="
BUCKET=$(terraform output -raw bucket_estatico)
REGIAO=$(terraform output -raw regiao)
curl -s -o /dev/null -w "%{http_code}\n" \
"https://${BUCKET}.s3.${REGIAO}.amazonaws.com${ARQ}"
# APROVA: 403. O objeto existe e nao e servido — quem serve e a distribuicao.
# REPROVA com 200: bloqueio de acesso publico ou politica do bucket estao
# frouxos, e o WAF do L47 seria contornavel por quem soubesse o nome do bucket.
# A origem dinamica, pelo nome dela, direto — sem passar pela borda:
curl -s -o /dev/null -w "%{http_code}\n" --max-time 8 \
"https://origem.${DOMINIO}/api/catalogo" || echo "tempo esgotado (esperado)"
# APROVA: tempo esgotado, porque o grupo de seguranca so aceita as faixas de
# origem do CloudFront — a conexao nem chega a ser aceita.
# REPROVA com 200 ou 403: 200 significa que a regra de entrada esta aberta e
# qualquer pessoa fala com o ALB; 403 significa que a conexao FOI aceita e a
# aplicacao recusou — o cabecalho salvou voce, e o grupo de seguranca esta
# frouxo. As duas leituras sao diferentes, e so a primeira e catastrofe.
echo "== Medicao de fechamento — taxa de acerto POR CAMINHO =="
# A metrica CacheHitRate da um numero unico para a distribuicao inteira, e ele
# mente por construcao: /api/* com 0% de acerto derruba a media do estatico.
# O registro de acesso e o unico lugar onde a pergunta certa cabe.
aws cloudwatch get-metric-statistics \
--namespace AWS/CloudFront --metric-name CacheHitRate \
--dimensions Name=DistributionId,Value="$DIST" Name=Region,Value=Global \
--start-time "$(date -u -d '1 hour ago' +%Y-%m-%dT%H:%M:%SZ)" \
--end-time "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
--period 300 --statistics Average --region us-east-1 \
--query "Datapoints[].Average" --output text
# Repare no `--region us-east-1`: metrica de CloudFront publica sempre ali,
# qualquer que seja a regiao da aplicacao. Procurar em sa-east-1 devolve lista
# vazia e a conclusao errada de que a metrica nao existe.
| Prova | Comando | Resultado que aprova | O que a reprovação significa |
|---|---|---|---|
| 1 · Certificado na região certa | `acm list-certificates --region us-east-1` | uma linha com `ISSUED` | `PENDING_VALIDATION` é CNAME ausente na zona; nada listado é certificado pedido na região da aplicação |
| 2 · Ápice resolve por ALIAS | `dig +short` e `list-resource-record-sets` | endereços IP, e `AliasTarget` apontando para `*.cloudfront.net` | nome em vez de IP indica CNAME (impossível no ápice); `AliasTarget` nulo é registro comum, e a consulta passa a ser cobrada |
| 3 · O estático vira acerto na segunda vez | `curl` com `Pragma: server-timing`, duas vezes | `cdn-cache-miss` e depois `cdn-cache-hit` com camada `EDGE`, com `Age` crescendo | dois miss seguidos: a origem manda `no-store`, ou a chave inclui algo que varia entre as duas chamadas |
| 4 · A API nunca é reutilizada | duas chamadas com cookies diferentes | os dois `cdn-cache-miss`, com `cache-control: no-store, private` | um `cdn-cache-hit` aqui é vazamento de resposta entre usuários: a ordem dos comportamentos está invertida |
| 5 · Nem o bucket nem a origem respondem por fora | `curl` no endereço regional do bucket e no nome da origem | 403 no bucket e tempo esgotado na origem | 200 no bucket é acesso público frouxo; 403 na origem significa que a conexão foi aceita e só a aplicação recusou — o grupo de segurança está aberto |
| Fechamento · Acerto por caminho | registro de acesso, não a métrica única | estático acima de 90%, `/api/*` em 0% | a métrica única da distribuição é a média das duas e não responde nada |
O número que a Cadência mediu, e o que ele não prova
Depois da mudança: primeiro carregamento de Lisboa em 1,2 s (era 4,8 s), acerto de 96,4% no estático e 0% em `/api/*` — como projetado. O tempo até o primeiro byte de um arquivo em cache caiu de 210 ms para 24 ms, e é aí que está o ganho real: não se acelerou nada, deixou-se de atravessar o oceano. O que esses números NÃO provam é desempenho da aplicação: a rota de pedido continua exatamente tão rápida quanto era, porque ela nunca foi o problema. Confundir as duas coisas é como o time erra a próxima decisão de otimização.
Sua aplicação roda em `sa-east-1`. Você pediu um certificado do ACM em `sa-east-1` para `loja.exemplo.com`, ele está `ISSUED`, e a distribuição do CloudFront não o aceita. Qual é a causa?
Quebrar de propósito: três falhas e o diagnóstico
As três acontecem de verdade, e nenhuma delas produz erro visível na página. É por isso que sabê-las reconhecer vale mais do que sabê-las evitar: você vai encontrá-las em sistema que outra pessoa montou.
| Falha | Como provocar | Sintoma | Onde olhar | Correção |
|---|---|---|---|---|
| Cookie na chave de cache do estático | troque `cookie_behavior` para `"all"` na política do estático e navegue com sessão | a página continua correta e a taxa de acerto cai para perto de zero; a fatura de requisições ao S3 cresce com o número de visitas | `Server-Timing` com `cdn-cache-miss` em toda recarga; `CacheHitRate` despencando | chave com o caminho apenas; o que a aplicação precisa receber e não deve identificar o objeto vai na política de requisição de origem |
| Origem apontada para o nome do balanceador | troque `domain_name` da origem `api` para o nome `*.elb.amazonaws.com` | 502 em toda chamada de API, com log da aplicação absolutamente limpo | `x-edge-detailed-result-type` no registro de acesso; a documentação prevê o 502 quando nenhum nome do certificado casa com o nome da origem | nome próprio para a origem, coberto pelo certificado regional |
| Publicar sobrescrevendo o mesmo nome de arquivo | copie um `app.js` novo sobre o antigo, mantendo `max-age=31536000` | parte dos usuários fica com o código antigo por até um ano, e a mistura de versões dá tela branca que "só acontece com alguns" | compare o hash do arquivo no bucket com o que a borda devolve; `Age` alto num arquivo recém-publicado | hash no nome; e, para o estrago já feito, invalidar aquele caminho — uma vez, não como hábito |
A quarta falha, que é a mais grave e a mais silenciosa
Inverta a ordem e ponha um comportamento amplo antes de `/api/*` — por exemplo `/a*` para uma pasta chamada `arquivos`. O primeiro padrão que casa vence, então `/api/pedidos` passa a ser tratado pelo comportamento do estático: armazenado, com chave sem cookie. A partir desse instante, a resposta do pedido de um lojista é servida ao próximo que pedir a mesma URL. Nada quebra, nada alarma, e a página funciona. A prova 4 existe exatamente para pegar isso, e é a razão de ela usar dois cookies diferentes em vez de repetir a mesma chamada.
Segurança: o que muda quando existe uma borda
A borda desloca a superfície de ataque em vez de reduzi-la: passa a existir um lugar que fala com a internet inteira e que não é a sua aplicação. Dois riscos aparecem por causa disso, e um desaparece.
| Risco | Probabilidade | Impacto | Controle preventivo | Detecção | Resposta |
|---|---|---|---|---|---|
| Resposta autenticada armazenada e servida a outro usuário | baixa | crítico | armazenamento desligado em `/api/*`, `no-store` na origem e ordem de comportamento revisada em cada mudança | registro de acesso com `x-edge-result-type=Hit` em caminho de `/api/` | invalidar o caminho, corrigir a ordem, e tratar como incidente de dados — porque é |
| Origem alcançada por fora da borda | média | alto | lista de prefixo gerenciada no grupo de segurança e cabeçalho secreto conferido na aplicação | log do ALB com requisição sem o cabeçalho; contagem de 403 da aplicação | rotacionar o segredo do cabeçalho e conferir se a porta 80 do ALB foi reaberta |
| Bucket do estático exposto | baixa | médio | bloqueio de acesso público nas quatro chaves, propriedade imposta pelo dono e política com condição de ARN da distribuição | Access Analyzer para S3; alerta de configuração de bloqueio alterada | restaurar o bloqueio e revisar quem tem `s3:PutBucketPolicy` |
| Certificado expira porque a renovação parou | média | alto | CNAME de validação permanece na zona, e a zona é administrada por Terraform | notificação de expiração do ACM; verificação sintética do TLS | republicar o CNAME e pedir certificado novo se já expirou — não há como acelerar validação |
| TLS antigo aceito por visitante | baixa | médio | versão mínima `TLSv1.2_2021` na distribuição e política de segurança equivalente no ALB | campo `ssl-protocol` no registro de acesso | elevar a versão mínima e medir quanto tráfego legítimo se perde antes, não depois |
| Registro de acesso guardando dado pessoal sem prazo | alta | médio | cookies desligados no registro e regra de ciclo de vida no bucket de logs | crescimento do bucket de logs; revisão de retenção | apagar o histórico excedente e fixar a retenção pela política de privacidade, não pelo espaço em disco |
// A politica do papel que publica o front. Ela e o novo caminho pelo qual
// codigo entra em producao, entao merece o mesmo rigor da politica de deploy.
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "EscreveSomenteNoPrefixoDoFront",
"Effect": "Allow",
"Action": ["s3:PutObject", "s3:DeleteObject"],
"Resource": "arn:aws:s3:::ffv-lab-front-111122223333/*"
},
{
// ListBucket opera sobre o BUCKET, nao sobre a chave: por isso o ARN
// aqui nao tem "/*". Confundir os dois e a causa mais comum de
// "AccessDenied" em politica que parece correta.
"Sid": "ListaParaOSyncCompararTimestamp",
"Effect": "Allow",
"Action": "s3:ListBucket",
"Resource": "arn:aws:s3:::ffv-lab-front-111122223333"
},
{
// A invalidacao aceita recurso: restrinja a ESTA distribuicao.
"Sid": "InvalidaSomenteEstaDistribuicao",
"Effect": "Allow",
"Action": "cloudfront:CreateInvalidation",
"Resource": "arn:aws:cloudfront::111122223333:distribution/E1PDQ2EXAMPLE"
},
{
// O unico "*" desta politica, e ele tem motivo: as acoes de LISTAGEM
// do CloudFront nao operam sobre um recurso — voce lista para
// descobrir quais recursos existem, entao nao ha ARN a informar. Se
// o seu roteiro descobre o ID da distribuicao de outra forma (saida
// do Terraform, por exemplo), APAGUE esta declaracao: ela deixa de
// ter justificativa no momento em que deixa de ser usada.
"Sid": "DescobreADistribuicaoQuandoOIdNaoEConhecido",
"Effect": "Allow",
"Action": "cloudfront:ListDistributions",
"Resource": "*"
}
]
}O risco que a borda ELIMINA, e vale registrar
Antes, o endereço público era o do balanceador, e qualquer pessoa na internet podia abrir conexão diretamente com ele. Depois desta arquitetura, o grupo de segurança só aceita a faixa de origem do CloudFront, e a aplicação exige um cabeçalho que só a distribuição injeta. Ninguém mais alcança a origem sem passar pela borda — e é essa propriedade, e não o WAF em si, que torna o L47 eficaz. WAF na frente de uma origem acessível por fora é teatro.
Observabilidade: as perguntas que o painel tem de responder
Um painel de borda tem uma função estreita: dizer se a borda está economizando e se ela está escondendo um problema da origem. Métrica que não ajuda numa dessas duas decisões pertence a outro painel.
O primeiro obstáculo é onde procurar, não o que procurar
Toda métrica de CloudFront é publicada no CloudWatch da região us-east-1, independentemente de onde a aplicação roda. Quem abre o CloudWatch de São Paulo vê lista vazia e conclui que a distribuição não emite métrica. Some-se a isso que taxa de acerto e latência de origem NÃO são métricas padrão: elas exigem uma assinatura por distribuição, cobrada por métrica-mês. Duas descobertas desagradáveis que costumam acontecer no meio de um incidente.
| Pergunta | Métrica ou consulta | O que significa mudar | Limiar inicial |
|---|---|---|---|
| A borda está economizando? | `CacheHitRate` (métrica adicional) | queda indica chave de cache alterada, TTL reduzido ou origem mandando `no-store` | < 85% por 15 min, medido só no caminho do estático |
| Qual caminho está sem acerto? | `x-edge-result-type` no registro de acesso, agrupado por prefixo de caminho | é a única forma de ver que 96% no estático e 0% na API dão uma média que não descreve nenhum dos dois | estático abaixo de 90% merece investigação |
| A origem está lenta ou inalcançável? | `OriginLatency` (métrica adicional) | sobe quando o miss aumenta ou quando a origem degrada; separa os dois casos junto com a taxa de acerto | > 2× a linha de base por 5 min |
| A borda está devolvendo erro que a origem não gerou? | `5xxErrorRate` da distribuição contra `HTTPCode_Target_5XX_Count` do ALB | 5xx na borda sem 5xx no alvo é problema da conexão borda-origem, tipicamente certificado ou grupo de segurança | qualquer valor acima de zero na borda sem par no alvo |
| Por que exatamente o 502? | `x-edge-detailed-result-type` no registro de acesso | distingue `OriginDnsError` (não resolveu), `OriginConnectError` (não conectou) e `OriginCommError` (esgotou o tempo lendo) — três causas com três correções diferentes; nome de certificado incompatível não tem código próprio e sobra por eliminação | qualquer ocorrência |
| O bucket está negando leitura? | `4xxErrorRate` e taxa de erro por código 403 | salto de 403 depois de publicar é política de bucket ou OAC; 403 constante é bloqueio de acesso público mal entendido | > 1% das requisições |
| Alguém trocou a configuração da distribuição? | CloudTrail em `UpdateDistribution` e `CreateInvalidation` | mudança fora do Terraform, ou invalidação virando hábito | qualquer identidade fora da esperada; mais de uma invalidação por publicação |
| Uma resposta em cache está desatualizada? | cabeçalho `Age` na resposta | idade próxima do TTL é normal; idade alta em arquivo recém-publicado é nome de arquivo reaproveitado | comparação manual durante a investigação |
-- Athena sobre o registro padrao de acesso. E a consulta que a metrica
-- unica nao substitui: ela responde "onde" o acerto acontece.
--
-- A tabela externa vem do guia do Athena para logs do CloudFront; os campos
-- estao na referencia de registro padrao.
SELECT
CASE
WHEN uri LIKE '/api/%' THEN 'api'
WHEN uri LIKE '/assets/%' THEN 'estatico'
ELSE 'documento'
END AS grupo,
count(*) AS requisicoes,
round(100.0 * sum(CASE WHEN status_cache IN ('Hit', 'RefreshHit')
THEN 1 ELSE 0 END) / count(*), 1) AS acerto_pct,
round(avg(tempo_total) * 1000, 0) AS media_ms
FROM logs_cloudfront
WHERE data >= current_date - INTERVAL '1' DAY
GROUP BY 1
ORDER BY requisicoes DESC;
-- Na Cadencia, um dia depois:
-- estatico 412.884 requisicoes 96,4% de acerto 28 ms
-- documento 11.207 requisicoes 88,1% de acerto 41 ms
-- api 9.113 requisicoes 0,0% de acerto 287 ms
--
-- A media ponderada disso e 93,3%, e esse numero nao descreve nenhuma das
-- tres linhas. RefreshHit conta como acerto de proposito: a origem foi
-- consultada, mas o objeto nao foi retransferido — a economia de banda
-- aconteceu, e so a de latencia nao.Por que `RefreshHit` conta como acerto, e quando não deveria
Um `RefreshHit` é um objeto que estava no cache, venceu, e foi revalidado com a origem sem ser retransferido. Para custo de transferência, é acerto pleno. Para latência do visitante, não é: a ida até a origem aconteceu. Se o seu problema é fatura, agrupe com `Hit`; se é tempo de resposta em outro continente, separe — e a resposta provavelmente é validade mais longa, não mais cache.
Escala: 10, 10 mil, 1 milhão
A propriedade que muda tudo aqui é que a carga da origem para o estático não cresce com o número de visitantes: cresce com o número de caches independentes que precisam ser aquecidos. É a fórmula da seção 6 em prosa.
| Volume | O que acontece na borda | O que passa a doer | O que fazer |
|---|---|---|---|
| 10 visitas/dia, um país | quase tudo é miss: cada visita chega antes de o objeto ser reutilizado | nada, e vale saber: em volume baixo o CloudFront não parece ajudar | nada. O ganho aqui é o TLS terminado perto e o domínio próprio, não o cache |
| 10 mil visitas/dia, dois continentes | os pontos de presença ativos ficam quentes; acerto acima de 90% no estático | nada na borda; o `index.html` com 60 s vira o objeto mais buscado da origem | nada. É o cenário deste laboratório |
| 1 milhão de visitas/dia, público global | dezenas de pontos de presença ativos, cada um com o próprio cache frio no início | o fator de pontos de presença domina: a origem passa a receber uma rajada de miss por região a cada expiração | Origin Shield para concentrar o miss numa camada só; e validade longa com hash no nome, que reduz expiração a quase zero |
| Pico de campanha | a borda absorve o estático linearmente e a origem só vê o dinâmico | a rota de API vira o gargalo isolado — o que é bom, porque agora é mensurável | escala automática do serviço pela métrica certa (L06); a borda já removeu o ruído da conta |
| Falha de uma AZ | o estático continua servido: o S3 é regional e não depende de AZ | só o dinâmico sofre; com duas AZs no ALB, metade dos alvos sai | nada novo aqui — a resiliência do dinâmico é o L01 e o L07. A novidade é que uma parte grande do site deixou de depender de AZ |
| Falha total da origem | estático continua no ar; `/api/*` devolve 502 ou 504 | o site aparece funcionando e não funciona — o pior estado para o usuário | grupo de origens com reserva, ou página de manutenção servida do bucket quando a API falha; é decisão de produto, não de infraestrutura |
A conta que surpreende em público global
Com validade de um dia e 40 pontos de presença ativos, cada objeto é buscado na origem até 40 vezes por dia — não uma. Com validade de um ano e hash no nome, ele é buscado 40 vezes no total, para sempre. A diferença entre esses dois desenhos não aparece em volume baixo, e aparece de uma vez quando o público se espalha. É o mesmo raciocínio que faz o cache regional existir.
Custo: onde o dinheiro vai nesta arquitetura
Este laboratório move custo em vez de acrescentá-lo: a computação que servia arquivo sai da conta do Fargate e vira transferência de dados servida da borda. Em quase todo caso a troca é favorável — e vale saber quais são as dimensões, porque uma delas cresce por hábito e não por uso.
| Cenário | Volume | O que domina a fatura | Tendência | Otimização |
|---|---|---|---|---|
| Protótipo | 10 visitas/dia | a zona hospedada, cobrada por mês independentemente de tráfego | praticamente fixa e pequena | nenhuma. Otimizar aqui é gastar atenção onde não há dinheiro |
| Produção pequena | 10 mil visitas/dia, dois continentes | transferência de dados servida pela borda, por faixa geográfica | baixa e proporcional ao tamanho da página | comprimir na borda, reduzir o peso do build e validade longa nos ativos |
| Alta escala | 1 milhão de visitas/dia, público global | transferência e número de requisições HTTPS; a transferência varia por região do mundo | passa a ser a maior linha do projeto | medir o peso da página antes de negociar preço; classe de preço restrita só se o público permitir; Origin Shield para cortar miss |
| Dimensão | Cobra por | Cuidado |
|---|---|---|
| Transferência servida pela borda | GB servidos ao visitante, com preço por faixa geográfica | a mesma página custa diferente em cada continente; público novo em região caro muda a fatura sem mudar o volume |
| Requisições na borda | número de requisições HTTP e HTTPS | 42 arquivos por página são 42 requisições; juntar bundles reduz esta linha e não a de transferência |
| Invalidação de caminhos | há franquia mensal de caminhos e cobrança acima dela | é a única linha desta arquitetura que cresce por HÁBITO. `/*` a cada publicação é o padrão que a documentação recomenda evitar |
| Zona hospedada do Route 53 | por zona-mês, mais consultas | consulta a registro ALIAS para recurso da AWS não é cobrada; a CNAME é — e CNAME que aponta para outro registro da zona conta como duas |
| Certificado do ACM | certificado público não tem cobrança própria | o que cobra é autoridade certificadora privada, que este laboratório não usa |
| Métricas adicionais da distribuição | por métrica-mês, valor fixo por distribuição | independe do volume de requisições; e não aparece em listagem de recurso, então vira fatura sem dono depois que a distribuição some |
| Registro padrão de acesso | armazenamento no S3 do que é entregue | cresce com o tráfego e não expira sozinho: sem regra de ciclo de vida, é a linha que ninguém nota crescendo |
| Requisições da borda ao S3 | `GET` no bucket a cada miss | é proporcional a (1 − taxa de acerto). Chave de cache errada aparece aqui antes de aparecer em qualquer painel |
A economia que não está na linha do CloudFront
Servir 3,1 MB por visita pelo contêiner consumia vCPU, banda e thread da mesma task que atende pedido. Depois da mudança, a task processa apenas as 9 mil chamadas de API por dia, e a decisão de quantas tasks manter passou a depender só da carga real de negócio. O ganho aparece na fatura do Fargate, não na do CloudFront — e é por isso que comparar "quanto custa o CloudFront" com zero é a comparação errada.
Well-Architected nos seis pilares
| Pilar | Situação ao fim deste laboratório | Risco que fica | Melhoria | Prioridade |
|---|---|---|---|---|
| Excelência operacional | publicação do front por cópia de arquivo, sem rollout; configuração inteira em Terraform | a publicação ainda sai da máquina de uma pessoa, e a distribuição pode ser editada no console sem rastro no estado | pipeline com credencial federada (L54) e detecção de desvio no Terraform | alta |
| Segurança | HTTPS obrigatório com TLS 1.2 no mínimo, HSTS, bucket privado com OAC, origem fechada à faixa da borda mais cabeçalho secreto | nenhuma inspeção do conteúdo da requisição: injeção e tráfego automatizado chegam à origem intactos | WAF com regra gerenciada e limite de taxa na borda (L47) | alta |
| Confiabilidade | o estático deixou de depender da disponibilidade da aplicação e de qualquer AZ | falha da origem produz site que parece funcionar e não funciona | grupo de origens com reserva, ou página de indisponibilidade servida do bucket | média |
| Eficiência de performance | TLS terminado perto do visitante e 96% do estático sem tocar a origem; primeiro carregamento de 4,8 s para 1,2 s | a página ainda são 42 arquivos e 3,1 MB, e o `index.html` não é renderizado no servidor | decidir entre SPA na borda e renderização no servidor com TTFB medido (L20) | média |
| Otimização de custos | computação de estático removida da task; validade longa nos ativos; invalidação restrita a um caminho | transferência por região não é acompanhada, e a assinatura de métricas adicionais é fácil de esquecer ligada | rateio por tag e alerta de orçamento (L09), com a distribuição etiquetada | média |
| Sustentabilidade | menos bytes atravessando o oceano por visita, e computação de estático eliminada | a página não emagreceu: só passou a ser entregue mais perto | reduzir o peso do build; o ganho por byte não transferido é o mesmo em custo e em energia | baixa |
Evolução em níveis: o que muda, e o que passa a doer
A terceira arquitetura não é um desenho: é a resposta a QUANDO trocar de desenho. Cada nível resolve um risco e compra outro, e é a segunda parte que raramente se escreve.
Estático no contêiner, endereço do balanceador, HTTP. É onde a Cadência estava, e continua legítimo em ambiente de desenvolvimento e em público local.Domínio próprio, dois certificados, distribuição com dois comportamentos, bucket privado com OAC, publicação por hash no nome.WAF com regra gerenciada e limite de taxa na borda, registro de acesso consultável em Athena, alarme de taxa de acerto e de 5xx da borda (L47).Origin Shield para concentrar o miss, funções na borda para normalizar a chave de cache e rotear, terceiro comportamento para o catálogo público, e a escolha entre SPA e renderização no servidor (L20).Zona e certificados em conta de rede compartilhada, distribuição por ambiente promovida por pipeline, restrição geográfica e assinatura de URL onde o conteúdo é restrito.O registro de acesso vira dado: taxa de acerto por objeto, curva de expiração e assinatura de tráfego automatizado alimentam decisão de validade e de bloqueio.A ordem não é negociável, e o motivo é concreto
WAF no nível 3 só protege porque a origem, no nível 2, deixou de ser alcançável por fora. Função de borda no nível 4 só faz sentido depois de a chave de cache ser entendida, senão ela normaliza parâmetro sem saber qual importa. E dado de borda no nível 6 depende do registro de acesso ligado no nível 3. Quem começa pelo WAF monta inspeção que se contorna digitando o nome do balanceador.
Onde IA entra nesta arquitetura, e onde não entra
Neste módulo, IA não resolve o problema central, e forçá-la seria o antipadrão que a própria série critica. "Qual deve ser a validade deste arquivo?" tem resposta determinística: se o nome muda com o conteúdo, um ano; se não muda, o tempo que você aceita esperar por uma correção. "O que entra na chave de cache?" é resposta de requisito, não de estimativa. Um modelo não melhora nenhuma das duas.
Há um lugar onde IA acrescentaria valor real, e ele é modesto: distinguir tráfego automatizado de tráfego humano a partir do registro de acesso, quando as regras gerenciadas já pegaram o óbvio e sobra o que imita navegador. Isso é assunto do L47 e da banda de segurança, não deste módulo — e mesmo lá a regra simples vem primeiro.
| Pergunta | Resposta honesta para este módulo |
|---|---|
| Qual problema a IA resolveria aqui? | nenhum dos declarados. Validade, chave de cache, região de certificado e tipo de registro DNS são todos determinísticos |
| E o caso adjacente que ela resolveria? | separar bot que imita navegador do visitante real, para decidir o que barrar na borda — e isso é o L47 |
| Por que uma regra não bastaria lá? | bastaria para começar, e é por onde se começa: regra gerenciada mais limite de taxa por IP cobrem a maior parte. IA só se justifica no resíduo que sobra depois |
| De onde viriam os dados? | do registro padrão de acesso, que este módulo já liga: agente, resultado de cache, tempo, caminho e país |
| Qual o risco de modelo aqui? | aprender de tráfego já filtrado pelo WAF e concluir que não há ataque; e bloquear cliente legítimo com padrão incomum, o que aparece como "o site não abre para mim" |
| O que seria IA decorativa neste módulo? | pedir a um modelo para "sugerir o TTL ideal" a partir do log. O TTL certo é derivável da política de publicação em uma frase, e trocar dedução exata por estimativa é acrescentar incerteza sem ganho |
O uso de IA que parece atraente e é armadilha aqui
Gerar a configuração da distribuição com um modelo é tentador, porque o Terraform do CloudFront é verboso. O problema não é o modelo errar sintaxe — ele acerta. É que os dois defeitos mais graves deste laboratório são semanticamente invisíveis: uma ordem de comportamento trocada e um cookie a mais na chave de cache produzem configuração válida, que aplica, e que funciona na primeira visita. Revisar código gerado exige entender exatamente o que este módulo ensina — então o módulo continua sendo o pré-requisito, gerado ou não.
Anti-padrões deste laboratório
| Anti-padrão | Por que alguém faz | Por que é problema | Sintoma em produção | Forma correta | Quando é aceitável |
|---|---|---|---|---|---|
| Pedir o certificado da distribuição na região da aplicação | a regra "recurso e certificado na mesma região" vale para o ALB e parece geral | o certificado emite, fica válido, e a distribuição não o aceita — não há mensagem que explique isso | "o certificado existe e não aparece na lista do CloudFront" | us-east-1 para a distribuição, região da aplicação para o balanceador | nunca; não há exceção regional documentada |
| Apontar a origem HTTPS para o nome `*.elb.amazonaws.com` | é o nome que o Terraform já expõe, e evita criar um registro | nenhum nome do seu certificado casa com ele, e a documentação prevê 502 nesse caso | 502 em toda chamada de API, com log da aplicação limpo | nome próprio de origem, coberto pelo certificado regional | com protocolo HTTP até a origem, onde não há validação de nome — e aí o tráfego vai em claro pela internet |
| Invalidar `/*` a cada publicação | é uma linha, funciona sempre, e resolve qualquer dúvida sobre o que mudou | apaga também o que jamais mudaria, obriga a rebuscar o build inteiro em cada ponto de presença e cobra por caminho acima da franquia | pico de latência depois de toda publicação, e uma linha de fatura que cresce com a frequência de deploy | hash no nome do arquivo, invalidando apenas o `index.html` | uma vez, para corrigir estrago já feito por nome de arquivo reaproveitado |
| Incluir todos os cookies ou toda a consulta na chave de cache | é o comportamento antigo do CloudFront e o que "não quebra nada" | multiplica as entradas de cache por usuário ou por combinação de parâmetro, e a taxa de acerto vai a zero sem nenhum erro | fatura de origem proporcional a visitas; latência internacional que voltou sem ninguém ter mexido no código | chave mínima, e o que a aplicação precisa receber vai na política de requisição de origem | quando a resposta realmente varia por aquele valor — e aí o valor certo é o específico, não "todos" |
| Bucket como ponto de site estático, público | é o tutorial mais curto que existe, e dispensa entender OAC | ponto de site é origem personalizada e não aceita controle de acesso de origem: o conteúdo passa a ser servível fora da borda | ninguém percebe nada — até o WAF do L47 ser instalado e contornado pelo endereço do bucket | bucket comum, privado, com OAC assinando em SigV4 | conteúdo genuinamente público em que redirecionamento do ponto de site é necessário; e aí a borda não é camada de segurança |
| Subdomínio separado para a API por "organização" | parece mais limpo, e separa os dois times no diagrama | cria requisição de verificação prévia antes de cada chamada com cabeçalho, transforma o cookie de sessão em cookie de terceiro e acrescenta um certificado | primeira chamada de cada sessão com o dobro da latência; login que "às vezes não persiste" em navegador com bloqueio padrão | mesmo nome, comportamento por caminho | quando front e API têm times, ciclos e SLAs realmente separados |
| Mapear 404 e 403 para `/index.html` com status 200 | é a receita padrão de SPA, e está em todo tutorial | página de erro customizada é configuração da DISTRIBUIÇÃO, não do comportamento: o 404 legítimo da API passa a responder HTML com 200 | cliente de API tratando erro como sucesso; monitoração que nunca vê 4xx | rotas com arquivo real, ou função de borda reescrevendo só o que não tem extensão (L20) | distribuição que serve exclusivamente um SPA, sem nenhuma rota de API |
| Usar TTL mínimo maior que zero para "garantir" o cache | parece a forma direta de forçar validade longa | faz o CloudFront ignorar `no-cache`, `no-store` e `private` da origem — a documentação avisa, e isso remove sua capacidade de tirar algo do ar | conteúdo errado no ar pelo tempo do TTL mínimo, sem nada que o derrube a não ser invalidação | validade pelo `Cache-Control` da origem, com TTL máximo como teto | origem que você não controla e que manda cabeçalho inadequado — e aí é remendo consciente, com o motivo escrito |
Quando algo não funciona
| Sintoma | Causa provável | Como investigar | Onde olhar | Correção |
|---|---|---|---|---|
| O domínio não resolve, e não há erro em lugar nenhum | a delegação de NS do registrador não aponta para a zona do Route 53 | compare os quatro servidores de nome da zona com os do registrador | `dig NS seu-dominio` contra `aws route53 get-hosted-zone` | atualizar a delegação no registrador; nada na AWS corrige isso |
| Certificado parado em `PENDING_VALIDATION` | o CNAME de validação não está na zona, ou o nome foi digitado com o domínio duplicado | confira o par nome/valor exato que o ACM pediu | `describe-certificate` → `DomainValidationOptions` | publicar o CNAME exatamente como fornecido; depois de 72 h o certificado vira `VALIDATION_TIMED_OUT` e só resta pedir outro |
| O certificado não aparece na lista da distribuição | foi pedido fora de us-east-1 | liste em us-east-1 e na região da aplicação, e compare | ARN do certificado: a região está embutida nele | pedir um novo em us-east-1; o regional continua servindo ao balanceador |
| 502 em toda chamada de `/api/*` | nome da origem não coberto pelo certificado dela, ou origem inalcançável | elimine primeiro DNS e conectividade pelo detalhe do resultado; sobrando as duas, o nome do certificado é a causa | `x-edge-detailed-result-type`: `OriginDnsError` e `OriginConnectError` são resolução e conexão; incompatibilidade de nome não tem código próprio documentado e cai nos genéricos de origem | nome próprio de origem coberto pelo certificado regional; e regra de entrada com a lista de prefixo |
| 403 em todo arquivo estático | política do bucket ausente, sem a condição de ARN da distribuição, ou OAC não anexado à origem | compare o ARN da condição com o ARN real da distribuição | política do bucket e campo `OriginAccessControlId` na configuração da distribuição | aplicar a política com o ARN correto; ordem importa, a distribuição existe antes da política |
| 403 apenas na raiz do site | objeto raiz padrão não configurado | peça `/index.html` explicitamente e compare com `/` | `DefaultRootObject` na configuração | definir `index.html` como objeto raiz padrão |
| Taxa de acerto perto de zero no estático | cookie, cabeçalho ou consulta na chave de cache; ou origem mandando `no-store` | peça o mesmo arquivo duas vezes e leia `Server-Timing`; depois inspecione a política | `Pragma: server-timing` na requisição; `get-cache-policy` | chave mínima; e conferir o `Cache-Control` gravado no objeto do bucket |
| Código antigo servido depois da publicação | nome de arquivo reaproveitado com validade longa | compare o hash do objeto no bucket com o corpo que a borda devolve; leia o `Age` | cabeçalho `Age` e `ETag` da resposta | hash no nome; invalidar aquele caminho uma vez para corrigir o passado |
| 404 da API respondendo HTML com status 200 | página de erro customizada configurada na distribuição | confira se existe mapeamento de 404 para `/index.html` | `CustomErrorResponses` na configuração da distribuição | remover o mapeamento; roteamento de SPA é decisão do L20 |
| Métricas da distribuição não aparecem | procuradas na região da aplicação, ou métricas adicionais não assinadas | consulte o CloudWatch de us-east-1 e verifique a assinatura | `get-monitoring-subscription` e `--region us-east-1` | consultar em us-east-1; assinar as métricas adicionais se precisar de taxa de acerto |
| Login para de persistir depois da mudança | cookie marcado para o nome antigo, ou domínio da API diferente do domínio do front | inspecione o atributo de domínio do cookie emitido | cabeçalho `Set-Cookie` na resposta da API | emitir o cookie para o domínio público, e manter front e API no mesmo nome |
| A aplicação vê o endereço da borda como cliente | cabeçalhos encaminhados não processados, ou limite de saltos insuficiente | registre o valor bruto de `X-Forwarded-For` uma vez e conte as entradas | log da aplicação e configuração de cabeçalhos encaminhados | processar os cabeçalhos com limite igual ao número de proxies — dois nesta topologia |
A pergunta que resolve metade destes casos
Antes de mexer em configuração, pergunte: o erro veio da borda, da origem ou do DNS? 502 e 403 na borda com log de aplicação limpo significam que nada chegou à origem — é certificado, política ou grupo de segurança. Erro com log na aplicação é problema dela, e a borda só o transportou. E se `dig` não devolve endereço, nenhuma das duas camadas foi sequer consultada. Três lugares, três conjuntos de ferramentas, e o `x-edge-result-type` do registro de acesso é o que diz em qual você está.
Limpeza: o que o destroy não leva
A distribuição é o recurso mais lento de destruir de todo este laboratório, porque ela precisa ser desabilitada e propagada antes de ser apagada. Interromper o destroy no meio é a forma mais comum de deixar uma distribuição de pé — e ela continua cobrando por requisição e por byte servido.
# limpar.sh — a ordem importa, e tres itens continuam cobrando depois
set -euo pipefail
PROJETO="${PROJETO:-ffv-lab}"
# 1. Uma distribuicao NAO pode ser apagada enquanto esta habilitada. O
# Terraform desabilita e espera a propagacao — que leva minutos, nao
# segundos. Interromper aqui deixa a distribuicao de pe.
terraform destroy -auto-approve
# 2. ZONA HOSPEDADA: se voce criou a zona fora do Terraform (o caminho comum,
# porque ela vem do registro do dominio), ela sobrevive e cobra por mes.
# E o destroy da zona falha se restar qualquer registro que nao seja NS/SOA.
aws route53 list-hosted-zones-by-name --dns-name "cadencia.com.br" \
--query "HostedZones[].[Name,Id,ResourceRecordSetCount]" --output table
# Apagar a zona invalida os CNAME de validacao do ACM: a renovacao automatica
# de qualquer certificado que dependa deles para de funcionar em silencio, e o
# sintoma aparece meses depois, na expiracao.
# 3. BUCKET DE LOGS: nao entra no destroy com objeto dentro, e o registro de
# acesso continua chegando por algumas horas depois de a distribuicao sair.
# Espere, depois limpe.
aws s3 rm "s3://${PROJETO}-logs" --recursive --quiet || true
aws s3 rb "s3://${PROJETO}-logs" || true
# 4. BUCKET DO ESTATICO COM VERSIONAMENTO: `rm --recursive` apaga a versao
# corrente e deixa as anteriores, que continuam ocupando GB-mes. Sao duas
# listagens diferentes.
aws s3api list-object-versions --bucket "${PROJETO}-front-$(aws sts get-caller-identity --query Account --output text)" \
--query "{Versoes:length(Versions||`[]`),Marcadores:length(DeleteMarkers||`[]`)}"
# 5. ASSINATURA DE METRICAS ADICIONAIS: cobra por metrica-mes e nao aparece em
# lista de recurso nenhuma. Se a distribuicao foi apagada, ela vai junto —
# mas confirme, porque assinatura orfa e fatura sem dono.
aws cloudfront get-monitoring-subscription --id "$(terraform output -raw distribuicao_id)" 2>/dev/null || \
echo "sem assinatura ativa (esperado depois do destroy)"
# 6. CERTIFICADOS: certificado em uso nao pode ser apagado, entao a ordem e
# distribuicao/listener primeiro, certificado depois. Certificado nao
# cobra parado — o que fica e um inventario que ninguem entende.
aws acm list-certificates --region us-east-1 \
--query "CertificateSummaryList[?contains(DomainName,'cadencia')].[DomainName,CertificateArn]" \
--output table
# 7. E o que veio do L01 e cobra por hora: ALB, RDS, NAT Gateway e o Elastic IP
# dele. Rode a limpeza do L01 se voce nao vai seguir para o L06.
aws resourcegroupstaggingapi get-resources \
--tag-filters Key=Projeto,Values="${PROJETO}" \
--query "ResourceTagMappingList[].ResourceARN" --output table
| Recurso | Sai no destroy? | Cobra parado? | Por que fica |
|---|---|---|---|
| Distribuição do CloudFront | sim, depois de desabilitar e propagar | sim, por requisição e byte | a espera é de minutos; interrupção deixa a distribuição habilitada |
| Zona hospedada do Route 53 | só se foi criada pelo Terraform | sim, por zona-mês | quase sempre vem do registro do domínio, fora do módulo — e apagá-la mata os CNAME de validação de todos os certificados |
| Bucket do estático | só com `force_destroy` | sim, GB-mês | o destroy falha com objeto dentro; e com versionamento ligado, apagar a versão corrente não apaga as anteriores |
| Bucket de logs de acesso | não, se tiver objeto | sim, GB-mês | o registro continua sendo entregue por horas depois de a distribuição sair |
| Certificados do ACM | sim, se não estiverem em uso | não | certificado em uso não pode ser apagado: a ordem é distribuição e listener primeiro |
| Assinatura de métricas adicionais | sim, junto com a distribuição | sim, por métrica-mês | não aparece em nenhuma listagem de recurso — vira fatura sem dono se sobreviver |
| Controle de acesso de origem (OAC) | sim | não | não cobra, mas acumula: cada laboratório refeito deixa um com nome parecido |
| Segredo do cabeçalho de origem | sim se em Terraform | sim, por segredo-mês | segredo apagado entra em período de recuperação e continua contando até ser destruído de fato |
| ALB, RDS, NAT Gateway e Elastic IP (L01/L02) | sim | sim, por hora | vêm dos módulos anteriores; o Elastic IP cobra justamente por estar ocioso |
O item desta lista que causa o dano mais duradouro
Apagar a zona hospedada apaga também os CNAME de validação do ACM. Os certificados atuais continuam válidos até expirar, então nada quebra hoje — e a renovação automática para de funcionar em silêncio, porque ela depende do registro continuar na zona. O sintoma aparece meses depois, como TLS expirado num domínio que ninguém tocou. Se você vai manter o domínio, mantenha a zona e apague só o resto.
Resumo: problema, peça e motivo
| Problema | Peça | Por que ela, e não outra |
|---|---|---|
| HTTPS impossível no endereço atual | domínio próprio em zona do Route 53 | certificado público exige provar controle do nome, e o nome do balanceador não é seu |
| A distribuição recusa o certificado | ACM em us-east-1 | é exigência da distribuição, e não tem exceção por região de aplicação |
| O domínio sem `www` precisa responder | registro ALIAS tipo A e AAAA | CNAME no ápice não existe; e ALIAS para recurso da AWS não cobra consulta |
| Latência internacional por arquivo | cache no ponto de presença | o gargalo é o número de idas à origem, e a única forma de encurtar a viagem é não fazê-la |
| Front e API sem CORS nem cookie de terceiro | dois comportamentos num domínio | domínio único elimina verificação prévia e cookie de terceiro de uma vez |
| Resposta de usuário reutilizada por outro | armazenamento desligado em `/api/*` mais `no-store` | a ordem dos comportamentos é regra de segurança: o primeiro padrão que casa vence |
| Taxa de acerto que desaba sem erro | chave de cache só com o caminho | cada valor na chave multiplica as entradas; o que a aplicação precisa receber vai na política de requisição |
| Publicar texto exigia rollout da API | estático no S3, fora da imagem | separa os ciclos de vida, e a publicação passa a ser cópia de arquivo |
| Estático servível por fora da borda | bucket privado com OAC e condição de ARN | é o que torna o WAF do L47 eficaz em vez de contornável |
| Atualizar conteúdo sem esperar nem pagar | hash no nome, validade de um ano | nome que muda com o conteúdo nunca precisa ser removido do cache |
| Falha | O que a protege | O que ela NÃO protege |
|---|---|---|
| TLS impossível ou inválido | certificado gerenciado com validação por DNS | renovação, se o CNAME de validação for apagado da zona |
| Latência por número de idas à origem | validade longa com hash no nome | peso da página: 3,1 MB continuam sendo 3,1 MB na primeira visita |
| Estático alcançável sem a borda | OAC com condição de ARN da distribuição | origem dinâmica: para o ALB é lista de prefixo mais cabeçalho secreto |
| Resposta autenticada em cache compartilhado | política de armazenamento desligado | cache do navegador e proxies no caminho — isso é o `no-store` da origem |
| Conteúdo velho depois da publicação | nome de arquivo versionado | o `index.html`, que tem nome fixo e depende da validade curta |
| Tráfego automatizado consumindo capacidade | nada, neste módulo | é explicitamente o L47; a borda apenas tornou o bloqueio possível |
- O visitante consulta o ápice do domínio e recebe endereços de ponto de presença, por ALIAS.
- O TLS termina no ponto de presença, com o certificado que vive em us-east-1.
- O caminho é comparado com os padrões na ordem declarada, e o coringa é o último.
- A chave de cache é montada com o que a política manda incluir — no estático, só o caminho.
- Havendo objeto válido, a resposta sai do ponto de presença e a origem não é tocada.
- No miss, a requisição tenta o cache regional antes de sair para a origem.
- Para o estático, a borda assina em SigV4 e o bucket só aceita esta distribuição.
- Para `/api/*`, tudo do visitante é encaminhado ao nome de origem, por HTTPS.
- A aplicação confere o cabeçalho secreto, responde, e marca a resposta como não armazenável.
- O `Cache-Control` de cada resposta decide o que a borda guarda — e por quanto tempo.
Perguntas frequentes
❓ Por que o certificado do CloudFront precisa estar na região us-east-1?
❓ Posso usar CNAME no domínio raiz para apontar para o CloudFront?
❓ Qual a diferença entre política de cache e política de requisição de origem?
❓ Como servir o front e a API no mesmo domínio com o CloudFront?
❓ Devo invalidar o cache do CloudFront a cada deploy?
❓ Por que meu CloudFront retorna 502 apontando para o ALB?
❓ Por que minha taxa de acerto de cache está baixa?
❓ Cache-Control da origem ou TTL do CloudFront: quem manda?
Fixando
Um site com estático na borda tinha 95% de taxa de acerto. Depois de uma mudança na política de cache para incluir todos os cookies em `/assets/*`, a página continua correta e a taxa de acerto caiu para 3%. Por quê?
A origem passou a responder `Cache-Control: no-cache` na página inicial para tirar do ar um conteúdo errado. A página continua sendo entregue desatualizada por cerca de uma hora. A política de cache tem TTL mínimo de 3600, TTL padrão de 60 e TTL máximo de 86400. O que explica isso?
Conhecimentos, próximo módulo e documentação
| Item | Conteúdo |
|---|---|
| Conhecimentos anteriores necessários | L01 no ar (ECS Fargate, RDS em sub-rede privada, ALB), um domínio registrado e delegado ao Route 53, Terraform e um build de front que gere hash no nome dos arquivos |
| Conhecimentos adquiridos | por que HTTPS é impossível no nome de um balanceador; a região do certificado da borda como regra sem exceção; ALIAS no ápice e o que muda na fatura; comportamento por caminho e a ordem como regra de segurança; a chave de cache como fator que multiplica; o `Cache-Control` da origem como decisor do TTL real; versionamento de nome em vez de invalidação |
| Limitação que fica | nada inspeciona o conteúdo da requisição: tráfego automatizado e injeção chegam à origem intactos. E o front continua sendo 42 arquivos servidos como arquivo, sem renderização no servidor |
| Próximo exemplo recomendado | L20 — SPA na borda ou renderização no servidor. Ele usa esta distribuição e mede TTFB e custo das duas opções, e é onde a página de erro customizada ganha tratamento |
| Também habilitado por este módulo | L47 (WAF e Shield) só é eficaz porque a origem deixou de ser alcançável por fora; L17 (upload direto ao S3 com URL pré-assinada) reaproveita o bucket e a distribuição |
| Data da última validação técnica | 7 de agosto de 2026 |
Documentação oficial consultada: Requirements for using SSL/TLS certificates with CloudFront — a exigência de us-east-1 e o 502 quando nenhum nome do certificado casa com o nome da origem; Understand cache policies — a semântica dos três TTL e a advertência de que TTL mínimo acima de zero prevalece sobre no-cache; Cache behavior settings — o padrão de caminho, a ordem de avaliação e o coringa como último; Invalidate files to remove content — a recomendação explícita de versionar nome de arquivo, com os cinco motivos; AWS Certificate Manager DNS validation — o CNAME único, a renovação automática dependente de ele permanecer na zona e as 72 h até Validation timed out; Choosing between alias and non-alias records — ALIAS no ápice e a cobrança de consulta; Use managed origin request policies — o que AllViewer encaminha e para que existe AllViewerExceptHostHeader; Restrict access to an Amazon S3 origin — OAC recomendado sobre OAI e a política com condição de ARN; Understand response headers policies — Server-Timing e o Pragma: server-timing que força o cabeçalho em qualquer amostragem; Standard logging reference — os valores de x-edge-result-type e de x-edge-detailed-result-type; View CloudFront and edge function metrics — métricas adicionais por assinatura e publicação em us-east-1; e Locations and IP address ranges of CloudFront edge servers — os nomes das duas listas de prefixo gerenciadas. Preços não aparecem em número por decisão: use o AWS Pricing Calculator, porque transferência varia por região do mundo e envelhece mais rápido que o conteúdo.
O que não foi verificado, e você deve conferir na sua conta
Três coisas. Primeira: os tempos citados — 4,8 s de Lisboa, 1,2 s depois, 210 ms para 24 ms de primeiro byte, 96,4% de acerto — são medições da aplicação de exemplo e servem como ordem de grandeza, não como referência; a forma da cascata de dependência do SEU front decide o multiplicador, e é ela que você tem de medir. Segunda: o limite de dois saltos de proxy no processamento de cabeçalhos encaminhados foi derivado da topologia (borda mais balanceador), não lido em documentação — registre o valor bruto de `X-Forwarded-For` uma vez e conte as entradas antes de confiar. Terceira: existe uma forma mais recente de o CloudFront alcançar balanceador em sub-rede privada, sem expô-lo à internet, e não confirmamos o comportamento dela na documentação para este módulo; a arquitetura aqui usa lista de prefixo mais cabeçalho secreto, que é o caminho documentado no material consultado. Se você for adotar a alternativa, verifique antes de assumir equivalência.
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…