Object Lambdas: Modificación dinámica de datos

Object Lambdas

Modificación dinámica de datos de AWS S3 mediante AWS Object Lambdas

Podemos definir cargas útiles estáticas para Object Lambdas, pero ¿cómo parametrizamos su entrada en el punto de invocación?


Con la introducción de Object Lambdas a principios de 2021, fue posible transformar datos en S3 cuando se solicitó. Las lambdas invocadas en las solicitudes S3 GET a través de Object Lambda Access Points recuperan el objeto, lo modifican y escriben los datos modificados en la respuesta S3. Este proceso es configurable a través de payloads, para permitirnos tener múltiples escenarios.

Considere que queremos almacenar un PNG de 200×200 píxeles, completamente negro y recuperarlo en uno de los tres colores primarios. Podemos crear tres puntos de acceso, con una carga útil codificada de los colores que se envía a nuestro Lambda. Llamamos a uno de los tres puntos de acceso, que a su vez invoca la lambda, convierte la imagen y modifica la respuesta GET con esos datos de imagen.


¿Qué pasa si queremos admitir más que los colores primarios?

¿Qué pasa si queremos admitir un color determinado? Es posible enviar parámetros adjuntos a nuestras URL de solicitud GET de S3 cuando llamamos a nuestro objeto, de modo que podamos solicitar dinámicamente cualquier color hexadecimal, independientemente de las cargas útiles predefinidas.

Considere lo siguiente, tenemos una función que toma un color definido como parámetro y devuelve nuestro PNG con ese color.

const AWS = require('aws-sdk');
// These are the various access points, which each have a specified payload
// that will call the lambda we have attached with said payload.
const AWS_OBJECT_LAMBDA_ACCESS_POINT = {
  red: 'arn:of:red',
  blue: 'arn:of:blue',
  yellow: 'arn:of:yellow',
};

function getObject(colour: 'red' | 'blue' | 'yellow', key: string) {
  const s3 = new AWS.S3();
  // The bucket when calling an object lambda access point is the access point ARN
  // The key, is the unique ID of the object in S3
  const params = {
      Bucket: AWS_OBJECT_LAMBDA_ACCESS_POINT[colour], 
      Key: key,
  };
  // We call GET on the access point, it calls the lambda,
  // which in turn writes the modified data to the response.
  return s3.getObject(params);
}

No es práctico (y casi imposible) crear los innumerables puntos de acceso con cargas útiles que admitan todos los colores posibles que deseemos. Lo solucionamos pasando un código hexadecimal que nuestra lambda consume dinámicamente a través del objeto de evento event.userRequest.url.

Todo lo que agreguemos al final de nuestra S3_OBJECT_KEY después de una / en el ejemplo anterior será accesible en ese campo en el objeto de evento.


Una solución completa

Necesitamos dividir userRequestUrl para poder recuperar la clave del objeto, así como el valor hexadecimal que hemos pasado como, esencialmente, un parámetro. El resto de la función del objeto lambda permanece igual. Recuperamos el original a través del punto de acceso de soporte que está asociado con el depósito, modificamos el cuerpo del objeto a través de nuestro método changeColour y escribimos ese cuerpo en nuestra respuesta S3 GET a través de writeGetResponseOutput.

Teniendo esto en cuenta, a continuación se muestran las funciones lambda y getObject modificadas que hacen que el ejemplo anterior sea capaz de devolver un PNG de cualquier color hexadecimal dado.

const AWS = require('aws-sdk');
// The access point now does not change
const AWS_OBJECT_LAMBDA_ACCESS_POINT = 'arn:of:access:point';

function getObject(hex: string, key: string) {
  const s3 = new AWS.S3();
  // The key now becomes our S3 unique ID, appended with the hex code
  const params: S3.Types.GetObjectRequest = {
      Bucket: AWS_OBJECT_LAMBDA_ACCESS_POINT, 
      Key: `${key}/${hex}`,
  };
  return s3.getObject(params);
}
// Scaffold object lambda event interface,
// it is not comprehensive
export interface ObjectLambdaEvent {
  getObjectContext: {
    inputS3Url: string;
    outputRoute: string;
    outputToken: string;
  }
  userRequest: {
    url: string;
  }
  configuration: {
    supportingAccessPointArn: string;
  }
}

// This is the function to change the colour,
// it takes and returns the buffer of the PNG
async function changeColour(png: Buffer, hex: string) {
  return replaceColor({
    image: png,
    colors: {
      type: 'hex',
      targetColor: '#000000',
      replaceColor: `#${hex}`,
    },
  // The replaceColour function returns a imp object...
  }).then((object) => {
    let buffer;
    // ...we take the object and get a buffer from it
    object.getBuffer(Jimp.MIME_PNG, (err, data) => {
      buffer = data;
    });
    return buffer;
  });
}

exports.handler = async (event: ObjectLambdaEvent) => {
  const s3 = new S3();

  // This is where we are writing the modified data to
  const outputRoute = event.getObjectContext?.outputRoute;
  const outputToken = event.getObjectContext?.outputToken;
  // This is the request url in which we get our appended hex code
  const userRequestUrl = event.userRequest?.url;
  // This is the access point we call the original, unmodified object from
  const supportingAccessPointArn = event.configuration?.supportingAccessPointArn;

  try {
    const unescapedUrl: string = querystring.unescape(userRequestUrl);
    const urlObj = new URL(unescapedUrl);

    // Split the pathname to get the key and hex we have appended
    const split = urlObj.pathname.split('/');
    const key = split[1];
    const hex = split[2];

    // We will use the following params...
    const params: S3.Types.GetObjectRequest = {
      Bucket: supportingAccessPointArn,
      Key: s3Key,
    };

    // ...to call the original, unmodified object
    const object: AWS.S3.GetObjectOutput = await new Promise((resolve, reject) => {
      return s3.getObject(params, (err, result) => {
        if (err) reject(err);
        else resolve(result);
      });
    });

    const returnBody = changeColour(object.Body, hex);

    // We write the modified data to the S3 response
    await s3.writeGetObjectResponse({
      RequestRoute: outputRoute,
      RequestToken: outputToken,
      Body: returnBody,
    }).promise();

    return { statusCode: 200 };
  } catch (e) {
    return { statusCode: 400, error: e };
  }
};

Por supuesto, esta solución puede ampliarse, de modo que podamos proporcionar múltiples parámetros, separados más allá del primero / pasado la clave, con lo que queramos usar como delimitador.

key/hexValue#shape#... funcionará, al igual que key /  key/hexValue@shape@...; solo recuerde agregar la funcionalidad para dividir y manejar estos parámetros adicionales.

Una cosa clave que debe asegurarse para que esto funcione es que la ID única es el primer componente del parámetro Key que pasamos a la solicitud getObject. Si no es así, es probable que vea un error de Access Denied, debido a que la solicitud buscará el primer componente de dicha clave en el depósito S3 y no encontrará nada.

Finalmente, nuestra función replaceColour depende de dos módulos, replace-color para el reemplazo de color y jimp para recuperar el búfer del objeto devuelto por el primero.

Recent Post