Pular para o conteúdo

AWS · parte 4 de 11

S3: armazenamento de objetos na AWS

Como o S3 guarda objetos com alta durabilidade, quais classes de armazenamento existem, o ciclo de vida dos arquivos e as permissões de acesso.

Revisada em · 18 min de leitura

Tamanho da fonte:
Largura da coluna:

O Amazon S3 (Simple Storage Service) é o serviço de armazenamento de objetos da AWS. Diferente do armazenamento de arquivos tradicional (como o do seu computador) ou do armazenamento em bloco (usado no EC2), o S3 foi projetado para guardar e recuperar qualquer quantidade de dados, de qualquer lugar da internet, com durabilidade e escalabilidade muito altas.

Os pontos a seguir explicam como ele funciona. A segunda metade da nota mostra como usar o serviço de verdade, com a CLI e com código.

1. Estrutura: buckets e objetos

O S3 não tem pastas e arquivos no sentido tradicional de um sistema operacional. Ele trabalha com buckets e objetos.

Bucket é o contêiner dos dados. Para guardar algo, você primeiro cria um bucket e escolhe a região onde ele fica, por exemplo São Paulo ou Virgínia. O nome do bucket é único em toda a AWS.

Objeto é o arquivo em si: foto, vídeo, documento, log. Cada objeto tem três partes: o conteúdo, os metadados (informações sobre o arquivo, como o tipo) e uma chave, que é o caminho dele dentro do bucket. Quando você vê fotos/capa.png no console, a parte fotos/ é um prefixo que faz parte da chave, e não uma pasta de verdade. O console mostra esse prefixo como pasta para facilitar a navegação.

2. Classes de armazenamento inteligente

Uma das características mais úteis do S3 é a possibilidade de reduzir custos escolhendo a classe de armazenamento adequada para a frequência com que você acessa os dados.

S3 Standard serve para dados acessados com frequência, como as imagens de um site. É a classe padrão, usada quando você não escolhe outra.

S3 Standard-IA (Infrequent Access) serve para dados acessados com pouca frequência, mas que precisam voltar rápido quando forem necessários. O custo de armazenamento é menor, em troca de uma taxa por recuperação. Ela tem duração mínima de 30 dias e cobra objetos menores que 128 KB como se tivessem 128 KB.

S3 Glacier Flexible Retrieval serve para arquivamento de longo prazo, como backups antigos que quase nunca são lidos. O armazenamento é muito barato, mas o objeto não fica disponível em tempo real: a recuperação leva de minutos a horas, e a duração mínima é de 90 dias.

3. Funcionalidades de gerenciamento

O S3 tem ferramentas para cuidar dos dados sem intervenção manual.

Lifecycle, ou ciclo de vida, cria regras para mover objetos para classes mais baratas conforme eles envelhecem, ou para apagá-los depois de um prazo. O exemplo típico é mover logs com mais de 30 dias para uma classe de arquivamento. A seção “Ciclo de vida” abaixo detalha isso.

Versionamento guarda várias versões de um mesmo objeto no bucket. Quando alguém sobrescreve ou apaga um arquivo, a versão anterior continua lá. Isso protege contra exclusões acidentais, mas também significa que o bucket cresce se ninguém limpar as versões antigas.

Replicação copia objetos automaticamente para outro bucket, em outra região. Serve para redundância geográfica ou para diminuir a latência de usuários distantes.

4. Para que serve? (casos de uso)

Hospedagem de sites estáticos: HTML, CSS, JavaScript e imagens podem ficar direto no S3, sem um servidor web rodando em uma EC2. Um blog gerado por um gerador estático é o caso clássico, e a seção prática mostra esse fluxo.

Data lakes e big data: a escalabilidade quase ilimitada faz do S3 o lugar padrão para dados brutos que serão analisados depois.

Backup e disaster recovery: cópias de bancos de dados e de sistemas ficam guardadas com durabilidade alta, a custo baixo, principalmente com regras de ciclo de vida.

Diferença para o EBS e o EFS

É comum confundir o S3 com outros serviços de armazenamento da AWS.

S3 é armazenamento de objetos, acessível pela API e pela internet. Serve bem para arquivos, backups e mídia.

Amazon EBS (Elastic Block Store) é um disco virtual conectado a uma única instância EC2. Ele é usado para o sistema operacional e para bancos de dados que gravam o tempo todo.

Amazon EFS (Elastic File System) é um sistema de arquivos compartilhado, que vários servidores EC2 podem montar ao mesmo tempo, como um servidor de arquivos na rede.

Ciclo de vida

O gerenciamento do ciclo de vida dos objetos (S3 Lifecycle) é um recurso central para controlar custos sem intervenção manual. Você define regras, e o S3 move ou apaga os arquivos com base na idade do objeto e no prefixo dele.

aws s3 lifecycle
figura 1 · aws s3 lifecycle

O processo funciona assim:

1. Objetivo principal

Dados tendem a ser acessados com menos frequência à medida que envelhecem. O ciclo de vida permite mover esses dados para classes mais baratas, ou apagá-los quando não forem mais necessários, sem alguém precisar fazer isso manualmente.

2. Ações de ciclo de vida

Existem dois tipos de ação. Ações de transição definem quando um objeto muda de classe. Por exemplo, um arquivo recém-criado fica no S3 Standard, depois de 30 dias sem acesso pode ir para o S3 Standard-IA e, depois de 90 dias, para o S3 Glacier. Ações de expiração definem quando o objeto é apagado de vez. Logs de servidor que só precisam ser guardados por um ano podem expirar em 365 dias.

3. Aplicação prática

Imagine um e-commerce. As imagens de produtos novos são acessadas o tempo todo e ficam no S3 Standard. Produtos antigos ou fora de linha raramente são vistos, então as imagens deles podem ir para o S3 One Zone-IA, uma classe mais barata para dados que podem ser recriados, ou para arquivamento. Assim você paga o menor preço possível por cada conjunto de dados.

Diferença de custo entre o S3 e o EBS

A diferença de custo entre S3 e EBS vem da arquitetura de cada um: armazenamento de objetos de um lado, armazenamento em bloco do outro.

Amazon S3

O S3 costuma ser a opção mais econômica para grandes volumes, backups e arquivos estáticos, principalmente por causa das classes de armazenamento e do ciclo de vida.

O modelo de cobrança segue o volume armazenado, as requisições feitas e a transferência de dados. Você pode configurar o ciclo de vida para mover dados antigos para classes mais baratas conforme a frequência de acesso cai.

Amazon EBS

O EBS costuma ser mais caro por GB, porque entrega armazenamento de blocos com alta performance e baixa latência, necessário para rodar sistemas operacionais e bancos de dados. Ele funciona como um disco persistente ligado a uma instância EC2, e os volumes são replicados dentro de uma zona de disponibilidade. Você paga pela capacidade provisionada e, em alguns tipos de volume, pelas operações de entrada e saída (IOPS).

Resumo da decisão financeira

CaracterísticaAmazon S3Amazon EBS
Tipo de custoVolume armazenado e requisições. Otimizável com ciclo de vida.Capacidade provisionada e performance (IOPS), para rodar junto com a EC2.
Melhor paraBackups, arquivos de mídia, sites estáticos, data lakes.Sistema operacional, bancos de dados, aplicações que gravam com frequência.
OtimizaçãoMove dados antigos para classes muito baratas (como o Glacier) automaticamente.O custo é mais fixo e depende do tamanho do disco escolhido para a instância.

Se o objetivo é arquivar dados ou guardar arquivos que não precisam ser processados em tempo real por um sistema operacional, o S3 costuma sair muito mais barato, por causa das camadas de armazenamento. Se o dado precisa estar num disco onde um software grava o tempo todo, como um banco de dados, o EBS é a escolha técnica.

Usando o S3 na prática

As seções acima explicam o que é o S3. Esta parte mostra como usá-lo no dia a dia, com a CLI da AWS e com código em Go e TypeScript.

Os exemplos seguem o fluxo do Grijp.Blog, que é publicado em S3 com CloudFront na região sa-east-1 (São Paulo): pnpm build gera a pasta dist/, aws s3 sync envia os arquivos para o bucket, e uma invalidação do CloudFront remove as cópias antigas da borda. Os nomes de bucket e de distribuição são placeholders, e nenhum ID de conta aparece aqui. Como a configuração real do blog não foi revisada nesta nota, trate os trechos como exemplos do fluxo e não como cópia da configuração atual.

Preparar a CLI

A CLI lê credenciais de um perfil. Use aws configure sso (ou aws configure, se você usa chave de acesso) e confirme a identidade com:

aws sts get-caller-identity

A saída é um JSON com Account, Arn e UserId. Não cole esse JSON em lugar público. A escolha da credencial importa: use um usuário ou um SSO com permissões limitadas, e não a conta root. Veja IAM para o motivo e para como montar essas permissões.

A região padrão do perfil decide para onde vão os comandos que não recebem --region. Por isso os exemplos passam a região explicitamente.

Criar o bucket com a configuração mínima

Criar o bucket em outra região exige um LocationConstraint com o nome da região. Sem ele, o bucket é criado em us-east-1 por padrão:

aws s3api create-bucket \
  --bucket meu-blog-exemplo \
  --region sa-east-1 \
  --create-bucket-configuration LocationConstraint=sa-east-1

Em seguida, mantenha o acesso público bloqueado. Buckets novos já vêm assim, mas vale conferir e registrar a configuração:

aws s3api put-public-access-block \
  --bucket meu-blog-exemplo \
  --public-access-block-configuration BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true

Cada uma das quatro opções fecha uma porta diferente. BlockPublicAcls rejeita ACLs públicas nas chamadas de escrita. IgnorePublicAcls ignora as ACLs públicas que já existem. BlockPublicPolicy rejeita políticas de bucket que deem acesso público. RestrictPublicBuckets limita o acesso a buckets com política pública às contas e serviços autorizados. Essas configurações não alteram políticas e ACLs que já existem: se você remover o bloqueio, um bucket com política pública volta a ficar público.

Desde janeiro de 2023 todo objeto novo é criptografado com SSE-S3 (chaves gerenciadas pela S3) automaticamente, sem custo adicional, então não é preciso configurar nada para isso. Se você quiser chaves próprias, use SSE-KMS, que tem custo de KMS.

Versionamento é ativado assim:

aws s3api put-bucket-versioning \
  --bucket meu-blog-exemplo \
  --versioning-configuration Status=Enabled

Ative versionamento só junto com uma regra de ciclo de vida que apague versões antigas (há um exemplo mais abaixo). Sem isso, o bucket cresce a cada sobrescrita.

Enviar, listar e remover com a CLI

Estes são os comandos do dia a dia:

aws s3 ls s3://meu-blog-exemplo/ --recursive --human-readable --summarize
aws s3 cp ./relatorio.pdf s3://meu-blog-exemplo/docs/relatorio.pdf
aws s3 cp s3://meu-blog-exemplo/docs/relatorio.pdf ./baixado.pdf
aws s3 rm s3://meu-blog-exemplo/docs/relatorio.pdf
aws s3 rm s3://meu-blog-exemplo/rascunhos/ --recursive

ls lista objetos. --recursive percorre todos os prefixos, e --human-readable com --summarize mostra tamanhos legíveis e o total. cp copia nos dois sentidos, e a CLI imprime uma linha por arquivo no formato upload: origem to destino ou download: .... rm apaga; sem --recursive, ele apaga só um objeto, e com ele apaga tudo sob o prefixo.

O comando de sincronização é o que mais se usa em deploy:

pnpm build
aws s3 sync dist/ s3://meu-blog-exemplo --delete --dryrun
aws s3 sync dist/ s3://meu-blog-exemplo --delete

O sync compara cada arquivo pelo tamanho e pela data de modificação, e envia só o que é novo ou mudou. Por isso o primeiro comando usa --dryrun: ele imprime o que faria sem fazer nada. Confira a lista antes de rodar o segundo.

O --delete faz o bucket ficar igual à pasta de origem, apagando no destino o que não existe em dist/. Isso vale para uploads que você colocou no bucket fora do build, como imagens soltas. Arquivos excluídos por --exclude não são apagados. A regra de comparação pode ser trocada por --size-only, que compara só o tamanho.

Cabeçalhos de cache por tipo de arquivo

Um arquivo com nome fixo, como index.html, muda a cada deploy e precisa ser revalidado com frequência. Um arquivo com hash no nome, como app.3f9a1c.js, nunca muda: se o conteúdo mudou, o nome também mudou. O primeiro pode ter cache curto, e o segundo pode ter cache de um ano. O diagrama mostra a separação:

s3 03 sync cache control
figura 2 · s3 03 sync cache control

A forma prática é fazer dois sync, um para cada tipo, com --cache-control diferente:

aws s3 sync dist/ s3://meu-blog-exemplo --exclude "*.html" \
  --cache-control "public,max-age=31536000,immutable"

aws s3 sync dist/ s3://meu-blog-exemplo --exclude "*" --include "*.html" \
  --cache-control "no-cache"

A ordem de --exclude e --include importa: a CLI aplica as regras na sequência em que aparecem. No primeiro comando, todo HTML fica de fora. No segundo, tudo fica de fora, exceto os HTML.

Um detalhe que surpreende: o sync só reenvia o que mudou. Um objeto que não foi alterado mantém os cabeçalhos com que foi enviado antes, então um --cache-control novo só vale para os arquivos que forem reenviados depois da mudança. Se você mudar o cabeçalho de todos, precisa forçar o reenvio.

Os cabeçalhos também influenciam o CloudFront, mas com uma ressalva. A política de cache gerenciada CachingOptimized define o próprio TTL, e não usa o Cache-Control do origin. Para respeitar o cabeçalho do origin, a política a usar é a UseOriginCacheControlHeaders. Veja Amazon CloudFront.

URL pré-assinada: upload e download

Imagine um usuário que envia uma foto de perfil para um app. Fazer o arquivo passar pelo seu backend custa banda, memória e tempo de resposta, e obriga o cliente a ter credenciais, o que é pior ainda. A URL pré-assinada resolve isso. O backend, que tem a credencial, assina um pedido de PUT ou GET para uma chave específica e por um prazo curto, e devolve a URL. O navegador usa essa URL para falar direto com o S3. O diagrama mostra o fluxo:

s3 01 upload url pre assinada
figura 3 · s3 01 upload url pre assinada

A URL funciona como um token ao portador: quem a tiver pode usá-la até o prazo acabar, sem credencial nenhuma. Por isso ela deve ter validade curta e ser gerada só quando o usuário precisa.

Duas regras de validade pesam mais do que parecem. Pela documentação, a URL vale pelo prazo pedido, com limite de 7 dias quando gerada pela CLI ou por SDK. Mas ela também morre junto com a credencial que a assinou. Se o backend usa uma role com sessão de uma hora, a URL assinada dentro dessa sessão deixa de funcionar quando a sessão acaba, mesmo que o prazo ainda não tenha passado.

A CLI gera URLs de download (GET), com 3600 segundos por padrão:

aws s3 presign s3://meu-blog-exemplo/docs/relatorio.pdf --expires-in 600 --region sa-east-1

Para upload, a CLI não serve, porque aws s3 presign gera apenas a URL de GET. Para PUT, use um SDK. Este é o código em Go, que compilei e executei localmente com credenciais fictícias, sem chamar a AWS:

func urlDeUpload(ctx context.Context, cfg aws.Config, bucket, chave, tipo string) (string, error) {
	presign := s3.NewPresignClient(s3.NewFromConfig(cfg))
	req, err := presign.PresignPutObject(ctx, &s3.PutObjectInput{
		Bucket:      aws.String(bucket),
		Key:         aws.String(chave),
		ContentType: aws.String(tipo),
	}, s3.WithPresignExpires(5*time.Minute))
	if err != nil {
		return "", err
	}
	return req.URL, nil
}

A URL gerada tem este formato (a assinatura foi omitida):

https://exemplo-bucket.s3.sa-east-1.amazonaws.com/fotos/capa.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=...&X-Amz-Date=20261027T192206Z&X-Amz-Expires=300&X-Amz-SignedHeaders=content-type%3Bhost&x-id=PutObject&X-Amz-Signature=...

O campo X-Amz-SignedHeaders=content-type;host diz que o Content-Type faz parte da assinatura. Quem fizer o PUT precisa mandar exatamente o mesmo tipo, senão a S3 recusa a chamada.

Em TypeScript, o equivalente usa getSignedUrl. Também compilei e executei este trecho com credenciais fictícias:

import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3";
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";

const s3 = new S3Client({ region: "sa-east-1", requestChecksumCalculation: "WHEN_REQUIRED" });

export async function urlDeUpload(bucket: string, chave: string, tipo: string): Promise<string> {
  const comando = new PutObjectCommand({ Bucket: bucket, Key: chave, ContentType: tipo });
  return getSignedUrl(s3, comando, { expiresIn: 300 });
}

Essa opção requestChecksumCalculation: "WHEN_REQUIRED" existe porque, sem ela, a versão do SDK que testei colocou na URL um parâmetro x-amz-checksum-crc32=AAAAAA==, que é o CRC32 de um corpo vazio. Com a opção, o parâmetro some. Eu testei só a URL gerada, não fiz um upload real contra a S3, então não afirmo que o upload falharia sem a opção. Deixe o parâmetro explícito e teste com um arquivo de verdade antes de publicar.

No navegador, o envio fica assim (trecho não executado):

const { url } = await fetch("/api/uploads", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ nome: arquivo.name, tipo: arquivo.type }),
}).then((r) => r.json());

const resposta = await fetch(url, {
  method: "PUT",
  headers: { "Content-Type": arquivo.type },
  body: arquivo,
});
if (!resposta.ok) throw new Error(`upload falhou: ${resposta.status}`);

O navegador só consegue fazer esse PUT se o bucket aceitar a origem da página. Isso é CORS, configurado no bucket com um JSON como este:

[
  {
    "AllowedHeaders": ["*"],
    "AllowedMethods": ["PUT", "GET"],
    "AllowedOrigins": ["https://www.exemplo-blog.com"],
    "ExposeHeaders": ["ETag"],
    "MaxAgeSeconds": 3000
  }
]
aws s3api put-bucket-cors --bucket meu-blog-exemplo --cors-configuration file://cors.json

Pela documentação, o S3 usa a primeira regra que bate com a origem, o método e os cabeçalhos da requisição. AllowedOrigins: ["*"] aceita qualquer site, então, para upload, prefira a origem exata. Para o ETag aparecer no código do navegador, ele precisa estar em ExposeHeaders.

Acesso negado é o erro mais comum e tem causas diferentes. O diagrama abaixo mostra a ordem de investigação:

s3 02 diagnostico 403
figura 4 · s3 02 diagnostico 403

Políticas de bucket

A política de bucket é uma política de recurso, anexada ao próprio bucket. Ela complementa a política da identidade no IAM: ela pode conceder acesso a outra conta, a um serviço (como a CloudFront) ou restringir quem já tem acesso. A chave é que o Block Public Access prevalece sobre ela. Uma política que dê acesso público é rejeitada quando você tenta aplicá-la com BlockPublicPolicy ligado, e o bloqueio também impede que o acesso público funcione mesmo se a política já existir.

Um exemplo comum é exigir HTTPS. O trecho abaixo mostra a forma da política, e vale testá-la em um bucket de teste primeiro, porque o Block Public Access avalia uma declaração com Principal * como possivelmente pública, mesmo quando é um Deny (esse ponto não foi verificado nesta revisão):

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "NegarSemHTTPS",
      "Effect": "Deny",
      "Principal": "*",
      "Action": "s3:*",
      "Resource": [
        "arn:aws:s3:::meu-blog-exemplo",
        "arn:aws:s3:::meu-blog-exemplo/*"
      ],
      "Condition": { "Bool": { "aws:SecureTransport": "false" } }
    }
  ]
}
aws s3api put-bucket-policy --bucket meu-blog-exemplo --policy file://politica.json

A política do CloudFront para bucket privado (com OAC) tem outra forma e está em Amazon CloudFront.

Ciclo de vida na prática

A regra abaixo arquiva logs por um ano e limpa versões antigas de todo o bucket:

{
  "Rules": [
    {
      "ID": "logs-por-um-ano",
      "Status": "Enabled",
      "Filter": { "Prefix": "logs/" },
      "Transitions": [
        { "Days": 30, "StorageClass": "STANDARD_IA" },
        { "Days": 90, "StorageClass": "GLACIER" }
      ],
      "Expiration": { "Days": 365 }
    },
    {
      "ID": "limpar-versoes-antigas",
      "Status": "Enabled",
      "Filter": { "Prefix": "" },
      "NoncurrentVersionExpiration": { "NoncurrentDays": 30 }
    }
  ]
}
aws s3api put-bucket-lifecycle-configuration \
  --bucket meu-blog-exemplo \
  --lifecycle-configuration file://lifecycle.json

Cada Filter precisa ter exatamente um critério, aqui o prefixo. NoncurrentVersionExpiration só faz sentido com versionamento ligado: ele apaga versões que já não são as atuais depois do número de dias informado.

Três restrições da documentação pesam nessa regra. Objetos menores que 128 KB não são transferidos por padrão para nenhuma classe, porque o custo da requisição de transição superaria a economia. A transição para Standard-IA exige que o objeto fique pelo menos 30 dias antes. E quem apaga ou transfere um objeto da Glacier antes de 90 dias paga o restante desse prazo, por isso a expiração aqui vem só depois de 90 dias. As transições são assíncronas: a data da regra não significa que a mudança acontece exatamente nesse dia.

Erros comuns

O --delete do sync é a opção que mais merece respeito, porque ele faz exatamente o que o nome diz. Quem sobe uma imagem diretamente no bucket e depois roda o deploy com --delete perde a imagem. A solução é guardar uploads de usuário em um prefixo separado e não usar --delete na raiz do bucket, ou usar --exclude para esse prefixo.

Um bucket que começa público por engano normalmente foi configurado com a desativação de um dos quatro bloqueios, ou com uma política que dá acesso a *. Confira o get-public-access-block e a política antes de qualquer outra coisa.

Versionamento sem regra de limpeza faz o custo crescer em silêncio. Sempre que ligar o versionamento, adicione NoncurrentVersionExpiration no mesmo lote de mudanças.

Um upload por URL pré-assinada falha com SignatureDoesNotMatch quando o Content-Type enviado é diferente do assinado, ou quando o relógio da máquina que gerou a URL está fora de sincronia. A documentação recomenda sincronizar o relógio por NTP.

Um upload que falha com ExpiredToken quase sempre é uma credencial temporária que já expirou, e não uma URL velha. Gere a URL de novo com uma credencial válida.

Fontes

Consultadas em 27/10/2026. Os limites de tamanho, as classes de armazenamento, as regras de ciclo de vida, o funcionamento das URLs pré-assinadas e o Block Public Access vêm das páginas da documentação oficial listadas abaixo. Os exemplos de código em Go e TypeScript foram compilados e executados localmente, com credenciais fictícias e sem chamada à AWS.

Não verificado nesta revisão

Preços por GB, por requisição e de recuperação não foram consultados, porque as páginas de preço são dinâmicas. Por isso a nota não traz valores em dinheiro.

O upload real por URL pré-assinada não foi feito contra a S3. Só a URL gerada foi conferida.

A mensagem exata que o navegador mostra quando o CORS bloqueia o PUT não foi reproduzida.

O comportamento do Block Public Access com uma declaração Deny de Principal * não foi testado.

O comportamento da S3 para chave inexistente em bucket privado com OAC (um 403 em vez de 404) é o conhecido, mas não foi reproduzido aqui.

↑ Voltar ao topo

Atalhos de teclado

Abrir busca
/
Alternar tema claro/escuro
t
Aula anterior na série
←
Próxima aula na série
→
Lista de atalhos
?