Creación de su primer servicio sin servidor con las funciones de AWS Lambda | Programar Plus

Muchos desarrolladores están al menos marginalmente familiarizados con las funciones de AWS Lambda. Son razonablemente sencillos de configurar, pero el vasto panorama de AWS puede hacer que sea difícil ver el panorama general. Con tantas piezas diferentes, puede ser desalentador y frustrantemente difícil ver cómo encajan perfectamente en una aplicación web normal.

El marco Serverless es de gran ayuda aquí. Agiliza la creación, la implementación y, lo que es más importante, la integración de las funciones de Lambda en una aplicación web. Para ser claros, hace mucho, mucho más que eso, pero estas son las piezas en las que me enfocaré. Con suerte, esta publicación despertará su interés y lo alentará a ver las muchas otras cosas que admite Serverless. Si es completamente nuevo en Lambda, primero puede consultar esta introducción de AWS.

No hay manera de que pueda cubrir la instalación y configuración inicial mejor que la guía de inicio rápido, así que comience allí para comenzar a funcionar. Suponiendo que ya tenga una cuenta de AWS, podría estar en funcionamiento en 5 a 10 minutos; y si no lo hace, la guía también lo cubre.

Tu primer servicio sin servidor

Antes de llegar a cosas geniales como la carga de archivos y los cubos S3, creemos una función Lambda básica, conéctela a un punto final HTTP y llámela desde una aplicación web existente. Lambda no hará nada útil o interesante, pero esto nos dará una buena oportunidad de ver lo agradable que es trabajar con Serverless.

Primero, vamos a crear nuestro servicio. Abra cualquier aplicación web nueva o existente que pueda tener (crear-reaccionar-aplicación es una excelente manera de crear rápidamente una nueva) y encuentre un lugar para crear nuestros servicios. para mi es mi lambda carpeta. Sea cual sea el directorio que elija, cd en él desde la terminal y ejecute el siguiente comando:

sls create -t aws-nodejs --path hello-world

Eso crea un nuevo directorio llamado hello-world. Vamos a abrirlo y ver qué hay dentro.

Si mira en handler.js, debería ver una función asíncrona que devuelve un mensaje. Podríamos golpear sls deploy en nuestro terminal ahora mismo, e implementar esa función Lambda, que luego podría invocarse. Pero antes de hacer eso, hagamos que se pueda llamar a través de la web.

Al trabajar con AWS de forma manual, normalmente tendríamos que ingresar a AWS API Gateway, crear un punto de enlace, luego crear una etapa y decirle que se dirija a nuestro Lambda. Con serverless, todo lo que necesitamos es un poco de configuración.

todavía en el hello-world ¿directorio? Abra el archivo serverless.yaml que se creó allí.

El archivo de configuración en realidad viene con repetitivo para las configuraciones más comunes. Descomentemos el http entradas y agregue una ruta más sensata. Algo como esto:

functions:
  hello:
    handler: handler.hello
#   The following are a few example events you can configure
#   NOTE: Please make sure to change your handler code to work with those events
#   Check the event documentation for details
    events:
      - http:
        path: msg
        method: get

Eso es. Serverless hace todo el trabajo duro descrito anteriormente.

configuración CORS

Idealmente, queremos llamar a esto desde el código JavaScript front-end con Fetch API, pero desafortunadamente eso significa que necesitamos configurar CORS. Esta sección lo guiará a través de eso.

Debajo de la configuración anterior, agregue cors: true, como esto

functions:
  hello:
    handler: handler.hello
    events:
      - http:
        path: msg
        method: get
        cors: true

¡Esa es la sección! CORS ahora está configurado en nuestro punto final de API, lo que permite la comunicación entre orígenes.

Ajuste CORS Lambda

Si bien nuestro punto final HTTP está configurado para CORS, depende de Lambda devolver los encabezados correctos. Así es como funciona CORS. Automaticemos eso volviendo a handler.js y agregando esta función:

const CorsResponse = obj => ({
  statusCode: 200,
  headers: {
    "Access-Control-Allow-Origin": "*",
    "Access-Control-Allow-Headers": "*",
    "Access-Control-Allow-Methods": "*"
  },
  body: JSON.stringify(obj)
});

Antes de regresar de Lambda, enviaremos el valor de retorno a través de esa función. Aquí está la totalidad de handler.js con todo lo que hemos hecho hasta este momento:

'use strict';
const CorsResponse = obj => ({
  statusCode: 200,
  headers: {
    "Access-Control-Allow-Origin": "*",
    "Access-Control-Allow-Headers": "*",
    "Access-Control-Allow-Methods": "*"
  },
  body: JSON.stringify(obj)
});


module.exports.hello = async event => {
  return CorsResponse("HELLO, WORLD!");
};

Vamos a ejecutarlo. Escribe sls deploy a su terminal desde el hello-world carpeta.

Cuando eso se ejecute, habremos implementado nuestra función Lambda en un punto final HTTP al que podemos llamar a través de Fetch. ¿Pero donde esta? Podríamos abrir nuestra consola de AWS, encontrar la API de puerta de enlace que sin servidor creó para nosotros y luego encontrar la URL de invocación. Se vería algo como esto.

La consola de AWS que muestra la pestaña Configuración que incluye la configuración de caché.  Encima hay un aviso azul que contiene la URL de invocación.

Afortunadamente, hay una manera más fácil, que es escribir sls info en nuestra terminal:

Así, podemos ver que nuestra función Lambda está disponible en la siguiente ruta:

https://6xpmc3g0ch.execute-api.us-east-1.amazonaws.com/dev/ms

Woot, ¡ahora llamémoslo!

Ahora abramos una aplicación web e intentemos obtenerla. Así es como se verá nuestro Fetch:

fetch("https://6xpmc3g0ch.execute-api.us-east-1.amazonaws.com/dev/msg")
  .then(resp => resp.json())
  .then(resp => {
    console.log(resp);
  });

Deberíamos ver nuestro mensaje en la consola de desarrollo.

Salida de la consola que muestra Hello World.

Ahora que nos hemos mojado los pies, repitamos este proceso. Esta vez, sin embargo, hagamos un servicio más interesante y útil. Específicamente, hagamos la Lambda canónica de “cambiar el tamaño de una imagen”, pero en lugar de ser activada por una nueva carga de depósito S3, dejemos que el usuario cargue una imagen directamente en nuestra Lambda. Eso eliminará la necesidad de agrupar cualquier tipo de aws-sdk recursos en nuestro paquete del lado del cliente.

Construyendo un Lambda útil

¡Vale, desde el principio! Este Lambda en particular tomará una imagen, la cambiará de tamaño y luego la cargará en un depósito S3. Primero, vamos a crear un nuevo servicio. lo estoy llamando cover-art pero sin duda podría ser cualquier otra cosa.

sls create -t aws-nodejs --path cover-art

Como antes, agregaremos una ruta a nuestro extremo HTTP (que en este caso será POST, en lugar de GET, ya que estamos enviando el archivo en lugar de recibirlo) y habilitaremos CORS:

// Same as before
  events:
    - http:
      path: upload
      method: post
      cors: true

A continuación, concedamos acceso a nuestro Lambda a cualquier depósito de S3 que vayamos a utilizar para la carga. Busque en su archivo YAML: debe haber un iamRoleStatements sección que contiene código repetitivo que se ha comentado. Podemos aprovechar algo de eso descomentándolo. Aquí está la configuración que usaremos para habilitar los cubos S3 que queremos:

iamRoleStatements:
 - Effect: "Allow"
   Action:
     - "s3:*"
   Resource: ["arn:aws:s3:::your-bucket-name/*"]

Nota la /* al final. No enumeramos nombres de depósitos específicos de forma aislada, sino rutas a los recursos; en este caso, son los recursos que existen dentro your-bucket-name.

Dado que queremos cargar archivos directamente en nuestro Lambda, debemos hacer un ajuste más. Específicamente, necesitamos configurar el punto final de la API para aceptar multipart/form-data como un tipo de medio binario. Localiza el provider sección en el archivo YAML:

provider:
  name: aws
  runtime: nodejs12.x

…y modifíquelo a:

provider:
  name: aws
  runtime: nodejs12.x
  apiGateway:
    binaryMediaTypes:
      - 'multipart/form-data'

Por si acaso, demos a nuestra función un nombre inteligente. Reemplazar handler: handler.hello con handler: handler.upload, luego cambia module.exports.hello a module.exports.upload en controlador.js.

Ahora podemos escribir algo de código.

Primero, busquemos algunos ayudantes.

npm i jimp uuid lambda-multipart-parser

Espera, ¿qué es Jimp? Es la biblioteca que estoy usando para cambiar el tamaño de las imágenes cargadas. uuid será para crear nombres de archivo nuevos y únicos de los recursos de tamaño, antes de cargarlos en S3. Oh y lambda-multipart-parser? Eso es para analizar la información del archivo dentro de nuestro Lambda.

A continuación, hagamos un asistente de conveniencia para cargar S3:

const uploadToS3 = (fileName, body) => {
  const s3 = new S3({});
  const  params = { Bucket: "your-bucket-name", Key: `/${fileName}`, Body: body };


  return new Promise(res => {
    s3.upload(params, function(err, data) {
      if (err) {
        return res(CorsResponse({ error: true, message: err }));
      }
      res(CorsResponse({ 
        success: true, 
        url: `https://${params.Bucket}.s3.amazonaws.com/${params.Key}` 
      }));
    });
  });
};

Por último, conectaremos un código que lee los archivos cargados, los redimensiona con Jimp (si es necesario) y carga el resultado en S3. El resultado final está abajo.

'use strict';
const AWS = require("aws-sdk");
const { S3 } = AWS;
const path = require("path");
const Jimp = require("jimp");
const uuid = require("uuid/v4");
const awsMultiPartParser = require("lambda-multipart-parser");


const CorsResponse = obj => ({
  statusCode: 200,
  headers: {
    "Access-Control-Allow-Origin": "*",
    "Access-Control-Allow-Headers": "*",
    "Access-Control-Allow-Methods": "*"
  },
  body: JSON.stringify(obj)
});


const uploadToS3 = (fileName, body) => {
  const s3 = new S3({});
  var params = { Bucket: "your-bucket-name", Key: `/${fileName}`, Body: body };
  return new Promise(res => {
    s3.upload(params, function(err, data) {
      if (err) {
        return res(CorsResponse({ error: true, message: err }));
      }
      res(CorsResponse({ 
        success: true, 
        url: `https://${params.Bucket}.s3.amazonaws.com/${params.Key}` 
      }));
    });
  });
};


module.exports.upload = async event => {
  const formPayload = await awsMultiPartParser.parse(event);
  const MAX_WIDTH = 50;
  return new Promise(res => {
    Jimp.read(formPayload.files[0].content, function(err, image) {
      if (err || !image) {
        return res(CorsResponse({ error: true, message: err }));
      }
      const newName = `${uuid()}${path.extname(formPayload.files[0].filename)}`;
      if (image.bitmap.width > MAX_WIDTH) {
        image.resize(MAX_WIDTH, Jimp.AUTO);
        image.getBuffer(image.getMIME(), (err, body) => {
          if (err) {
            return res(CorsResponse({ error: true, message: err }));
          }
          return res(uploadToS3(newName, body));
        });
      } else {
        image.getBuffer(image.getMIME(), (err, body) => {
          if (err) {
            return res(CorsResponse({ error: true, message: err }));
          }
          return res(uploadToS3(newName, body));
        });
      }
    });
  });
};

Lamento descargar tanto código sobre usted, pero, siendo esta una publicación sobre Amazon Lambda y sin servidor, prefiero no insistir en el trabajo duro dentro de la función sin servidor. Por supuesto, el suyo puede verse completamente diferente si está utilizando una biblioteca de imágenes que no sea Jimp.

Ejecutémoslo cargando un archivo de nuestro cliente. Estoy usando la biblioteca react-dropzone, por lo que mi JSX se ve así:

<Dropzone
  onDrop={files => onDrop(files)}
  multiple={false}
>
  <div>Click or drag to upload a new cover</div>
</Dropzone>

El onDrop la función se ve así:

const onDrop = files => {
  let request = new FormData();
  request.append("fileUploaded", files[0]);


  fetch("https://yb1ihnzpy8.execute-api.us-east-1.amazonaws.com/dev/upload", {
    method: "POST",
    mode: "cors",
    body: request
    })
  .then(resp => resp.json())
  .then(res => {
    if (res.error) {
      // handle errors
    } else {
      // success - woo hoo - update state as needed
    }
  });
};

¡Y así, podemos cargar un archivo y verlo aparecer en nuestro depósito S3!

Captura de pantalla de la interfaz de AWS para depósitos que muestra un archivo cargado en un depósito procedente de la función Lambda.

Un desvío opcional: empaquetar

Hay una mejora opcional que podríamos hacer a nuestra configuración. En este momento, cuando implementamos nuestro servicio, Serverless está comprimiendo toda la carpeta de servicios y enviándola a nuestro Lambda. El contenido actualmente pesa 10 MB, ya que todos nuestros node_modules están siendo arrastrados por el viaje. Podemos usar un paquete para reducir drásticamente ese tamaño. No solo eso, sino que un paquete reducirá el tiempo de implementación, el uso de datos, el rendimiento de arranque en frío, etc. En otras palabras, es bueno tenerlo.

Afortunadamente para nosotros, hay un complemento que integra fácilmente el paquete web en el proceso de compilación sin servidor. Instalamos con:

npm i serverless-webpack --save-dev

…y agréguelo a través de nuestro archivo de configuración YAML. Podemos dejar esto al final:

// Same as before
plugins:
  - serverless-webpack

Naturalmente, necesitamos un archivo webpack.config.js, así que agreguemos eso a la mezcla:

const path = require("path");
module.exports = {
  entry: "./handler.js",
  output: {
    libraryTarget: 'commonjs2',
    path: path.join(__dirname, '.webpack'),
    filename: 'handler.js',
  },
  target: "node",
  mode: "production",
  externals: ["aws-sdk"],
  resolve: {
    mainFields: ["main"]
  }
};

Observe que estamos configurando target: node por lo tanto, los activos específicos del nodo se tratan correctamente. También tenga en cuenta que es posible que deba establecer el nombre del archivo de salida en handler.js. también estoy agregando aws-sdk a la matriz externa para que el paquete web no lo incluya en absoluto; en su lugar, dejará la llamada a const AWS = require("aws-sdk"); solo, lo que permite que sea manejado por nuestro Lambdba, en tiempo de ejecución. Esto está bien ya que Lambda ya tiene el aws-sdk disponible implícitamente, lo que significa que no es necesario que lo enviemos por cable. Finalmente, el mainFields: ["main"] es decirle a webpack que ignore cualquier ESM module campos. Esto es necesario para solucionar algunos problemas con la biblioteca Jimp.

Ahora volvamos a implementar y, con suerte, veremos el paquete web ejecutándose.

Ahora nuestro código está muy bien empaquetado en un solo archivo de 935K, que se reduce aún más a solo 337K. ¡Eso es un montón de ahorro!

Retazos

Si se pregunta cómo enviaría otros datos a Lambda, agregaría lo que desea al objeto de solicitud, del tipo FormData, desde antes. Por ejemplo:

request.append("xyz", "Hi there");

…y luego lee formPayload.xyz en la lambda. Esto puede ser útil si necesita enviar un token de seguridad u otra información de archivo.

Si se pregunta cómo podría configurar las variables env para su Lambda, es posible que ya haya adivinado que es tan simple como agregar algunos campos a su archivo serverless.yaml. Incluso admite la lectura de valores de un archivo externo (presumiblemente no comprometido con git). Esta publicación de blog de Philipp Müns lo cubre bien.

Terminando

Serverless es un marco increíble. Lo prometo, apenas hemos arañado la superficie. Con suerte, esta publicación le ha mostrado su potencial y lo ha motivado a probarlo aún más.

Si está interesado en obtener más información, le recomiendo los materiales de aprendizaje de David Wells, ingeniero de Netlify y ex miembro del equipo sin servidor, así como el Manual sin servidor de Swizec Teller.

  • Taller sin servidor: un repositorio para aprender los conceptos básicos de la tecnología sin servidor
  • Estrategias de autenticación sin servidor: un repositorio que recorre diferentes estrategias para autorizar el acceso a las funciones.
  • Netlify Functions Worksop: lecciones de Netlify sobre los conceptos básicos del uso de funciones sin servidor
  • Manual sin servidor: Introducción a las tecnologías sin servidor
(Visited 13 times, 1 visits today)