====== 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''.