====== 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.
@startuml
rectangle "Aplicación (SDK)" as app
rectangle "Navegador" as web
rectangle "Servidor de objetos (S3 / MinIO)" as srv {
rectangle "bucket miapp" as b {
rectangle "documentos/2026/libro.pdf" as o1
rectangle "portadas/978316000123.webp" as o2
}
}
app -down-> srv : API HTTP\naccess key + secret key
web -down-> srv : URL directa o prefirmada
o1 -[hidden]right- o2
@enduml
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/clave'' sirve 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.
La configuración es del **bucket** y afecta a todo lo que contiene. Datos con la misma configuración: un bucket, separados por prefijos (''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 y ''mc''.
* ''9001'': la **consola web**, para trabajar desde el navegador (''http://localhost:9001'').
* El **volumen** ''minio-data'' guarda los objetos fuera del contenedor.
* La imagen ''elestio/minio'' replica en Docker Hub las compilaciones oficiales de MinIO, cuya imagen original ya no se publica allí y se descarga desde ''quay.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();
La única diferencia entre el MinIO de desarrollo y **S3** en producción es la configuración: URL del endpoint y credenciales. Por eso esos valores van en configuración (variables de entorno), nunca en el código: al cambiar de entorno cambia el ''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''.