Ya son muy conocidos los beneficios de las arquitecturas basadas en microservicios serverless. De hecho, algunos de los principales beneficios de la tecnología serverless son la ausencia de administración de servidores, el escalado flexible o la alta disponibilidad. En este mismo blog, otros compañeros nos cuentan las distintas maneras de trabajar con arquitecturas serverless, la arquitectura Lambda o profundizan en el API Gateway de AWS. Hoy veremos cómo configurar y crear localmente los microservicios para una aplicación que desplegaremos en el servicio AWS Lambda.

Paso a paso

Serverless Framework es un proyecto de código abierto para construir e implementar rápidamente aplicaciones sin servidor utilizando servicios en la nube. En este caso vamos a crear una API CRUD básica con nodejs como runtime, DynamoDB como base de datos, declarando las funciones de AWS Lambda y sus activadores a través de una simple sintaxis en YAML, y utilizando un archivo de configuración para la nube.

AWS Lambda es un servicio de Amazon Web Services que permite desplegar y ejecutar código en la nube y que puede ser activada por una función de AWS o por un servicio externo, y DynamoDB es una base de datos NoSQL no administrada.

Lambda es un servicio orientado a eventos, y tiene algunas limitaciones: hasta 15 minutos en la petición y sólo permite un máximo de 1000 ejecuciones concurrentes. Fuera de eso puede ejecutar cualquier lógica de negocio, llamar a otra API, etc.

Te contamos el paso a paso:

  1. Creamos una cuenta AWS

NOTA: Pide una tarjeta de crédito, aunque solo cobra si pasamos el nivel de la capa gratuita, explicado en su página, pero que básicamente calcula que no sobrepase:

  1. Configuramos de forma básica AWS
  1. Instalamos AWS Comand Line Interface (CLI)

Es una herramienta para configurar y administrar los productos de AWS, en la página hay abundante documentación, pero para este ejemplo vamos a necesitar:

aws – version

aws configure

Access Key ID:
Secret Access Key:
Region name:
Default output format:

Los 2 primeros son los datos que guardamos al crear el usuario. La región la configuraremos con nuestro código para poder cambiarlo programáticamente si lo necesitamos, y lo dejamos en blanco. Y, el último, tampoco necesitamos completarlo ahora.

  1. Instalamos Serverless Framework

En Getting Started indican varias formas de hacerlo, por ejemplo con:

npm install -g serverless

En el directorio donde crearemos el proyecto ejecutamos el comando:

serverless

Permite seleccionar plantillas para el tipo de proyecto, en este caso crear una API:

Descarga una plantilla para el proyecto, y pregunta si queremos registrarnos en Serverless Dashboard y si queremos desplegar ahora.

El proyecto ya está creado.

  1. Abrimos con el editor de código y probamos la plantilla automática

Serverless Framework ha creado una plantilla que muestra cómo hacer una HTTP API básica con Node.js.

Incluye una función de ejemplo ( handler.js ) que recibe un evento que trae información (cliente, si envía un dato, etc.), y siempre retorna un objeto.

  "use strict";

  module.exports.hello = async (event) => {
    return {
      statusCode: 200,
      body: JSON.stringify(
        {
          message: "Go Serverless v2.0! Your function executed
  successfully!",
          input: event,
        },
        null,
        2
      ),
    };
  };}

El framework utiliza serverless.yml donde se detallan las definiciones del servicio, funciones, configuración, recursos, permisos, etc.

  service: prueba-serverless-crud
  frameworkVersion: '2 || 3'

  provider:
    name: aws
    runtime: nodejs12.x
    lambdaHashingVersion: '20201221'
    region: 'eu-west-3'

  functions:
    hello:
      handler: handler.hello
      events:
        - httpApi:
            path: /
            method: get

1. service: referencia del proyecto, suele ser el nombre con el que se desplegará. Si vamos a utilizar varios ervicios es conveniente que cada uno tenga su propio fichero serverless.yml.

2. frameworkVersion: podemos establecer una versión exacta "=" o un rango de versiones ">=".

3. provider: indica el nombre del proveedor y su configuración: versión del lenguaje, memoria (opcional), etc. La configuración y permisos establecidos son heredados por todas las funciones.

4. functions: listado con cada una de las implementaciones (similar a un enrutador de Express).

4.1 Identificador: Nombre de la función.

4.2 handler: clase y método.

4.3 events: tipo de eventos y especificaciones para el tipo de evento.

serverless deploy –verbose



  Service Information
  service: prueba-serverless-crud
  stage: dev
  region: eu-west-3
  stack: prueba-serverless-crud-dev
  resources: 11
  api keys:
         None
  endpoints:
         GET - https://xxxxxxxxxx.execute-api.eu-west-3.amazonaws.com/
  functions:
         hello: prueba-serverless-crud-dev-hello
  layers:
         None
  1. Conectamos a DynamoDB

En AWS-Resources tenemos la documentación completa para definir una tabla de DynamoDB con sus propiedades y atributos.

Para entender el funcionamiento, crearemos una tabla para guardar películas, con los datos del nombre de la película, su director y año de estreno, que en próximos pasos utilizaremos para trabajar con nuestra API.

Configuramos un recurso MoviesTable ( 1 ), de tipo DynamoDB ( 2 ), con el nombre de la tabla MoviesTable ( 3 ), forma de facturación: PAY_PER_REQUEST ( 4 ), definimos los campos, en este caso un id de tipo String ( 5 ), y definimos el campo único ( primary key ) para identificar cada película ( 6 ):

      resources:
        Resources:
 ( 1 )      MoviesTable:
 ( 2 )        Type: AWS::DynamoDB::Table
              Properties:
 ( 3 )          TableName: MoviesTable
 ( 4 )          BillingMode: PAY_PER_REQUEST
                AttributeDefinitions:
 ( 5 )            - AttributeName: id
                  AttributeType: S
 ( 6 )          KeySchema:
               - AttributeName: id
                 KeyType: HASH

serverless deploy –verbose

La terminal muestra todo lo que ha creado de forma automática (ApiGateway, Lambda, IAM, etc), entre otras cosas, la tabla DynamoDB:

 CloudFormation - CREATE_IN_PROGRESS - AWS::DynamoDB::Table - MoviesTable
 CloudFormation - CREATE_IN_PROGRESS - AWS::DynamoDB::Table - MoviesTable
 CloudFormation - UPDATE_IN_PROGRESS - AWS::Lambda::Function - HelloLambdaFunction
 CloudFormation - UPDATE_COMPLETE - AWS::Lambda::Function - HelloLambdaFunction
 CloudFormation - CREATE_COMPLETE - AWS::DynamoDB::Table – MoviesTable

arn:aws:dynamodb:eu-west-3:xxxxxxxxxxxx:table/MoviesTable

     provider:
         name: aws
         runtime: nodejs12.x
         lambdaHashingVersion: '20201221'
         region: 'eu-west-3'
( 1 )    iamRoleStatements:
( 2 )      - Effect: Allow
             Action:
( 3 )            - dynamodb:*
             Resource:
                 - arn:aws:dynamodb:eu-west-3:xxxxxxxxxxxx:table/MoviesTable

  1. Instalación de aws-sdk

La guía del desarrollador de SDK de AWS detalla la instalación y dependencias necesarias para trabajar con Node.js:

npm install aws-sdk

Para para crear el id de cada nueva entrada de la tabla:

npm install uuid

  "dependencies": {
      "aws-sdk": "^2.1074.0",
      "uuid": "^8.3.2"
  }
  1. Creamos una API CRUD básica
const { v4 } = require('uuid');
const AWS = require('aws-sdk');

const addMovie = async(event) => {

    // Conectar a la Base de datos a través del ClientId
    // y el Client Secret ya configurado:
    const dynamodb = new AWS.DynamoDB.DocumentClient();

    // Recoger los datos provenientes del body de la petición
    const { title, director, year } = JSON.parse(event.body);

    const createAt = new Date();
    const id = v4();

    // Crear el objeto para guardar
    const newMovie = {
        id,
        title,
        director,
        year,
        createAt
    }

    // put permite guardar un dato
    // ( no es como el PUT en REST )
    await dynamodb.put({
        TableName: 'MoviesTable',
        Item: newMovie
    }).promise()

    return {
        statusCode: 200,
        body: JSON.stringify(newMovie)
    }
}

module.exports = {
    addMovie
};
  createMovie:
    handler: src/addMovie.addMovie
    events:
      - httpApi:
          path: /movies
          method: post
  endpoints:
    GET - https://xxxxxxxxxx.execute-api.eu-west-3.amazonaws.com/
    POST - https://xxxxxxxxxx.execute-api.eu-west-3.amazonaws.com/movies

De la misma forma, podemos obtener el listado de películas, una película por su id, actualizar una película y borrar una película.

getMovies.js:

  const AWS = require('aws-sdk');
  const getMovies = async(event) =>  {
      try {
          const dynamodb = new AWS.DynamoDB.DocumentClient();
          // scan es como hacer un fetch de toda la tabla
          const result = await dynamodb.scan({
              TableName: 'MoviesTable'
          }).promise();
          const movies = result.Items;

          return {
              status: 200,
              body: {
                  movies
              }
          };
      } catch(error) {
          console.log(error);
      }   
  }
  module.exports = {
      getMovies
  }

getMovie.js:

const AWS = require('aws-sdk');
  const getMovie = async(event) =>  {
    try {
        const dynamodb = new AWS.DynamoDB.DocumentClient();
        const { id } = event.pathParameters;
        // método get para obtener un elemento:
        const result = await dynamodb.get({
            TableName: 'MoviesTable',
            Key: {
                id
            }
        }).promise();

        const movie = result.Item;
        return {
            status: 200,
            body: {
                movie
            }
        };
    } catch(error) {
        console.log(error);
    }    
  }
  module.exports = {
    getMovie
  }

updateMovie.js:

  const AWS = require('aws-sdk');

  const updateMovie = async(event) =>  {

    try {
        const dynamodb = new AWS.DynamoDB.DocumentClient();

        // Extraer el id desde los parámetros del path:
        const { id } = event.pathParameters;

        // Extraer valores recibidos a través del evento:
        const { title, director } = JSON.parse(event.body);

        const result = await dynamodb.update({
            TableName: 'MoviesTable',

            // Setear cada uno de los valores
            UpdateExpression: 'set title = :title, director = :director',
            ExpressionAttributeValues: {                
                ':title': title,
                ':director': director
            },

            // Indicar el id que debe modificar:
            Key: { id },

            // Retornar el valor actual:
            ReturnValues: 'ALL_NEW'

        }).promise();

        return {
            status: 200,
            body: JSON.stringify({
                message: 'Movie updated successfully'
            })
        };
    } catch(error) {
        console.log(error);
    }    
  }

  module.exports = {
    updateMovie,
  }

deleteMovie.js:

  const AWS = require('aws-sdk');
  const deleteMovie = async(event) =>  {
    try {
        const dynamodb = new AWS.DynamoDB.DocumentClient();
        const { id } = event.pathParameters;       
        const result = await dynamodb.delete({
            TableName: 'MoviesTable',          
            Key: { id }
        }).promise();
        return {
            status: 200,
            body: JSON.stringify({
                message: 'Movie deleted successfully'
            })
        };
    } catch(error) {
        console.log(error);
    }    
  }
  module.exports = {
    deleteMovie,
  }
  service: prueba-serverless-crud
  frameworkVersion: '2 || 3'

  provider:
    name: aws
    runtime: nodejs12.x
    lambdaHashingVersion: '20201221'
    region: 'eu-west-3'
    iamRoleStatements:
      - Effect: Allow
        Action:
          - dynamodb:*
        Resource:
          - arn:aws:dynamodb:eu-west-3:xxxxx:table/MoviesTable

  functions:
    hello:
      handler: handler.hello
      events:
        - httpApi:
            path: /
            method: get
    createMovie:
      handler: src/addMovie.addMovie
      events:
        - httpApi:
            path: /movies
            method: post
    getMovies:
      handler: src/getMovies.getMovies
      events:
        - httpApi:
            path: /movies
            method: get
    getMovie:
      handler: src/getMovie.getMovie
      events:
        - httpApi:
            path: /movies/{id}
            method: get
    updateMovie:
      handler: src/updateMovie.updateMovie
      events:
        - httpApi:
            path: /movies/{id}
            method: put
    deleteMovie:
      handler: src/deleteMovie.deleteMovie
      events:
        - httpApi:
            path: /movies/{id}
            method: delete

  resources:
    Resources:
      MoviesTable:
        Type: AWS::DynamoDB::Table
        Properties:
          TableName: MoviesTable
          BillingMode: PAY_PER_REQUEST
          AttributeDefinitions:
            - AttributeName: id
              AttributeType: S
          KeySchema:
            - AttributeName: id
              KeyType: HASH
  1. CloudWatch: Monitorizar recursos y aplicaciones

Si tenemos algún error, AWS tiene el servicio CloudWatch:

En la opción Grupos de registros, muestra el listado de todas las funciones creadas, haciendo click en cada una de ellas podemos ver las peticiones realizadas con sus logs.

Con este ejemplo podemos comprobar que Serverless Framework es fácil de implementar para crear una aplicación rápidamente.

También existe la posibilidad de modificar o ampliar la configuración mediante gran cantidad de plugins no utilizados en este ejemplo, aunque existen otras herramientas como AWS CDK, o AWS SAM, que iremos viendo en próximos posts con el objetivo de poder elegir la solución adecuada para cada proyecto. Esperamos que este tutorial os haya sido de utilidad. Y si tienes alguna duda, ¡déjanos un comentario!

Cuéntanos qué te parece.

Los comentarios serán moderados. Serán visibles si aportan un argumento constructivo. Si no estás de acuerdo con algún punto, por favor, muestra tus opiniones de manera educada.

Suscríbete

Estamos comprometidos.

Tecnología, personas e impacto positivo.