Construir API Node.js Serverless con AWS Lambda y DyanmoDB

Una de las ultimas tecnolog铆as que se ha introducido a nuestra cartera de posibilidades es la metodolog铆a serverless, en mi opini贸n una de las mas convenientes, con un muy buen balance de costo y beneficio. Utilizando este se reduce mucho el tiempo de despliegue de una aplicaci贸n y dedicarle m谩s tiempo a la programaci贸n de funciones que aporten valor.

En este articulo, te mostrarte como implementar un CRUD con el Framework de Serverless.

驴Que es Serverless?

驴Serverless? Si mi aplicaci贸n no corre sobre un servidor entonces 驴Donde corre? Es lo primero que podemos preguntarnos, pero en realidad la palabra se refiere a que esta metodolog铆a abstrae el proceso de configuraci贸n y mantenimiento de un servidor, s铆 adi贸s a configurar un servidor nginx para poder exponer tu aplicaci贸n.

Tambi茅n nos facilita la configuraciones de escaladas autom谩ticas o balanceo de carga, por lo que nos deja mucho m谩s tiempo disponible para dedicarnos al c贸digo.

Otra gran victoria, no menor, es el costo. Cuando ejecutas tu c贸digo en una maquina virtual o servidor local, este servicio tiene que tener alta disponibilidad y estar constantemente consumiendo recursos, en cambi贸, con serverless solo genera costos al momento de realizar una solicitud y la cantidad de tiempo que se ejecuta tu c贸digo. Esto implica una reducci贸n de los costos significativa.

Pros

Los beneficios son:

  • Costo - solo pagas por lo que usas.
  • Sencillez - evita la necesidad de configurar infraestructura
  • Soportado en m煤ltiples lenguajes de programaci贸n
  • Utilizado por cualquier proveedor de cloud
  • Varias alternativas para utilizar tus funciones (Endpoint de una api, colas de mensajes, ejecuciones programadas...)

Cons

Los contras que debes tener en cuenta:

  • Manejar u Organizar el c贸digo puede ser complejo, efecto de caja negra en AWS
  • El proceso de debug en local es un desafi贸
  • La aplicaci贸n no tendr谩 acceso al sistema de archivos
  • Cambiar tu proveedor de cloud puede significar cambiar el c贸digo
  • Cold starts (segundos extra que demora en ejecutarse una funci贸n por primera vez)
  • Tiempo de ejecuci贸n de una funci贸n (m谩ximo 15 minutos)

Puedes ver algunas otras limitaciones de AWS Lambda aqu铆.

Para manejar los archivos de la aplicaci贸n debes utilizar otro servicio de tu proveedor de cloud, en nuestro caso AWS, puedes ocupar un S3.

En mi ultima implementaci贸n de esta m茅todolog铆a pude comprobar que el Cold Start para funciones escribas en JavaScript no llega a ser un problema, por otro lado si tu aplicaci贸n corre sobr茅 JAVA la historia cambia, pero no queda todo all铆 como seguramente ya lo pensaste existen formas de mantener las funciones "tibias" y esperando a que llegue una petici贸n esta t茅nica se llama Lambda Warm Start.

Serverless el Framework

Si, el Framework comparte su nombr茅 con la m茅todolog铆a de desarrollo, este es una herramienta que nos permite desplegar una aplicaci贸n en Node.js f谩cilmente. Serverless es la navaja suiza Open Source en forma de CLI que nos facilita la integraci贸n con m煤ltiples proveedores de cloud.

Tambi茅n cuenta con plugins que nos pueden facilitar la v铆a del desarrollo local, m谩s adelante en este articulo utilizaras el plugin de DynamoDB Local.

Como si esto fuera poco tambi茅n cuenta con una comunidad activa y buena documentaci贸n que te pueden ayudar a la hora de cualquier duda Serverless.com.

Manos a la masa

Algunos de los problemas que puedes tener durante el desarrollo de este articulo:

  • La version del Serverless Framework o alg煤n plugin.
  • Tener las credenciales de Amazon AWS correctamente configuradas. 驴Como lo haces?

Habiendo dejado en claro eso podemos comenzar, instalando el CLI de Serverless:

npm i -g serverless serverless login

Ahora prepararemos el directorio donde construiremos la aplicaci贸n:

mkdir serverless-api && cd $_ npm init -y npm i --save aws-sdk body-parser express node-uuid serverless-http

Ahora dentro del este directorio que acabamos de crear debemos agregar el archivo serverless.yml y llenarlo con lo siguiente:

1service: lambda-rest-api 2 3custom: 4 tableName: 'todos-${self:provider.stage}' 5 6provider: 7 name: aws 8 runtime: nodejs8.10 9 stage: dev 10 region: us-east-1 11 iamRoleStatements: 12 - Effect: Allow 13 Action: 14 - dynamodb:Query 15 - dynamodb:Scan 16 - dynamodb:GetItem 17 - dynamodb:PutItem 18 - dynamodb:UpdateItem 19 - dynamodb:DeleteItem 20 Resource: 21 - { 'Fn::GetAtt': ['TodosDynamoDBTable', 'Arn'] } 22 environment: 23 TODOS_TABLE: ${self:custom.tableName} 24 25functions: 26 todo-app: 27 handler: index.handler 28 events: 29 - http: ANY / 30 - http: 'ANY {proxy+}' 31 32resources: 33 Resources: 34 TodosDynamoDBTable: 35 Type: 'AWS::DynamoDB::Table' 36 Properties: 37 AttributeDefinitions: 38 - AttributeName: todoId 39 AttributeType: S 40 KeySchema: 41 - AttributeName: todoId 42 KeyType: HASH 43 ProvisionedThroughput: 44 ReadCapacityUnits: 1 45 WriteCapacityUnits: 1 46 TableName: ${self:custom.tableName}

Hay algunas cosas que resaltar en este archivo: el campo service ser谩 el nombre de nuestra aplicaci贸n, dentro de custom vamos a recibir un par谩metro que ser谩 el nombre de la tabla en DynamoDB, que luego se guarda en una variable llamada TODOS_TABLE. Luego podremos acceder al valor de esta variable por el process.env en el c贸digo.

El resto de este archivo configura los permisos, campos, esquemas... con los que vamos a trabajar en DynamoDB, ahora vamos a agregar algunos plugins que son necesarios para el desarrollo local o tambi茅n llamado offline.

npm i --save serverless-dynamodb-local@0.2.30 serverless-offline

Ahora debes incluir en el archivo serverless.yml las siguientes lineas, justo debajo del campo service:

plugins: - serverless-dynamodb-local - serverless-offline

El orden es importante, primero debe de estar el serverless-dynamodb-local y luego el serverless-offline.

Hasta aqu铆 ya tenemos configurado todo el entorno que necesita nuestra aplicaci贸n para ejecutarse, ahora podemos comenzar a trabajar en el c贸digo.

Creas el archivo index.js y lo llenas con lo siguiente:

1const serverless = require('serverless-http') 2const bodyParser = require('body-parser') 3const express = require('express') 4const app = express() 5const AWS = require('aws-sdk') 6const uuid = require('node-uuid') 7 8const { TODOS_TABLE, IS_OFFLINE } = process.env 9 10const dynamoDb = 11 IS_OFFLINE === 'true' 12 ? new AWS.DynamoDB.DocumentClient({ 13 region: 'localhost', 14 endpoint: 'http://localhost:8000', 15 }) 16 : new AWS.DynamoDB.DocumentClient() 17 18app.use(bodyParser.json({ strict: false })) 19 20app.get('/todos', (req, res) => { 21 const params = { 22 TableName: TODOS_TABLE, 23 } 24 25 dynamoDb.scan(params, (error, result) => { 26 if (error) { 27 res.status(400).json({ error: 'Error retrieving Todos' }) 28 } 29 30 const { Items: todos } = result 31 32 res.json({ todos }) 33 }) 34}) 35 36module.exports.handler = serverless(app)

Si has tenido experiencia con alguna otra aplicaci贸n construida con Express el contenido de este archivo te ser谩 bastante familiar, as铆 le estamos dando a nuestra aplicaci贸n un 煤nico endpoint para leer todas las notas guardados.

Ahora a ejecutar nuestra api:

sls offline start --migrate

Luego de que termine de iniciar la aplicaci贸n podr谩s acceder a la ruta http://localhost:3000/todos y deber铆as de tener como respuesta un objeto con un array de "notas" vaci贸: {"todos":[]}.


En caso de ver un error similar a este:

Error: spawn java ENOENT at exports._errnoException (util.js:1022:11) at Process.ChildProcess._handle.onexit (internal/child_process.js:193:32) at onErrorNT (internal/child_process.js:359:16) at _combinedTickCallback (internal/process/next_tick.js:74:11) at process._tickDomainCallback (internal/process/next_tick.js:122:9)

Debes ejecutar el siguiente comando:

sls dynamodb install

Para agregar algunas notas necesitamos agregar un endpoint que lo permita, agrega este bloque de c贸digo a tu archivo index.js justo encima del module.exports:

1app.post('/todos', (req, res) => { 2 const { title, done = false } = req.body 3 4 const todoId = uuid.v4() 5 6 const params = { 7 TableName: TODOS_TABLE, 8 Item: { 9 todoId, 10 title, 11 done, 12 }, 13 } 14 15 dynamoDb.put(params, error => { 16 if (error) { 17 console.log('Error creating Todo: ', error) 18 res.status(400).json({ error: 'Could not create Todo' }) 19 } 20 21 res.json({ todoId, title, done }) 22 }) 23})

Luego de detener y volver a ejecutar la api podremos probarla con CURL:

curl -H "Content-Type: application/json" -X POST http://localhost:3000/todos -d '{"title": "Finish bug tickets"}'

Esto debe de devolvernos el objeto creado en la base de datos, por ejemplo:

{"todoId":"5c30e169-26e3-44de-9564-d23a403ddf1b","title":"Finish bug tickets","done":false}

Si la respuesta es como esta y no tienes ning煤n error, acabas de crear una nota en tu aplicaci贸n, si vas al endpoint podr谩s ver las notas creadas http://localhost:3000/todos.

Continuando con la API, agregamos un endpoint que nos permita ver solo una nota especificada por el ID.

1app.get('/todos/:todoId', (req, res) => { 2 const { todoId } = req.params 3 4 const params = { 5 TableName: TODOS_TABLE, 6 Key: { 7 todoId, 8 }, 9 } 10 11 dynamoDb.get(params, (error, result) => { 12 if (error) { 13 res.status(400).json({ error: 'Error retrieving Todo' }) 14 } 15 16 if (result.Item) { 17 const { todoId, title, done } = result.Item 18 res.json({ todoId, title, done }) 19 } else { 20 res.status(404).json({ error: `Todo with id: ${todoId} not found` }) 21 } 22 }) 23})

Una ves agregado este endpoint puedes probarlo con el id de alguna nota creado con anterioridad (http://localhost:3000/todos/5c30e169-26e3-44de-9564-d23a403ddf1b), la respuesta debe de ser solo la nota a la que corresponde el ID

{"todoId":"5c30e169-26e3-44de-9564-d23a403ddf1b","title":"Finish bug tickets","done":false}

Agregamos el m茅todo PUT de nuestra API:

1app.put('/todos', (req, res) => { 2 const { todoId, title, done } = req.body 3 4 var params = { 5 TableName: TODOS_TABLE, 6 Key: { todoId }, 7 UpdateExpression: 'set #a = :title, #b = :done', 8 ExpressionAttributeNames: { '#a': 'title', '#b': 'done' }, 9 ExpressionAttributeValues: { ':title': title, ':done': done }, 10 } 11 12 dynamoDb.update(params, error => { 13 if (error) { 14 console.log(`Error updating Todo with id ${todoId}: `, error) 15 res.status(400).json({ error: 'Could not update Todo' }) 16 } 17 18 res.json({ todoId, title, done }) 19 }) 20})

Lo puedes probar cambiando el campo done de una nota de false a true para indicar que esta lista

curl -H "Content-Type: application/json" -X PUT http://localhost:3000/todos -d '{"todoId": "5c30e169-26e3-44de-9564-d23a403ddf1b", "title": "Finish bug tickets", "done": done}'

Y por ultimo el m茅todo DELETE para nuestra api de notas

1app.delete('/todos/:todoId', (req, res) => { 2 const { todoId } = req.params 3 4 const params = { 5 TableName: TODOS_TABLE, 6 Key: { 7 todoId, 8 }, 9 } 10 11 dynamoDb.delete(params, error => { 12 if (error) { 13 console.log(`Error updating Todo with id ${todoId}`, error) 14 res.status(400).json({ error: 'Could not delete Todo' }) 15 } 16 17 res.json({ success: true }) 18 }) 19})

Para probarlo

curl -H "Content-Type: application/json" -X DELETE http://localhost:3000/todos/5c30e169-26e3-44de-9564-d23a403ddf1b

Debe de eliminar la nota a la cual corresponde el ID seleccionado.

Con esto tendremos la API REST lista y funcionando en local, ahora es donde el Framework Serverless hace su magia.

Solo ejecuta sls deploy se tomar谩 unos minutos y al final tendr谩s desplegada tu API en AWS y en caso de que todo sea un 茅xito podr谩s ver un mensaje como el siguiente:

Serverless: WARNING: Missing "tenant" and "app" properties in serverless.yml. Without these properties, you can not publish the service to the Serverless Platform. Serverless: Packaging service... Serverless: Excluding development dependencies... Serverless: Creating Stack... Serverless: Checking Stack create progress... ..... Serverless: Stack create finished... Serverless: Uploading CloudFormation file to S3... Serverless: Uploading artifacts... Serverless: Uploading service .zip file to S3 (26.16 MB)... Serverless: Validating template... Serverless: Updating Stack... Serverless: Checking Stack update progress... .................................... Serverless: Stack update finished... Service Information service: lambda-rest-api stage: dev region: us-east-1 stack: lambda-rest-api-dev api keys: None endpoints: ANY - https://gqrbje0go5.execute-api.us-east-1.amazonaws.com/dev ANY - https://gqrbje0go5.execute-api.us-east-1.amazonaws.com/dev/{proxy+} functions: todo-app: lambda-rest-api-dev-todo-app layers: None

Ahora puedes probar los mismos comandos CURD con este nuevo endpoint que gener谩 AWS y validar que tu API este funcionando correctamente.

Extras

  • Puedes usar este comando para ver algunos logs serverless logs -f todo-app -t
  • Para borrar todos los servicios que levanto esta aplicaci贸n al desplegarse en AWS serverless remove. Cuidado no solicita confirmaci贸n.
  • Agregar a la secci贸n de scripts en el archivo package.json la linea "develop": "sls offline start --migrate".

Pueden ver el c贸digo relacionado a este articulo como gu铆a en GitHub.

Creditos

Este articulo fue publicado originalmente por Matthew Brown en keyholesoftware.com.

Me tom茅 la libertad de traducirlo y compartirlo, agregando algunos comentarios seg煤n mi experiencia con la finalidad de compartir conocimientos.

Actualizado 24/10/2019 a las 00:54