Trucos avanzados para Amazon S3 o MinIO

Según vas ampliando las features de tu aplicación, no solo vale con subir y bajar ficheros a Amazon S3 o MinIO, cada vez necesitarás realizar procedimientos más avanzados. En este artículo te cuento cómo sacarle más provecho a tu almacenaje "en la nube" con ejemplos.
Trucos avanzados para Amazon S3 o MinIO

Antiguamente, los ficheros que los usuarios subían a una aplicación se solían almacenar en el disco duro del servidor. Aquí se almacenaban en bloque, generalmente junto al código de la aplicación o al menos, en el mismo disco duro. Sin embargo, con la llegada de los contenedores, de la ciencia DevOps y de los contenedores Docker y Kubernetes, ha cambiado la forma en la que alojamos los ficheros que los usuarios suben a nuestra web.

Si has estado en una cueva o acabas de comenzar con el desarrollo, Amazon S3 es un servicio de AWS que permite alojar datos sin límite virtual de espacio: imágenes, vídeos, documentos e incluso información en general. Estos ficheros son completamente externos al proyecto web y se guardan en la nube de Amazon, por lo que no deberás preocuparte por si se rompe el disco o por un FTP: Amazon se encarga de todo.

Amazon S3 proporciona una interfaz de servicio web simple para que puedas almacenar y recuperar tantos datos como necesites desde cualquier lugar. Con este servicio, puedes crear fácilmente aplicaciones que utilicen almacenamiento nativo en la nube y evitar así que necesiten espacio dentro del contenedor donde están funcionando (o en el sistema de archivos local). Debido a que Amazon S3 es un servicio altamente escalable y vas a pagar por lo que uses, puedes comenzar usando poco espacio y escalar la aplicación según sea necesario.

Amazon S3 proporciona almacenamiento con altos niveles de rendimiento y disponibilidad (siempre es rápido y siempre funciona) para escalar y mantener fácilmente aplicaciones móviles y basadas en Internet de alta velocidad y rentables. S3 te permite agregar tanto contenido como necesites y acceder a él desde cualquier lugar para que pueda implementar aplicaciones más rápido.

Sin embargo, este servicio que nos ofrece Amazon tiene una pega… el coste añadido que supone. Es aquí cuando entra en juego MinIO. En un artículo anterior te contaba cómo configurar un servidor MinIO para que hiciera las veces de un Amazon S3 a coste cero. En él, también te enseñé a configurar los bucket, los usuarios y las políticas de MinIO, un servidor de almacenamiento compatible con Amazon S3 pero autoalojado, sin coste añadido, ideal para las pruebas en desarrollo o para la creación de prototipos.

MinIO es un alojamiento de objetos compatible con S3, lo que quiere decir que la mayoría de funciones de S3 están implementadas, por lo que no tendrás que pagar por espacio o peticiones. Además, el código nunca debería enterarse si un día cambias de uno a otro: todo seguirá funcionando igual. Sin embargo, tú eres el responsable de vigilar que es accesible, de los discos duros donde esté montado, del servidor, etc. Quizás en producción no es la mejor solución cuando quieres desentenderte del mantenimiento, pero para los entornos locales o de prueba, es la mejor opción para no gastar mucho dinero. Por ello, te animo a que lo pruebes antes de lanzarte a S3.

Sin embargo, en aquel artículo no llegamos a utilizar el propio almacenaje desde ninguna aplicación externa. Pero ese momento ha llegado: hoy toca ver ejemplos del mundo real en los cuales necesitamos subir un fichero desde el frontal o comprobar si un usuario tiene permisos en nuestra App antes de permitir descargar. En definitiva, trucos interesantes para usar en Amazon S3 y MinIO en la vida real que te pueden venir bien cuando estés usando este tipo de servicios de almacenaje de ficheros.

Y no te preocupes, no deberías tener problemas con estos trucos, puesto que funcionan tanto en Amazon S3 como en MinIO.

Casos de uso típicos de Object Storage

Tanto si te has decidido por Amazon S3, como si quieres empezar a usar MinIO en tu entorno de pruebas, el uso típico de un Object Storage suele ser el mismo: alojar los ficheros que tus usuarios suben a la aplicación. Las imágenes de sus perfiles, los documentos que generan en la app, imágenes de los productos que quieren vender, etc.

Estos casos de uso tradicionales básicamente se resumen en alojar estáticos o ficheros que no se incluyen con el código fuente del proyecto y, que en algún sitio tienen que estar alojados.

Los ficheros públicos, como imágenes o vídeos, tienen permisos de lectura, por lo que el navegador puede acceder a ellos y nuestro código solo tiene que hacer referencia a una URL (no al fichero en sí). Por ejemplo <img src="https://<bucket-name>.s3.amazonaws.com/<key>" >

Por otro lado, a los documentos privados que solo deben ser accesibles para ciertos usuarios se accede, generalmente, por el código/server y se descargan a una carpeta temporal. Posteriormente, el server envía al cliente el fichero descargado y lo borra.

Sin embargo, tienes algunas alternativas para evitar que el fichero pase por tu servidor, sin desperdiciar así espacio o tiempo en procesar la descarga. A continuación, te enseño con ejemplos algunos casos de uso un poco más avanzados para Amazon S3 o MinIO.

Subiendo el fichero mediante URL Presigned

Generalmente, cuando el front (llámese Javascript o cualquiera de sus framework) tiene que subir un fichero al servidor, primero hay una comunicación cliente - servidor para subir el archivo y posteriormente servidor - S3 para guardar el fichero.

Sin embargo, ¿sabías que esto no es necesario realmente? Hay un camino más rápido para el servidor que permite que el front se comunique directamente con S3 para que suba el fichero, dejando de lado al servidor (y sin exponer los credenciales de la conexión).

El medio para lograrlo son los enlaces URL prefirmados (presigned en inglés). Se denominan prefirmadas porque aquel que use la URL no tiene que aportar los credenciales (ya está autorizada previamente).

El servidor le pide al alojamiento S3-compatible que genere una URL prefirmada (para que, posteriormente, no tenga que volver a autenticarse. Algo así como un cheque firmado en blanco) y, cuando el alojamiento la tenga y la entregue, el servidor se la enviará al cliente. Ahora el cliente puede subir el fichero, sin pasar por el servidor, utilizando esta URL.

Tienes el código completo en la propia página de MinIO. En él y mediante un alojamiento MinIO, un servidor Express y una página HTML que se ejecuta en el cliente, consiguen subir un fichero desde el cliente al alojamiento sin pasar por el servidor. El núcleo de la cuestión reside en el siguiente código:

const Minio = require('minio')

var client = new Minio.Client({
    endPoint: 'play.min.io',
    port: 9000,
    useSSL: true,
    accessKey: 'Q3AM3UQ867SPQQA43P2F',
    secretKey: 'zuf+tfteSlswRu7BJ86wekitnifILbZam1KYY3TG'
})

//Realizar esta acción cuando sea necesario, seguramente en el router
client.presignedPutObject('uploads', 'newName', (err, url) => {
  if (err) throw err
  //url contiene la URL presigned
  res.end(url)
})

Descargar ficheros sin listar el directorio

Las políticas de seguridad de los bucket en un alojamiento S3-compatible son algo vital para ofrecer seguridad. A nadie le gustaría que los datos privados sean accesibles sin autenticación a través de Internet. De igual forma, en ocasiones, necesitamos hacer públicos los ficheros de un bucket (porque a lo mejor son estáticos de una web, o son informes públicos que nos gustaría compartir con todo el mundo).

En estos casos, tanto Amazon S3 como MinIO y la mayoría de Object Storages tienen una política de bucket llamada download, que permite justamente eso: hacer que todos los ficheros del bucket sean públicos y descargables (o de una carpeta concreta del bucket). Pero tienen una pega que puede ser molesta: permiten listar todos los ficheros que contienen.

Esto es genial si queremos un listado de informes que están dentro de la carpeta, pero no queremos que liste todos los avatares de los usuarios (por privacidad y seguridad). Es por ello que tenemos que crear una política de seguridad del bucket que permita descargar los ficheros pero no listar el contenido de la carpeta. Esto se consigue aplicando la siguiente política:

{
   "Statement":[
      {
         "Action":[
            "s3:GetBucketLocation"
         ],
         "Effect":"Allow",
         "Principal":{
            "AWS":[
               "*"
            ]
         },
         "Resource":[
            "arn:aws:s3:::<bucket_name>"
         ]
      },
      {
         "Action":[
            "s3:GetObject"
         ],
         "Effect":"Allow",
         "Principal":{
            "AWS":[
               "*"
            ]
         },
         "Resource":[
            "arn:aws:s3:::<bucket_name>/*"
         ]
      }
   ],
   "Version":"2012-10-17"
}

Esta política permite descargar los ficheros que están contenidos en bucket_name pero no permite que se listen los directorios. La política aplica a todo el bucket completo. Una modificación sería mostrar/ocultar listados en una carpeta concreta dentro del bucket. Esto lo podemos conseguir con:

{
 "Version":"2012-10-17",
 "Statement": [
   {
     "Sid": "ListarDirectorio",
     "Effect": "Allow",
     "Action": ["s3:ListBucket"],
     "Resource": ["arn:aws:s3:::<bucket_name>/<custom_dir>"]
   }
 ]
}

Esta última modificación añade una condición de que sí se podrá listar el contenido en <bucket name>/<custom_dir> pero en ningún otro sitio (pues la anterior política sigue vigente).

¡Por cierto! Las políticas de Amazon S3 pueden ser un poco complicadas de gestionar, así que para que te sea más fácil, te dejo un enlace al AWS Policy Generator.

Descargando un fichero restringido

Imagina que tienes una web donde los usuarios registrados o suscritos pueden descargar ciertos ficheros pero los usuarios anónimos no. Tienes varias opciones para manejar esta situación:

  • Dejarlos accesibles a través del servidor web, pero no mostrar la URL. Esto es útil y puede funcionar, pero si un usuario registrado le pasa la URL a otro usuario, nada impide que este segundo se pueda descargar el archivo.

  • En tu servidor web, comprobar si el usuario tiene permisos (de sesión, de pago, etc.) y si, efectivamente tiene permisos, hacer que el servidor lea el fichero y lo devuelva al usuario. De esta forma, aún teniendo la URL, el usuario anónimo no podrá bajar el fichero. Sin embargo, este sistema tiene una pega: el servidor está gastando tiempo en leer el fichero original y en entregarlo al cliente. Si son ficheros grandes, estás fastidiado.

  • Confiar en un alojamiento S3-compatible para que haga las veces de guardian de tus ficheros. Te lo cuento en seguida.

El método más corriente es generar un enlace privado cada vez que el cliente intenta descargar un fichero restringido, pero el truco está en que dicho enlace caduca pasado unos instantes. De esta forma, el servidor le pide a S3 que genere un enlace prefirmado con un tiempo de expiración de un par de segundos y cuando S3 lo devuelva al servidor, este lo reenvía al cliente. Así el cliente accederá a la URL y se descargará el fichero. Pero si intenta compartir el enlace, ¡lástima, ya ha caducado!

$s3Client = new Aws\S3\S3Client([
    // ... config
]);

$cmd = $s3Client->getCommand('GetObject', [
    'Bucket' => 'my-bucket',
    'Key' => 'testKey'
]);

$request = $s3Client->createPresignedRequest($cmd, '+20 seconds');

// Get the actual presigned-url
$presignedUrl = (string)$request->getUri();

Forzar a descargar el fichero en vez de mostrarlo

En ocasiones el navegador decide si descargar el fichero o mostrarlo dentro de él. Ten en cuenta que S3 almacena información y ficheros, pero también metadata (información asociada al propio fichero). Es aquí donde haremos la magia: cuando subamos un fichero, le añadiremos la cabecera Content-Disposition: attached al metadata del fichero que estemos subiendo.

De esta forma, cuando el usuario acceda a dicho fichero, S3 enviará las cabeceras al navegador y este verá que el fichero tiene que ser descargado, no mostrado por pantalla.

$result = $client->putObject(array(
  'Bucket'     => $bucket,
  'Key'        => $fileName,
  'SourceFile' => $fileTempName,
  'ContentDisposition' => 'attachment', //<-- fuerza la descarga
  'Metadata'   => array(
    'Foo' => 'abc',
    'Baz' => '123'
  )
 ));

También tienes la opción de establecer el metadata desde la consola de Amazon o MinIO. Aquí tienes un enlace donde te detallan como añadir metadata desde la consola de Amazon S3.

Conclusión

Los sistemas de almacenamiento son la abstracción perfecta cuando necesitas guardar cantidades ingentes de ficheros fuera de tu propio proyecto. Permiten al desarrollador olvidarse de cuotas de espacio, permisos Linux, etc. Y gracias a las features que añaden a estos servicios, se pueden hacer maravillas con la gestión de ficheros.

Espero que hayas aprendido algunas cosas interesantes sobre estos sistemas de almacenamiento y que lo puedas aprovechar en tu próximo proyecto.

¡Qué tengas un feliz coding!