Arquitectura Resiliente: Subidas Directas a S3 con Presigned URLs y Multipart Upload

Imagina que tu servidor es una pequeña oficina de correos y, de repente, 100 usuarios intentan enviar sofás enteros a través de la ventanilla al mismo tiempo. El resultado es inevitable: Bloqueo de memoria (OOM), timeouts y caída del servicio. Si estás manejando subidas de archivos grandes (videos, datasets) pasando los bytes por tu API Backend, estás desperdiciando CPU y ancho de banda.

AWS S3 Multipart Upload con Presigned URLs es una arquitectura donde el backend actúa solo como un "autorizador", entregando permisos temporales al cliente para que este suba el archivo directamente a la nube en fragmentos paralelos. Esto reduce la carga del servidor a casi cero.

El Concepto: El Controlador Aéreo vs. El Cargador

Analogía: En lugar de que tú (el servidor) cargues personalmente las maletas de cada pasajero al avión (S3), tú actúas como el Controlador Aéreo. Le das al pasajero (el cliente) un pase de seguridad temporal (Presigned URL) y le dices: "Ve a la puerta 5 y cárgalo tú mismo".

Multipart Upload: Si la maleta es gigante, la desarmamos en 10 cajas pequeñas y las enviamos por cintas transportadoras separadas al mismo tiempo. Si una caja se cae, solo reenvías esa caja, no todo el envío.

Técnicamente, AWS recomienda usar Multipart Upload para archivos mayores a 100 MB. Es obligatorio para archivos mayores a 5 GB. Al usar AWS SDK v3, podemos orquestar este proceso de manera segura.

Implementación en Node.js (AWS SDK v3)

El flujo requiere tres pasos críticos en el backend: iniciar la carga, firmar las URLs para cada parte y finalizar la carga.


// Stack: Node.js, AWS SDK v3 Client S3
import { 
  S3Client, 
  CreateMultipartUploadCommand, 
  UploadPartCommand, 
  CompleteMultipartUploadCommand 
} from "@aws-sdk/client-s3";
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";

const s3Client = new S3Client({ region: "us-east-1" });
const BUCKET_NAME = "tu-bucket-produccion";

// 1. Iniciar la Carga (Obtener UploadId)
export const startUpload = async (fileName, fileType) => {
  const command = new CreateMultipartUploadCommand({
    Bucket: BUCKET_NAME,
    Key: fileName,
    ContentType: fileType,
  });
  
  const { UploadId } = await s3Client.send(command);
  return { uploadId: UploadId, key: fileName };
};

// 2. Generar URLs Firmadas para las Partes
// El cliente dice: "Tengo 5 partes" -> Servidor devuelve 5 URLs
export const getUploadUrls = async (fileName, uploadId, parts) => {
  const promises = [];
  
  for (let index = 0; index < parts; index++) {
    const command = new UploadPartCommand({
      Bucket: BUCKET_NAME,
      Key: fileName,
      UploadId: uploadId,
      PartNumber: index + 1, // S3 cuenta desde 1, no 0
    });
    
    // URL válida por 15 min
    promises.push(getSignedUrl(s3Client, command, { expiresIn: 900 }));
  }

  const urls = await Promise.all(promises);
  return urls; // El frontend usará PUT en estas URLs
};

// 3. Finalizar la Carga (Ensamblaje)
export const completeUpload = async (fileName, uploadId, partsETags) => {
  // partsETags debe ser array de { ETag, PartNumber } enviado por el cliente
  const command = new CompleteMultipartUploadCommand({
    Bucket: BUCKET_NAME,
    Key: fileName,
    UploadId: uploadId,
    MultipartUpload: {
      Parts: partsETags.sort((a, b) => a.PartNumber - b.PartNumber),
    },
  });

  return await s3Client.send(command);
};

¡Cuidado con CORS! Para que el navegador del cliente pueda hacer PUT directamente a S3, debes configurar CORS en tu Bucket. Permite los métodos PUT y el origen (domain) de tu frontend, y expón el header ETag.

El costo oculto: Si una carga Multipart se interrumpe y nunca se llama a CompleteMultipartUpload o AbortMultipartUpload, los fragmentos "huérfanos" se quedan en S3 y te cobran por ese almacenamiento. Configura una Lifecycle Rule para borrar cargas incompletas después de 3 días.

Preguntas Frecuentes (FAQ)

Q. ¿Cuál es el tamaño mínimo de cada parte (Part Size)?

A. S3 exige que cada parte (excepto la última) tenga un tamaño mínimo de 5 MB. Si intentas subir partes de 1 MB, la operación CompleteMultipartUpload fallará con un error EntityTooSmall.

Q. ¿Por qué usar Multipart en lugar de una sola Presigned URL?

A. Una sola URL falla si se corta la conexión al 99%. Con Multipart, si falla la parte 50 de 100, solo reintentas esa parte pequeña. Además, permite paralelismo, saturando el ancho de banda del cliente para subir mucho más rápido.

Q. ¿Es seguro exponer Presigned URLs al cliente?

A. Sí, porque están firmadas criptográficamente, tienen una expiración corta (ej. 15 minutos) y están limitadas a una acción específica (PutObject) sobre un archivo específico. El cliente no puede usar esa URL para borrar archivos ni leer otros objetos.

Post a Comment