aws.crafter.run

Frontera

Presigned URLs

También llamado Presigned URLs

Dar al cliente una URL firmada y con caducidad para que suba o descargue directamente de S3, sin que el archivo atraviese tu API ni tus funciones.

IntermedioVerificado el

El problema

Tu aplicación deja subir un archivo. Lo natural es hacer lo mismo que harías con un servidor: un endpoint POST /archivos, el cliente manda el archivo, tu código lo recibe y lo guarda en S3.

El archivo pasa por tu códigoClientesube 40 MBAPI Gatewaytecho: 10 MBsubirArchivotecho: 6 MBS3admite 48 TB40 MBRechazado en el primer salto · el destino admitía el archivo de sobraY si cupiera, pagarías la duración de la Lambda por copiar bytes
El archivo atraviesa dos servicios que no están para eso. API Gateway admite 10 MB y una Lambda síncrona 6 MB, así que el techo real son 6 MB — y mientras tanto pagas duración de Lambda por mover bytes de un sitio a otro.

En serverless eso choca contra tres muros, y el primero llega enseguida:

El tamaño. API Gateway admite 10 MB de payload. Una Lambda síncrona admite 6 MB. El techo real de tu endpoint es el menor de los dos, así que son 6 MB — y S3, el destino, admitía 48 TB. Has puesto dos porteros delante de un almacén que no necesitaba ninguno.

El coste. Aunque el archivo cupiera, estás pagando duración de Lambda por copiar bytes de una conexión a otra. Es el trabajo más caro por unidad de valor que puede hacer una función: nada de lógica, solo transporte.

El tiempo. Una subida lenta desde un móvil con mala cobertura mantiene viva tu invocación, consume concurrencia y corre contra el timeout de integración de 29 segundos de API Gateway.

La solución

Tu API deja de transportar y pasa a autorizar. Cuando el cliente quiere subir algo, pide permiso; tu Lambda comprueba quién es y qué puede hacer, firma una URL con caducidad, y responde. El cliente sube directamente a S3.

El archivo va directoClientesube 40 MBAPI GatewayfirmarSubidaresponde en msS3procesarArchivo1 · pide permiso2 · URL firmada3 · PUT directo a S3 · sin intermediarioseventoLa URL caduca cuando dices tú — o cuando caduca la credencial que la firmó, lo que ocurra antes
Tu código autoriza; no transporta. La Lambda genera una URL firmada y termina en milisegundos. Los bytes van del cliente a S3 por un camino donde no hay ni techo de 6 MB ni duración que pagar.

Una URL prefirmada es una URL normal de S3 con la firma de una credencial en la query string. Quien la tenga puede hacer esa operación concreta, sobre ese objeto concreto, hasta esa hora concreta, sin credenciales de AWS y sin que toques la política del bucket.

Los tres muros desaparecen a la vez: no hay techo de 6 MB porque nadie intermedia, no pagas duración porque tu función responde en milisegundos, y una subida de veinte minutos no consume nada tuyo.

Cuando la subida termina, S3 emite un evento y ahí sí invocas una función —para validar, transcodificar, indexar— pero ya con el archivo quieto en su sitio y con la clave, no con los bytes.

Cuándo usarlo

Cuándo NO usarlo

Cómo implementarlo

  1. Autoriza en tu Lambda: quién es el usuario y si puede escribir ahí. Esa es toda la lógica que queda.
  2. Genera tú la clave del objeto. Nunca aceptes el nombre de archivo del cliente tal cual (ver trampas).
  3. Firma con caducidad corta. Minutos para subir, no días.
  4. Devuelve la URL y la clave. El cliente sube; tu backend ya sabe dónde va a aparecer.
  5. Reacciona al evento de S3, no a la respuesta del cliente: el cliente puede subir y desaparecer sin decírtelo.
  6. Pon CORS en el bucket si el que sube es un navegador.
  7. Ciclo de vida que limpie las subidas incompletas y los archivos que nadie confirmó.

El código

import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';

const s3 = new S3Client({});

export const firmarSubida = async (evento: APIGatewayProxyEventV2) => {
  const usuario = autenticar(evento);
  const { tipo } = JSON.parse(evento.body!);

  if (!['image/jpeg', 'image/png', 'application/pdf'].includes(tipo)) {
    return { statusCode: 400, body: 'tipo no admitido' };
  }

  // La clave la generamos NOSOTROS. El nombre que manda el cliente
  // solo sirve para enseñarselo de vuelta, nunca para construir la ruta.
  const clave = `subidas/${usuario.id}/${crypto.randomUUID()}`;

  const url = await getSignedUrl(
    s3,
    new PutObjectCommand({
      Bucket: process.env.BUCKET,
      Key: clave,
      ContentType: tipo,
    }),
    // Corto. Y ojo: si la credencial que firma caduca antes,
    // manda la credencial. Ver trampas.
    { expiresIn: 300 },
  );

  return { statusCode: 200, body: JSON.stringify({ url, clave }) };
};
// El trabajo de verdad ocurre cuando S3 avisa, no cuando el cliente
// dice que ha terminado: el cliente puede subir y desaparecer.
export const procesarArchivo = async (evento: S3Event) => {
  for (const r of evento.Records) {
    const clave = decodeURIComponent(r.s3.object.key.replace(/\+/g, ' '));
    await validarYRegistrar(clave, r.s3.object.size);
  }
};

Te va a morder

Coste

Aquí el patrón gana por goleada, y conviene ver por qué:

Concepto Con proxy Con URL prefirmada
Duración de Lambda la que tarde la subida milisegundos
Payload de API Gateway 10 MB de techo no interviene
Techo real 6 MB 48 TB
Transferencia de entrada pasa dos veces una vez
Petición de S3 1 PUT 1 PUT

Lo que desaparece de la factura es la duración de Lambda proporcional al tamaño del archivo. Una función que tarda 8 segundos en recibir y reenviar 5 MB pasa a tardar 20 milisegundos en firmar una cadena. Con 100 000 subidas al mes, esa es la diferencia entre una línea visible y una invisible.

Subir a S3 no cuesta transferencia de entrada; lo que cuesta es sacar los archivos después. Si se descargan mucho, CloudFront delante.

Fuentes

Caducidad máxima según el tipo de credencial, comportamiento con credenciales temporales, naturaleza de bearer token, reutilización, condición s3:signatureAge, restricciones de red y errores comunes: Download and upload objects with presigned URLs. Tamaño máximo de objeto y de parte: Amazon S3 endpoints and quotas. Payload síncrono de Lambda y payload de API Gateway: Lambda quotas y Amazon API Gateway endpoints and quotas.

Patrones relacionados

Un enlace sin la relación nombrada es un "ver también". Aquí cada uno dice qué relación tiene y por qué.