Creando un API-Rest con AWS Lambda y API-Gateway

El último proyecto que he realizado me ha permitido sumergirme hasta las profundidades de una de las tecnologías que están más en boga hoy en día: AWS Lambda.

En este post voy a contar cómo ha sido mi experiencia montando un API-Rest totalmente con arquitectura ServerLess.

El proveedor cloud que hemos escogido para este proyecto es Amazon, que nos ofrece los siguientes productos:

  • API-Gateway: servicio totalmente administrado que facilita a los desarrolladores la publicación, el mantenimiento, la monitorización y la protección de API a cualquier escala con la que hemos construido un API-Rest. Lo vemos en detalle más adelante.
  • Lambda: permite ejecutar código sin aprovisionar ni administrar servidores. Es la capa que actúa como controladora de los endpoints definidos en el API-Gateway.
  • S3: es el responsable de, entre otras cosas, almacenar el código de las funciones Lambda, que utilizamos para desplegar éstas gracias a nuestro proceso de CI/CD.
  • CloudWatch: es la pieza que nos proporciona los logs de aplicación.

También utilizamos un par de elementos externos a AWS: una instancia de MongoDB desplegada en cloud y otra de SQL Server (éste es legacy y por ello no la incluyo en el stack de AWS).  

Vamos a ver un poco más en detalle todas las piezas que componen nuestro puzle:

Qué es ServerLess

ServerLess es un tipo de arquitectura que, como su propio nombre indica, no precisa de un servidor. ¿Qué quiere decir esto?, ¿dónde se va a ejecutar nuestro código si no hay un servidor?

Tradicionalmente, cuando programamos, tenemos que ser muy conscientes de dónde se va a desplegar nuestro código, qué memoria tiene, cómo escala… Las arquitecturas ServerLess tratan de solventar esos problemas por nosotros, para que nos podamos centrar en lo realmente importante: el código.

Las arquitecturas FaaS (Functions as a Server) nos proporcionan una infraestructura autogestionada, que nos permite ejecutar directamente nuestro código reaccionando a eventos.

Estos eventos pueden ser tanto eventos web como eventos provenientes de otras partes de la infraestructura.

Analicemos en este ejemplo cómo sería la subida de un fichero al servidor. Nuestra función se ejecutaría de la siguiente forma:

  • Se despliega un contenedor gestionado por la propia infraestructura de AWS Lambda, totalmente transparente para nosotros.
  • El escalado es automático en función del número de peticiones que tengamos, sin que nos tengamos que preocupar por ello.
  • No es necesario dimensionado de máquinas, ni de nada que tenga que ver con la maquina física.

Los gigantes de la computación en la nube están apostando muy fuerte por las arquitecturas ServerLess. Amazon ha incorporado muchas novedades en su último AWS re:invent2017.

Google también está apostando fuerte con Google Cloud Functions y Azure con Azure Functions.

Qué es AWS Lambda

Como ya hemos visto en el punto anterior, las lambdas son funciones que se caracterizan por desempeñar un objetivo muy concreto, como reaccionar ante la subida de un fichero o un evento web.

En el caso de la plataforma de Amazon son multilenguaje, se pueden escribir en Java, Phyton, Node.js y más recientemente han incorporado GO, del que dicen es el lenguaje con la mejor performance.

Vemos un ejemplo de función lamba en Node.js:

	exports.handler = function(event, context) {
		// Do your stuff
   	}
}

Como podemos ver, es muy simple. Solo necesitamos un handler, que es el que ejecutará nuestro código. Esta función recibe dos parámetros event y context y, opcionalmente, callback.

El event es el evento que desencadena (trigger) la ejecución de una función lambda. El context nos da información acerca de la lambda: tiempo de ejecución pendiente, alias de la función lambda invocada…

Veamos algunos ejemplos de triggers. Una función lambda puede ser invocada entre otros triggers por uno de estos:

  • Por una subida de un fichero a S3.
  • Una publicación de un topic en Amazon SNS.
  • Inserción en dynamoDB.
  • Llamada a un endpoint de un API, mediante APIGateway.

Una lambda es un contenedor que se levanta en la infraestructura de AWS y nos permite ejecutar el código que hemos definido en la función.

Cada petición concurrente que llega, si hablamos de un API-Rest, levanta uno de estos contenedores. Por defecto AWS ha determinado que el número de contenedores concurrentes es 1000 pudiendo configurarse para aumentar este número.

Los contenedores se reutilizan, es decir, cuando la ejecución de una lambda finaliza no se destruye su contenedor inmediatamente, sino que se queda levantado a la espera de nuevas peticiones, pero no tenemos control sobre el ciclo de vida de este contenedor.

Cada función lambda se puede configurar para que disponga de más memoria. A más memoria más coste y mejor será el contenedor sobre el que se despliegue.

En el caso de Node, el desplegable de una lambda es un zip que incluye el handler y los ficheros .js necesarios que se requieran desde el handler y la carpeta node_modules con las dependencias necesarias para ejecutar nuestra lambda.

Qué es el API Gateway

Es la capa de API-management de AWS. Para que nos entendamos, el API-Gateway es donde creamos nuestro API, donde definimos los endpoints, métodos http y las integraciones que tienen todos estos.

En nuestro caso, todas los endpoints integran con AWS Lambda.

Además en el API-Gateway gestionamos los entornos (stages), definimos variables por entorno, nos permite documentar nuestros endpoints, mockearlos por ejemplo cuando la integración no está lista, securizarlos

Ejemplo de API, definido con API-Gateway.

Gestión y organización del código

Alguna de las preguntas que nos vienen a la cabeza a la hora de afrontar un proyecto basado en microservicios son:

  • ¿Cómo organizo el código?
  • ¿Cómo gestiono las dependencias entre paquetes?
  • ¿Repositorio único o repositorio por cada uno de los microservicios?
  • ¿Gestión de la configuración?

Antes de nada, aclarar que decidimos usar NodeJS para construir nuestras funciones lambda y, gracias a eso, cada función lambda es un paquete de NodeJS, que puede ser publicado en un registry privado.

Después de bastantes días de investigación y diversas pruebas, optamos por el patrón mono-repo. Este patrón, como su nombre indica, es un único repositorio que alberga todos los paquetes.

Para ello usamos una herramienta llamada Lerna, que nos facilita la vida a la hora de ejecutar acciones como:

  • Build de todos nuestros paquetes con su comando lerna bootstrap.
  • Clean con lerna clean.
  • Lanzar todos los test con Lerna run test.
  • Ejecutar un script personalizado en cada uno de nuestros paquetes, con lerna run <script>.
  • Y quizá lo más importante: nos informa qué paquetes se han modificado desde el último commit. De esta forma podemos construir sólo los microservicios que hemos actualizado, dejando al resto tal y como están gracias a la instrucción lerna updated.

Al final nos queda en nuestro repositorio principal un directorio denominado packages, del que cuelgan todos los módulos npm que componen la aplicación, cada uno con su versión.

Gestión de la configuración

Comencemos por explicar qué información manejamos en la configuración de nuestro proyecto. En nuestro caso, en el módulo config manejamos la información que nos conecta con las bases de datos, configuraciones de seguridad, etc… que varían según el entorno en el que nos movamos.

Más adelante hablaremos de los entornos y cómo hemos resuelto este escollo en ServerLess, ya que como hemos dicho anteriormente aquí no hay maquinas.

Para solucionar esta problemática típica de arquitecturas basadas en microservicios, optamos en primera instancia por una función lambda, que sería invocada cada vez que se llamase a una de las otras funciones.

Los beneficios fueron muchos, entre otros poder cambiar la configuración en caliente, sin afectar al resto de funciones, ya que cada vez que una función fuese llamada, invocará a la lambda de configuración para obtener la información que nos permite, entre otras cosas, conectar con la base de datos correspondiente.

Este primer aproach lo descartamos, ya que nos dimos cuenta que muy pocas veces íbamos a cambiar esa configuración. Por contra, estamos aumentando el coste, ya que por cada llamada a un endpoint íbamos a realizar dos.

Finalmente optamos por una dependencia interna, creamos un módulo npm que sería inyectado como una dependencia interna en nuestros paquetes. Así solventamos el problema del coste, pero sin embargo, un cambio en este módulo nos obligó a re-desplegar todos los módulos que dependían de él.

Gestión de las dependencias

Lerna nos facilita la gestión de las dependencias, todo son módulos npm que se construyen con npm install. Además, nos facilita la instalación de todas ellas con lerna bootstrap.

Para nuestra integración continua, hemos definido diferentes niveles de modelos, con un sistema de pesos que instala las dependencias siguiendo un orden establecido.

De esta forma tenemos el modulo config; paquetes denominados services, que se utilizan para extraer código común y que pueda ser reutilizable entre lambdas; y, por último, muy recientemente hemos introducido el concepto de controller.

Un controller es una lambda en la que manejamos un CRUD sobre un resource. Imaginad que tenemos una entidad libros, como sabéis en un API Rest esta entidad se explota con los siguientes endpoints:

  • [GET] /books obtiene la lista de libros.
  • [POST] /books da de alta un nuevo libro.
  • [PUT] /books/:bookid hace un update total del libro.
  • [PATCH] /books/:bookid hace una actualización parcial del libro.
  • [DELETE] /books/:bookid borra un libro.

Inicialmente creamos una lambda por cada endpoint y, con el paso del tiempo, nos dimos cuenta que tanta granularidad iba haciendo que nuestro proyecto fuera creciendo en número de lambdas.

Esto hizo, entre otras cosas, que se incrementasen los tiempos de despliegue y fue por lo que decidimos introducir el concepto de controller, que agrupa en una única lambda todas las operaciones sobre una entidad en concreto, pasando de 5 funciones lambda a una única.

Como todo tiene su lado bueno y su lado malo. El lado bueno es evidente: menos paquetes. El malo es que perdemos en independencia entre métodos, escalado de un método en concreto, que tenga un uso más exhaustivo; y también perdemos la independencia de los logs.

En conclusión, con Lerna gestionamos todas las dependencias, definimos módulos que no son lambdas, sino librerías npm locales, que nos permiten reutilizar código, y hacer menos código repetido.

Gestión de Entornos

Hemos definido diferentes entornos para nuestro proyecto dev, pre y pro, pero la integración continua queda fuera del ámbito de este artículo. Lo que quiero destacar es que se pueden generar diferentes entornos partiendo del mismo código, os explico cómo a continuación.

En las lambdas tenemos alias y versiones. Una versión es cada actualización que hacemos de nuestro código. En lambda son secuenciales 1,2,3… y no admite versionados custom. Por otro lado, alias son punteros a versiones.

Vamos a poner un ejemplo. Tenemos una función lambda “myLambda” y el siguiente escenario:

Lo que nos indica este escenario es que dev apunta a la versión 10. que es un .zip con la versión más reciente.

En pre hemos estado probando la versión 8 y, tras validarla, la hemos subido a pro. Tanto en pre como en pro apuntamos a la misma versión, pero ¿cómo hacemos para que accedan a diferentes bases de datos, por ejemplo?

Para solucionar este problema, lo que tenemos que hacer es parametrizar el API-Gateway. Es decir, en nuestro API-Gateway lo que hacemos es una template de nuestros endpoint, de forma que en lugar de apuntar a una lambda en concreto apuntamos a una plantilla que será de la forma myLambda:${myStageVariable}.

Aquí vemos, los diferentes Stages (entornos) que hemos creado y como se definen variables para ellos.

myStageVariable se define a nivel de cada uno de los diferentes stages. En el caso de la foto de arriba, nuestra variable se define como lambdaAlias y su valor es acceptance.

Cuando invoquemos a un endpoint en nuestro API, nuestra integración sustituirá la stageVariable definida en la template del endpoint por el valor de la función en el stage correspondiente.

En la imagen podemos ver, en el apartado Lambda Function, la función lambda que invocamos, junto la variable que define su alias.

En consecuencia, invocamos a un alias de función en lugar de a la propia función.

Concluyendo…

Hemos creado un API-Rest definiendo nuestros endpoints con AWS API-Gateway y hemos conseguido generar diferentes entornos en nuestro API.

Además, invocamos a las funciones lambda cuyo alias se corresponde con el entorno, permitiendo tener diferentes configuraciones y diferentes versiones de código por entorno.

Estos son los pasos básicos que necesitas para crear un API-Rest con AWS Lambda y API-Gateway. ¿Te atreves a probar tú?

Photo of Javier Álvarez

Javier Alvarez, con más de 10 años de experiencia en el mundo del desarrollo de Software, es un apasionado de su profesión y de las nuevas tecnologías, tras unos años en el exilio, Javier ha vuelto a España para unirse a Paradigma, tratando de aportar esa experiencia internacional y un punto de vista diferente.

Ver toda la actividad de Javier Álvarez