Cómo hacer Serverless: APIs listas para producción sobre la marcha

Esta es una publicación de blog con una opinión muy marcada. No te enfades si no estás de acuerdo. ¡Has sido advertido!

Si eres un desarrollador de cualquier tipo, es probable que hayas oído el término serverless y sepas lo que significa. Por eso, no voy a explicar qué es la arquitectura serverless, sino que voy a hablar sobre algunas herramientas que puedes usar para comenzar con serverless y crear APIs listas para producción.

Recursos

  1. NodeJS
  2. ExpressJS
  3. SequelizeJS
  4. AWS RDS
  5. AWS Lambda Functions
  6. AWS CloudFormation
  7. AWS API Gateway

El recurso más importante aquí es Serverless Framework, pero más sobre eso en la próxima sección. Esta es una publicación de blog con una opinión muy marcada.

Básicamente, la elección se reduce a tres cosas: lenguaje, base de datos y proveedor de servicios en la nube. Mi elección es NodeJS (ExpressJS) con una base de datos MySQL alojada en funciones de AWS Lambda. Pero puedes hacer cualquier combinación, como:

  • Python con PostgreSQL alojado en Azure
  • C# con MSSQL en Google Cloud Platform

Lo mejor de esta infraestructura es que puedes mezclar y combinar una serie de tecnologías, servicios y/o plataformas, con un cambio mínimo en la forma en que se crea, gestiona y despliega el código.

¿Por qué Serverless Framework?

Serverless Framework es una caja de herramientas de código abierto que permite a los desarrolladores crear, empaquetar y desplegar su base de código en cualquier plataforma en la nube, en cualquier lenguaje.

Ahora, es fácil confundirse entre la arquitectura serverless y el framework. Bueno, espero que ya sepas sobre la arquitectura serverless y lo que ofrece, es decir:

  1. Sin gestión de servidores.
  2. Escalabilidad infinita.
  3. Pago por uso.
  4. Alta disponibilidad.

¿Pero entonces qué demonios hace Serverless Framework? 🤔🤔🤔

  1. Solución única para todos: El framework se encarga de todas las configuraciones necesarias sin importar qué lenguaje o proveedor de nube uses, haciendo que tu código sea reutilizable.
  2. Enfócate en el desarrollo: El framework te permite enfocarte solo en tu lógica de negocio. Se encarga de configurar las diversas aplicaciones necesarias para crear una API completa.
  3. Código como infraestructura: El framework proporciona un solo archivo (serverless.yml) que permite a los desarrolladores definir y crear aplicaciones enteras con un solo comando.

En nuestro caso particular, Serverless gestiona los siguientes recursos:

Serverless usa AWS CloudFormation para crear una pila con todos los recursos que necesitamos. Podemos usar el CLI de Serverless para crear y desplegar estos recursos con:

serverless deploy

Paso 2

La pila consiste en funciones Lambda que ejecutan nuestro código. Las funciones Lambda son basadas en eventos, lo que significa que pueden ser activadas con una solicitud HTTP, lo cual es ideal ya que estamos creando una API.

Paso 3

La pila también crea un API Gateway, que está vinculado con nuestras funciones Lambda. API Gateway proporciona un endpoint configurado con SSL que puede ser accedido por los usuarios. También maneja el almacenamiento en caché por sí mismo.

Creo que ya entiendes la idea. Vamos a ensuciarnos las manos con un poco de código 😄😄😄

A Programar

Antes de comenzar, necesitarás una cuenta de AWS, aunque sea en el nivel gratuito.

La aplicación que estamos construyendo es solo para demostración. Tiene tres entidades: usuario, publicaciones y comentarios. Creo que todos tendrán bastante claro cual es el propósito de cada entidad y las relaciones entre ellas. Puedes encontrar el repositorio de código aquí.

  1. Lo primero que necesitas hacer es instalar el CLI de Serverless con:
    • npm install -g serverless
  2. Luego tendrás que configurar Serverless con AWS. Puedes ver los detalles aquí.
    • serverless config credentials --provider aws --key AWS_ACCESS_KEY --secret AWS_SECRET_KEY
  3. Crea un nuevo proyecto serverless
    • serverless create --template aws-nodejs --path PROJECT_NAME
      • template → se usa para definir qué proveedor y lenguaje deseas elegir. Hay varios templates pre-creados. Puedes verlos en serverless.com.
      • path → establece el nombre del proyecto/servicio.
  4. Luego, simplemente entra en tu proyecto con cd y configura npm.
    • cd PROJECT_NAME && npm init -y
  5. Instala las dependencias del proyecto.
    • npm install express body-parser sequelize mysql2 dotenv serverless-http2npm install —save-dev serverless-offline serverless-dotenv-plugin

No te hablaré de todas las dependencias, solo de las relacionadas con serverless, que podrían no ser familiares para ti:

  • serverless-http → Básicamente un contenedor para tu API (construida con Express, Hapi, Koa, etc.).
  • serverless-offline → Permite desarrollar y probar el código en tu máquina local.
  • serverless-dotenv-plugin → Nos permite usar archivos .env para definir variables de entorno.

serverless.yml

En este punto, debería haber un archivo llamado serverless.yml. Declararás todos los recursos que quieras utilizar en tu aplicación dentro de este archivo. Serverless Framework se encargará de crear y desplegar esos servicios usando AWS CloudFormation por sí solo.

Después de eliminar los comentarios del archivo, cámbialo al siguiente template:

service: serverless-node
provider:
name: aws
runtime: nodejs8.10
stage: dev
region: eu-west-1
functions:
user:
handler: functions/user.index
events:
- http:
path: /user
method: ANY
cors: true
- http:
path: /user/{proxy+}
method: ANY
cors: true
post:
handler: functions/post.index
events:
- http:
path: /post
method: ANY
cors: true
- http:
path: /post/{proxy+}
method: ANY
cors: true
comment:
handler: functions/comment.index
events:
- http:
path: /comment
method: ANY
cors: true
- http:
path: /comment/{proxy+}
method: ANY
cors: true
plugins:
- serverless-offline
- serverless-dotenv-plugin

Vamos a desglosar este archivo:

  • service → Nombre de tu proyecto.
  • provider → Contiene algunas configuraciones generales para todo el proyecto.
  • functions → Contenedores donde vivirá tu API.
  • plugins → Paquetes npm para serverless.

Voy a expandir más sobre functions:

  • api → Nombre de la función lambda.
  • handler → Punto de entrada de la función lambda. Se declara como archivo.función.
  • events → Las funciones lambda pueden ser activadas por diferentes tipos de eventos; nosotros estamos usando solicitudes HTTP.
  • path → Un recurso proxy con una variable de ruta {proxy+}.
  • method → Todos los tipos de métodos.
  • cors → habilitar CORS.

En este momento, tenemos tres funciones lambda que pueden ser activadas en cualquier ruta que coincida con los valores de “path” para todos los métodos HTTP. Esto es perfecto para nuestro escenario porque queremos que ExpressJS maneje el enrutamiento en lugar de API Gateway.

Ahora, como en cualquier proyecto de NodeJS, lo primero que necesitamos hacer es crear un servidor. Sabemos que “function_name.index” es el punto de entrada a nuestras funciones, así que aquí es donde inicializaremos el framework express y manejaremos el enrutamiento.

const express = require("express")
const bodyParser = require("body-parser")
const serverless = require("serverless-http")
const app = express()
/* Body Parser */
app.use(bodyParser.json())
app.use(bodyParser.urlencoded({ extended: false }))
/*
* ==================
* API Routes Go Here
* ==================
*/
module.exports.index = serverless(app)

En nuestro caso, tenemos tres funciones, a saber: usuario, comentario, publicación, una para cada entidad. Usaremos la misma estructura de archivo para crear tres funciones lambda, como se define en serverless.yml.

Nota: El nombre de los archivos (que contienen las funciones lambda) y los métodos exportados deben ser exactamente los mismos que los definidos en serverless.yml.

Conexiones a la Base de Datos

Un problema importante al que me enfrenté fue reutilizar la conexión a la base de datos. Inicialmente, cada vez que accedía a un endpoint, se creaba una nueva conexión a la base de datos. Tenía que cerrar manualmente las conexiones al final de cada función.

Esto significa que nuestras conexiones a la base de datos aumentarán linealmente y agregarán una sobrecarga innecesaria al crear nuevas conexiones cada vez. 😥😥😥

¿Por qué sucede esto?

Las funciones lambda son, por naturaleza, sin estado y no tienen afinidad con la infraestructura subyacente. Esto las hace infinitamente escalables, ¡lo cual es genial! pero también introduce un gran problema para nosotros. 😦😦😦

Para entender por qué sucede esto, veamos el ciclo de vida de una lambda:

  1. Descarga tu código.
  2. Inicia un nuevo contenedor.
  3. Arranca el runtime.
  4. Ejecuta tu código.
  5. Reclama el contenedor.

Ahora, cada vez que tu código se ejecuta, se provisiona un nuevo contenedor que no tiene el contexto de la ejecución anterior, por lo que no podemos reutilizar las conexiones a la base de datos.

Todavía hay esperanza 😌

AWS mantiene el contenedor en ejecución por un máximo de 15 minutos después de la ejecución del código, por lo que las solicitudes subsecuentes se ejecutan en el mismo contenedor y tu conexión a la base de datos puede ser reutilizada.

Sin embargo, los documentos de AWS dejan muy claro que no hay certeza de que esto siempre sea así.

dbCon.js

const Sequelize = require("sequelize")
const sequelize = new Sequelize(
process.env.DB_NAME,
process.env.DB_USERNAME,
process.env.DB_PASSWORD,
{
host: process.env.DB_HOST,
port: process.env.DB_PORT,
dialect: process.env.DB_DIALECT,
}
)
// Models
const User = require("./models/User")(sequelize, Sequelize)
const Post = require("./models/Post")(sequelize, Sequelize)
const Comment = require("./models/Comment")(sequelize, Sequelize)
/* The magic lies here */
let connection = {}
let Models = {
User,
Post,
Comment,
}
/**
* Creating Associations
*/
Object.keys(Models).forEach(function (modelName) {
if (Models[modelName].associate) {
Models[modelName].associate(Models)
}
})
module.exports = async () => {
if (connection.isConnected) {
console.log("use existing connection")
return Models
}
try {
await sequelize.sync()
await sequelize.authenticate()
connection.isConnected = true
console.log("use new connection")
return Models
} catch (error) {
console.log(`Connection Error: ${error}`)
}
}

Las funciones lambda congelan su estado por un máximo de 15 minutos después de la ejecución del código. En este tiempo, cualquier cosa declarada «fuera» de la función se mantiene.

El contenedor mantiene el estado del objeto connection y la conexión de sequelize porque están definidos fuera de la función lambda. Ahora, cada vez que se reutiliza el contenedor, simplemente usamos una condición — if — para verificar si existe una conexión a la base de datos o si necesitamos crear una nueva.

Probé un par de enfoques diferentes, sin obtener resultados satisfactorios. Luego encontré el enfoque anterior en la publicación de blog de Adnan Rahić.

Despliega tu código

Serverless Framework hace que el despliegue sea muy sencillo. Si tienes todo lo demás configurado, simplemente ejecuta el siguiente comando:

serverless deploy --env dev --stage qa

  • –env → Habilita el uso de variables de entorno específicas únicamente.
  • –stage → Permite desplegar solo en un entorno específico.

Remove your Code

serverless remove

El comando anterior elimina todos los recursos de AWS utilizados en tu aplicación. Agregar “–stage” eliminará solo la etapa especificada de API Gateway y los recursos relacionados.

Resultados

A continuación se muestran las métricas según CloudWatch en la consola de AWS, después de invocar cada lambda 500 veces a través de Postman.

Esta es mi aproximación para configurar una API lista para producción usando Serverless Framework. Nuevamente, puedes usar cualquier lenguaje que prefieras con cualquier base de datos. Con ligeras modificaciones, incluso puedes usar MongoDB.

Hay otras alternativas como DynamoDB y AWS Aurora Serverless, que están diseñadas con la arquitectura serverless en mente.

«Creo que este es el mejor conjunto de herramientas disponible en este momento, si alguien quiere entrar en el mundo de serverless.»

Get in touch

Contact details