Todos os artigos

// Knowledge.log — 技術記事

CloudFront depois do deploy: cache e invalidação sem sustos

Configure cache, invalidação e OAC no CloudFront com S3 privado, headers previsíveis e comandos verificáveis para o pós-deploy.

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:

PolicyIDMinTTLDefaultTTLUso principal
UseOriginCacheControlHeaders83da9c7e-98b4-4e11-a168-04f0df8e2c6500Deixar os headers da origem comandarem o TTL
CachingOptimized658327ea-f89d-4fab-a63d-7e88639e58f61 segundo86400 segundosCache 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-Control curto e invalidação direcionada quando o deploy precisar de troca imediata;
  • policy com MinTTL=0 quando 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.
  • CachingOptimized tem MinTTL=1 e DefaultTTL=86400; um objeto sem Cache-Control pode ficar um dia no edge.
  • no-store da origem não vence uma policy cujo MinTTL seja 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.

awsjava

// Continue.training — 次のステップ

Conhecimento só conta quando vira prática.

Volte ao artigo, execute os exemplos e compartilhe o que aprendeu.

Explorar mais artigos