07 - S3: almacenamiento de objetos
Las aplicaciones no guardan todo en la base de datos: imágenes, documentos, los ficheros que suben los usuarios. Ese material pertenece al almacenamiento de objetos, cuyo estándar de facto es la API de S3 de AWS: la hablan todas las nubes y los SDK de todas las plataformas. Para el desarrollo se ejecuta esa API en local con MinIO, un servidor compatible desplegado con Docker (tema 02).
1. Almacenamiento de objetos
Un servicio de objetos guarda objetos en buckets:
- Un bucket es un contenedor con nombre único (p. ej.
miapp): la unidad de organización y de permisos. - Un objeto son los datos + una clave + metadatos. La clave es un camino plano (
documentos/2026/libro.pdf): parece una ruta con carpetas, pero es una cadena, no un directorio. - Los objetos no se editan en su sitio: se sobrescriben publicando de nuevo la misma clave.
La aplicación habla con la API; el navegador solo con URLs: directas si el bucket es público, o prefirmadas (sección 3).
| Sistema de ficheros | Almacenamiento de objetos | |
|---|---|---|
| Unidad | Fichero en un árbol de carpetas | Objeto con clave plana dentro de un bucket |
| Acceso | Rutas locales del sistema | API HTTP por red |
| Metadatos | Los del sistema operativo | Los que lleve cada objeto |
| Edición | Los ficheros se modifican | Los objetos se sobrescriben |
| Escalado | Un servidor, su disco | Distribuido en muchos nodos (pensado para nube) |
2. S3: el estándar
S3 (*Simple Storage Service*) es el servicio de objetos de AWS. Su API es el estándar de facto: la implementan las demás nubes y servidores compatibles como MinIO, y el mismo código del SDK funciona contra cualquiera de ellos cambiando la configuración (endpoint y credenciales), nunca el programa.
Las operaciones básicas son pocas:
| Operación | Qué hace | Como |
|---|---|---|
| CreateBucket | crea el bucket | mkdir |
| PutObject | sube datos y metadatos bajo una clave | cp |
| GetObject | descarga los datos de una clave | cat |
| HeadObject | solo los metadatos (tipo, tamaño, fecha) | stat |
| DeleteObject | borra el objeto | rm |
| ListObjectsV2 | lista las claves del bucket, filtradas por prefijo | ls |
La autenticación usa credenciales access key / secret key: el SDK firma cada petición con ellas. No hay sesión ni token: la firma viaja en la petición y las credenciales solo las conoce la aplicación, que las recibe como configuración, nunca en el código.
3. Público, privado y URLs prefirmadas
Cada bucket define una política de acceso para sus objetos:
- Privado (por defecto): una petición anónima recibe 403. Solo acceden la aplicación (peticiones firmadas) y las URLs prefirmadas.
- Público de lectura: la URL directa
https://servidor/bucket/clavesirve el objeto a cualquiera. Es el modo de portadas, avatares, CSS y demás ficheros que solo se descargan; escribir sigue exigiendo credenciales.
Una URL prefirmada (*presigned URL*) la genera la aplicación con sus credenciales: lleva incrustadas la firma y una expiración. Quien la recibe descarga el objeto sin credenciales durante ese tiempo; al caducar, la URL vuelve a dar 403. Es la forma de compartir un objeto privado sin exponer credenciales ni hacer copias.
4. ¿Un bucket o varios?
La tentación inicial es un bucket por sección de la aplicación (books, movies…). Lo idiomático es lo contrario: la organización por tipo de contenido se resuelve con prefijos, y los buckets se reservan para configuraciones distintas.
documentos/, portadas/). Datos con una configuración diferente, como ficheros privados de la aplicación frente a imágenes públicas: otro bucket.
5. MinIO: S3 en local
MinIO es un servidor open source compatible con la API S3: un binario único que arranca en un segundo con Docker. En desarrollo simula AWS sin coste ni red; el código del SDK escrito contra MinIO funciona en producción contra S3 cambiando el endpoint y las credenciales, que viajan en configuración.
6. Arranque con Docker
$ docker run -d --name minio \
-p 9000:9000 -p 9001:9001 \
-e MINIO_ROOT_USER=admin \
-e MINIO_ROOT_PASSWORD=secreto123 \
-v minio-data:/data \
elestio/minio server /data --console-address ":9001"
9000: la API S3, la que consumen el SDK ymc.9001: la consola web, para trabajar desde el navegador (http://localhost:9001).- El volumen
minio-dataguarda los objetos fuera del contenedor. - La imagen
elestio/minioreplica en Docker Hub las compilaciones oficiales de MinIO, cuya imagen original ya no se publica allí y se descarga desdequay.io.
7. La consola y mc
La consola web permite crear buckets, subir y descargar ficheros, fijar la política de acceso de cada bucket y crear access keys para las aplicaciones (el usuario root del arranque no se usa en el código).
Para línea de comandos, mc (MinIO Client):
$ mc alias set local http://localhost:9000 admin secreto123 # conectar $ mc mb local/miapp # crear el bucket $ mc cp libro.pdf local/miapp/documentos/libro.pdf # subir $ mc ls local/miapp # listar
Con dos comandos más se practica el acceso público y prefirmado de la sección 3:
$ mc anonymous set download local/miapp # abre http://localhost:9000/miapp/documentos/libro.pdf en el navegador $ mc share download --expire 5m local/miapp/documentos/libro.pdf # URL prefirmada: funciona 5 minutos y luego da 403
8. Uso desde Java
El SDK de AWS apunta a MinIO con dos ajustes: el endpoint local y el acceso por ruta (path-style), que es como funciona MinIO:
S3Client s3 = S3Client.builder()
.endpointOverride(URI.create("http://localhost:9000"))
.region(Region.US_EAST_1)
.credentialsProvider(StaticCredentialsProvider.create(
AwsBasicCredentials.create("admin", "secreto123")))
.forcePathStyle(true) // URL http://host:9000/bucket/clave
.build();
s3.putObject(PutObjectRequest.builder()
.bucket("miapp")
.key("documentos/libro.pdf")
.build(),
RequestBody.fromFile(Path.of("libro.pdf")));
El mismo SDK genera URLs prefirmadas desde el código: una vez creado un S3Presigner con la misma configuración que el S3Client, la petición presignada devuelve la URL con expiración:
URL url = presigner.presignGetObject(GetObjectPresignRequest.builder()
.signatureDuration(Duration.ofMinutes(10))
.getObjectRequest(GetObjectRequest.builder()
.bucket("miapp").key("documentos/libro.pdf").build())
.build())
.url();
endpoint, no el programa.
En el siguiente tema, el almacén de objetos entra en la arquitectura de la aplicación como un servicio más del compose.yaml.