O deploy terminou, o arquivo novo chegou ao S3 e a página continua servindo a versão anterior. A reação comum é invalidar /*, confirmar que voltou a funcionar e transformar esse curinga em parte permanente da pipeline. Ele “resolve cache” em todo deploy, assim como desligar o disjuntor resolve uma lâmpada piscando.
O resultado esperado é mais previsível: bucket privado, acesso apenas pelo CloudFront com Origin Access Control (OAC), assets versionados com cache longo, HTML com cache curto e invalidação direcionada quando necessária. Ao final, você conseguirá inspecionar a política anexada, enviar uma invalidação, acompanhar seu estado e provar que o S3 não ficou público.
Pré-requisitos e desenho de produção
Você precisa de AWS CLI autenticada com permissão para consultar a distribuição, criar invalidações e ler a configuração do bucket. Nos comandos, substitua DISTRIBUTION_ID, BUCKET, REGION, ACCOUNT e o domínio dXXXX.cloudfront.net pelos valores da sua conta.
O desenho de produção é este:
Navegador -> CloudFront -> origem REST do S3 privado
-> OAC assina a requisição com SigV4
Não use o endpoint de site estático do S3 como origem. Ele é tratado como uma origem customizada, e OAC não pode ser associado a ele. Também não abra o bucket para compensar uma policy incompleta. A documentação de restrição de acesso a origens S3 recomenda OAC; Origin Access Identity (OAI) é o mecanismo legado.
Para OAC, mantenha o Block Public Access habilitado, use Object Ownership como Bucket owner enforced e configure a assinatura como always. A policy mínima de leitura restringe o principal de serviço à ARN da sua distribuição:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowCloudFrontServicePrincipalReadOnly",
"Effect": "Allow",
"Principal": {
"Service": "cloudfront.amazonaws.com"
},
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::BUCKET/*",
"Condition": {
"StringEquals": {
"AWS:SourceArn": "arn:aws:cloudfront::ACCOUNT:distribution/DISTRIBUTION_ID"
}
}
}
]
}
Em uma migração de OAI para OAC, permita temporariamente os dois principals, associe o OAC à distribuição e só então remova a declaração do OAI. Essa ordem mantém o acesso disponível durante a troca sem enfraquecer a segurança.
Cache policy e Cache-Control resolvem partes diferentes
A cache policy do CloudFront define o cache key e os limites de permanência no edge: MinTTL, DefaultTTL e MaxTTL. O header Cache-Control vem da origem e expressa por quanto tempo aquele objeto pode ser reutilizado. Os dois trabalham juntos; não existe uma “cache policy oficial do Spring Boot 4.1”.
Duas policies gerenciadas ajudam a enxergar a diferença:
| Policy | ID | MinTTL | DefaultTTL | Uso principal |
|---|---|---|---|---|
| UseOriginCacheControlHeaders | 83da9c7e-98b4-4e11-a168-04f0df8e2c65 | 0 | 0 | Deixar os headers da origem comandarem o TTL |
| CachingOptimized | 658327ea-f89d-4fab-a63d-7e88639e58f6 | 1 segundo | 86400 segundos | Cache geral otimizado quando esse mínimo é aceitável |
Com MinTTL=0, um Cache-Control: max-age=300 permite até 300 segundos no CloudFront, respeitado o MaxTTL. Se houver s-maxage, ele define o TTL compartilhado do CloudFront, enquanto o navegador continua usando max-age.
Com MinTTL maior que zero, o CloudFront segura o objeto por pelo menos esse valor, inclusive quando a origem envia no-cache, no-store ou private. Por isso a CachingOptimized, cujo MinTTL é 1 segundo, não equivale a obedecer integralmente à origem. E se o objeto S3 não tiver Cache-Control nem Expires, entra o DefaultTTL: 24 horas nessa policy. O objeto sem metadata parece inofensivo até sobreviver ao deploy seguinte. Aí ele ganha personalidade.
A documentação de expiração do CloudFront detalha essa interação. Ela também cobre stale-while-revalidate: o CloudFront pode servir uma cópia expirada enquanto busca a nova em segundo plano. Porém, a janela de conteúdo stale é limitada pelo MaxTTL da policy. Uma diretiva longa no header não atravessa esse teto.
Para um site gerado e armazenado no S3, uma divisão prática é:
- assets com hash no nome, como
/assets/app.a83f1c.js:Cache-Control: public, max-age=31536000, immutable; - HTML, como
/index.html:Cache-Controlcurto e invalidação direcionada quando o deploy precisar de troca imediata; - policy com
MinTTL=0quando o requisito é deixar esses headers controlarem a validade.
Você pode aplicar metadata durante o envio ao S3. Separe os grupos para não colocar cache anual no HTML:
aws s3 sync dist/assets/ s3://BUCKET/assets/ \
--cache-control "public,max-age=31536000,immutable"
aws s3 cp dist/index.html s3://BUCKET/index.html \
--cache-control "public,max-age=60"
Se a origem for uma aplicação Spring Boot, use os headers emitidos por ela na mesma estratégia. Spring Boot 4.1.1 com Java 25 LTS é uma combinação suportada pelos requisitos de sistema do Spring Boot 4.1; estas propriedades configuram cache e URLs com hash na aplicação, não uma policy do CloudFront:
spring.web.resources.cache.cachecontrol.max-age=1h
spring.web.resources.cache.cachecontrol.s-max-age=10m
spring.web.resources.chain.strategy.content.enabled=true
spring.web.resources.chain.strategy.content.paths=/**
Invalide o que mudou, não a distribuição por hábito
A API de invalidação exige paths iniciados por /. Eles diferenciam maiúsculas de minúsculas, e * funciona como curinga somente quando é o último caractere. Portanto, /assets/* é válido como árvore; um asterisco no meio do path é tratado literalmente. No shell, sempre coloque "/*" entre aspas para impedir expansão local.
Prefira invalidar o HTML que referencia os novos assets:
INVALIDATION_ID=$(aws cloudfront create-invalidation \
--distribution-id DISTRIBUTION_ID \
--paths "/index.html" \
--query 'Invalidation.Id' \
--output text)
aws cloudfront get-invalidation \
--distribution-id DISTRIBUTION_ID \
--id "$INVALIDATION_ID" \
--query 'Invalidation.Status' \
--output text
O estado chega a Completed quando a invalidação termina. Se for realmente necessário limpar tudo, o comando é explícito:
aws cloudfront create-invalidation \
--distribution-id DISTRIBUTION_ID \
--paths "/*"
O primeiro bloco de 1.000 paths de invalidação por mês é gratuito por conta AWS, somando todas as distribuições. Depois disso, paths adicionais são cobrados individualmente. Um /* conta como um path, mesmo que alcance muitos objetos; /index.html e /logo.svg contam como dois. Consulte a página atual de preços do CloudFront antes de estimar valores, pois as tarifas podem mudar.
Há ainda uma limitação importante: invalidar o CloudFront não limpa o cache do navegador. Um asset com URL estável e max-age longo pode continuar antigo no cliente. Hash no nome muda a URL e evita esse conflito; é a opção preferível para arquivos que mudam com frequência.
Verifique a configuração com os seus recursos
Comece descobrindo qual policy está no comportamento padrão e consulte seus TTLs:
POLICY_ID=$(aws cloudfront get-distribution-config \
--id DISTRIBUTION_ID \
--query 'DistributionConfig.DefaultCacheBehavior.CachePolicyId' \
--output text)
aws cloudfront get-cache-policy \
--id "$POLICY_ID" \
--query 'CachePolicy.CachePolicyConfig.{Name:Name,MinTTL:MinTTL,DefaultTTL:DefaultTTL,MaxTTL:MaxTTL}'
Depois confira os headers realmente entregues. Cache-Control mostra a diretiva enviada ao cliente, e Age ajuda a observar há quanto tempo a resposta está no cache compartilhado, quando presente:
curl -sI https://dXXXX.cloudfront.net/index.html
Por fim, prove a fronteira de acesso. A URL REST direta do objeto deve responder 403, enquanto o mesmo objeto pelo CloudFront deve responder 200:
aws s3api get-public-access-block --bucket BUCKET
aws s3api get-bucket-policy-status --bucket BUCKET
curl -sS -o /dev/null -w '%{http_code}\n' \
https://BUCKET.s3.REGION.amazonaws.com/index.html
curl -sS -o /dev/null -w '%{http_code}\n' \
https://dXXXX.cloudfront.net/index.html
Se o S3 responder 200, pare e corrija a exposição antes de discutir TTL. Se o CloudFront responder 403, revise a origem REST, a associação do OAC, o modo de assinatura e o AWS:SourceArn da bucket policy.
Armadilhas que merecem uma checagem no deploy
- S3 website endpoint não aceita OAC; use a origem REST do bucket.
CachingOptimizedtemMinTTL=1eDefaultTTL=86400; um objeto semCache-Controlpode ficar um dia no edge.no-storeda origem não vence uma policy cujoMinTTLseja maior que zero.- Invalidações não podem ser canceladas depois do envio.
- Assets sem hash exigem coordenação entre cache do navegador, edge e origem.
- Origin Shield é uma camada opcional com cobrança adicional, útil em cenários específicos; não é requisito para corrigir esta estratégia.
Como proteção financeira complementar, você pode criar um AWS Budget mensal e marcar a distribuição com uma tag de alocação, como project. A tag precisa ser ativada para alocação de custos antes de aparecer nos dados de cobrança, e os dados do mês corrente podem levar cerca de 24 horas para refletir essa ativação. Marque a distribuição, não a invalidação: invalidações não aceitam tags. Esse alarme é útil, mas não substitui headers e paths corretos.
Próximo passo
Escolha um deploy de teste, adicione Cache-Control aos objetos, anexe uma policy compatível com esses headers e troque /* pela lista de HTML alterado. Rode os comandos de verificação antes e depois da publicação e registre a policy e os paths no pipeline.
A recomendação é firme: se os assets recebem nomes com hash, mantenha cache longo neles e invalide apenas HTML; use /* somente quando você consegue explicar por que toda a distribuição precisa ser descartada naquele deploy. Se essa explicação for “sempre fizemos assim”, o curinga já virou configuração acidental.