La reutilización de código es algo básico cuando uno esta programando. Es de las primeras cosas que te enseñan sin importar con que lenguaje estes empezando y te lo repiten una y otra vez (ya sea indirecta o directamente) a través de diferentes conceptos, como la alta cohesión dentro de GRASP, el Single-Responsibility dentro de SOLID y porsupuesto el mismisimo DRY (Don’t Repeat Yourself).
Es normal pensar en implementar esto mismo en Lambda cuando empiezas a utilizar este servicio. Sin embargo no es tan claro a primera vista como lograrlo, ya que necesitas de otra funcionalidad de Lambda: las Lambda Layers (Capas).
Nota: No esta de más decir que si no has manejado funciones Lambdas aún, es mejor que primero te enfoques en aprender lo básico: Como crear una función de Lambda en AWS en 5 minutos
Caso de uso
Veamos un ejemplo para una librería de NodeJS: Axios. Esta librería se utiliza para hacer peticiones REST. Si ya las has utilizado, sabrás que se puede agregar a tu proyecto usando npm. Para este ejemplo crearemos un par de funciones que llamaremos a través de un API. Dichas funciones a su vez actuaran como un proxy para llamara a The Rick and Morty API.
Para empezar crea un par de funciones Lambda, cada una con la paquetería de axios instalada:
Nota: para instalar la paquetería necesitaras crear un folder en tu computadora y correr los siguientes comandos: npm init
y npm install --save axios
getCharacter
Directorio
- getCharacter
- node_modules
- getCharacter.js
- package-lock.json
- package.json
Código
const axios = require('axios')
exports.getCharacter = async (event, context, callback) => {
const characterId = event.queryStringParameters['id']
let statusCode = -1
let body = null
try {
const requestResult = await axios.get(`https://rickandmortyapi.com/api/character/${characterId}/`)
body = JSON.stringify(requestResult.data)
statusCode = 200
} catch (error) {
body = JSON.stringify({message: error})
statusCode = 500
}
return {
"headers": {
"Content-Type": "*/*",
"Access-Control-Allow-Origin": "*"
},
"isBase64Encoded": false,
"statusCode": statusCode,
"body": body
}
}
getEpisode
Directorio
- getEpisode
- node_modules
- getEpisode.js
- package-lock.json
- package.json
Código
const axios = require('axios')
exports.getEpisode = async (event, context, callback) => {
const episodeId = event.queryStringParameters['id']
let statusCode = -1
let body = null
try {
const requestResult = await axios.get(`https://rickandmortyapi.com/api/episode/${episodeId}/`)
body = JSON.stringify(requestResult.data)
statusCode = 200
} catch (error) {
body = JSON.stringify({message: error})
statusCode = 500
}
return {
"headers": {
"Content-Type": "*/*",
"Access-Control-Allow-Origin": "*"
},
"isBase64Encoded": false,
"statusCode": statusCode,
"body": body
}
}
Como podrás ver getCharacter
y getEpisode
cada una devuelven un personaje y un episodio de la serie de Rick and Morty respectivamente.
De igual manera crea un API simple en API Gateway que apunte a estas lambdas y publícalo.
El API no es obligatorio pero sirve para que pruebes un caso cercano a un uso real. Puedes omitirlo o si te interesa pero no sabes como puedes revisar: Como crear una REST API con Lambda en AWS en 6 pasos
Como puedes ver en ambas funciones se esta leyendo el parámetro id
del API y este valor a su vez se utiliza como parámetro en la petición hecha al API de Rick and Morty.
El código evidentemente es muy simple y podemos agregarle diferentes funcionalidades. Un ejemplo sería ponerle validaciones. Actualmente si mandamos un número como valor para id
, todo funciona correctamente para ambas funciones:
¿Pero qué pasa si usas una cadena de texto como valor?
Nuestro API devuelve un error 500. Es evidente que algo salió mal al intentar pasar un texto. Como regla siempre es mejor ser lo más explícito posible al momento de regresar errores. Los únicos escenarios que podrían ser una excepción son aquellos en los que haya información que haya que resguardar por cuestiones de seguridad. Sin embargo este no es el caso así que es mejor que regreses explícitamente que el parametro id
espera un número.
Para eso simplemente actualiza el código de las lambdas con lo siguiente:
getCharacter
const axios = require('axios')
exports.getCharacter = async (event, context, callback) => {
let statusCode = -1
let body = null
const characterId = event.queryStringParameters['id']
const valid = isIdValid(characterId)
if (!valid){
return {
"headers": {
"Content-Type": "*/*",
"Access-Control-Allow-Origin": "*"
},
"isBase64Encoded": false,
"statusCode": 400,
"body": JSON.stringify({message: "id must be a number"})
}
}
try {
const requestResult = await axios.get(`https://rickandmortyapi.com/api/character/${characterId}/`)
body = JSON.stringify(requestResult.data)
statusCode = 200
} catch (error) {
body = JSON.stringify({message: error})
statusCode = 500
}
return {
"headers": {
"Content-Type": "*/*",
"Access-Control-Allow-Origin": "*"
},
"isBase64Encoded": false,
"statusCode": statusCode,
"body": body
}
}
const isIdValid = (id) => {
const idCopy = id+''
let result = idCopy.match(/^\d+$/)
return result != null
}
getEpisode
const axios = require('axios')
exports.getEpisode= async (event, context, callback) => {
let statusCode = -1
let body = null
const episodeId = event.queryStringParameters['id']
const valid = isIdValid(episodeId)
if (!valid){
return {
"headers": {
"Content-Type": "*/*",
"Access-Control-Allow-Origin": "*"
},
"isBase64Encoded": false,
"statusCode": 400,
"body": JSON.stringify({message: "id must be a number"})
}
}
try {
const requestResult = await axios.get(`https://rickandmortyapi.com/api/episode/${episodeId}/`)
body = JSON.stringify(requestResult.data)
statusCode = 200
} catch (error) {
body = JSON.stringify({message: error})
statusCode = 500
}
return {
"headers": {
"Content-Type": "*/*",
"Access-Control-Allow-Origin": "*"
},
"isBase64Encoded": false,
"statusCode": statusCode,
"body": body
}
}
const isIdValid = (id) => {
const idCopy = id+''
let result = idCopy.match(/^\d+$/)
return result != null
}
Ahora al llamar a cualquier de nuestros 2 servicios con una cadena de texto en el id
el API nos devuelve un mensaje de error mucho más claro.
Técnicamente el código funciona bien de esta manera pero es evidente que tiene ciertos problemas:
- Hacer actualizaciones al código es más tardado. Si hay que cambiar la forma en la que se valid el parámetro de
id
se tendrá que actualizar dicho cambio en cada función que tengas. - Verificar consistencia es más difícil. Al tener varias versiones de la misma funcionalidad el código se encuentra mas expuesto a errores humanos, ya que todo depende de que uno se acuerde de cambiar las cosas en todos los lugares correctos.
- El tamaño del código se incrementa innecesariamente.
Solución: Lambda Layers
Por lo pronto hay 3 funcionalidades principales que podemos compartir entre funciones, ya que las probabilidades de que difieran entre las mismas son nulas o están muy cerca de serlo:
- La paquetería de axios que instalamos en
node_modules
- La función de validación:
isIdValid
- El formato de respuesta:
headers
yisBase64Encoded
Una Lamba Layer puede usar los mismos lenguajes de programación que una función Lambda, por lo que puedes utilizar el siguiente código para crearla.
const isIdValid = (id) => {
const idCopy = id+''
let result = idCopy.match(/^\d+$/)
return result != null
}
const getResponseObjectTemplate = () => {
return {
"headers": {
"Content-Type": "*/*",
"Access-Control-Allow-Origin": "*"
},
"isBase64Encoded": false,
"statusCode": null,
"body": null
}
}
module.exports.isIdValid = isIdValid
module.exports.getResponseObjectTemplate = getResponseObjectTemplate
Para ello simplemente ve a la opción de Layers(Capas) y selecciona Crear una Capa.
A continuación te aparece un formulario que puedes llenar de la siguiente manera:
- Nombre: proxyLambdaLayer
- Descripción: Layer para compartir utilidades para Lambda del API
- Cargar un archivo Zip: seleccionado
- Arquitecturas compatibles: vacío
- Tiempos de ejecución compatibles: Node.js 14.X
Al momento de subir el archivo es importante que subas un zip con un folder llamado nodejs
. Esta carpeta debe contener todos los archivos que sean necesarios para tu código. En este caso dado que la layer también va a contener la librería de axios, también se tener que subir el folder de node_modules
junto con el package.json
.
Dentro de tu zip, la estructura de folders debe quedar así:
- nodejs
- node_modules
- package-lock.json
- package.json
- proxyLambdasLayer.js
Nota: El requerimiento del folder padre (en este caso nodejs) depende del lenguaje de programación que utilices para crear la Lambda Layer.
Una vez hayas llenado el formulario AWS creará la Layer
Para utilizarla simplemente hay que entrar en las opciones de configuración de las lambdas e ir a la sección correspondiente:
Selecciona Añadir una capa. Una vez dentro debes seleccionar la layer que deseas junto con la versión de la misma.
Nota: Cada vez que actualices una layer AWS creará una versión nueva de la misma. De esta manera puedes tener versionadas las actualizaciones que hagas.
Una vez la hayas agregado puedes verificar que se haya añadido en la sección inferior de la función Lambda.
Nota: Una función Lambda puede tener n Layers.
Haz estos pasos con ambas funciones y procede a actualizar el código. Si bien las funciones ya estan referenciando a la Layer correcta falta hacer que el código en verdad haga uso de ella.
getCharacter
Directorio
- getCharacter
- getCharacter.js
Código
const proxyLambdasLayer = require('/opt/nodejs/proxyLambdasLayer')
const axios = require('axios')
exports.getCharacter= async (event, context, callback) => {
let statusCode = -1
let body = null
const characterId = event.queryStringParameters['id']
const responseTemplate = proxyLambdasLayer.getResponseObjectTemplate()
const valid = proxyLambdasLayer.isIdValid(characterId)
if (!valid){
return {
...responseTemplate,
"statusCode": 400,
"body": JSON.stringify({message: "id must be a number"})
}
}
try {
const requestResult = await axios.get(`https://rickandmortyapi.com/api/character/${characterId}/`)
body = JSON.stringify(requestResult.data)
statusCode = 200
} catch (error) {
body = JSON.stringify({message: error})
statusCode = 500
}
return {
...responseTemplate,
"statusCode": statusCode,
"body": body
}
}
getEpisode
Directorio
- getEpisode
- getEpisode.js
Código
const proxyLambdasLayer = require('/opt/nodejs/proxyLambdasLayer')
const axios = require('axios')
exports.getEpisode= async (event, context, callback) => {
let statusCode = -1
let body = null
const episodeId = event.queryStringParameters['id']
const responseTemplate = proxyLambdasLayer.getResponseObjectTemplate()
const valid = proxyLambdasLayer.isIdValid(episodeId)
if (!valid){
return {
...responseTemplate,
"statusCode": 400,
"body": JSON.stringify({message: "id must be a number"})
}
}
try {
const requestResult = await axios.get(`https://rickandmortyapi.com/api/episode/${episodeId}/`)
body = JSON.stringify(requestResult.data)
statusCode = 200
} catch (error) {
body = JSON.stringify({message: error})
statusCode = 500
}
return {
...responseTemplate,
"statusCode": statusCode,
"body": body
}
}
Probablemente solo con ver el código intrínsecamente ya entiendes que está pasando. Pero no esta de más corroborarlo.
const proxyLambdasLayer = require('/opt/nodejs/proxyLambdasLayer')
Esta línea es la que hace referencia a nuestra layer. Con el objeto proxyLambdasLayer
ahora tenemos acceso a todas las funciones que hayamos exportado de nuestro archivo proxyLambdasLayer.js
en la layer.
AWS por default coloca nuestro código debajo del folder de opt. Tu no tienes control sobre este comportamiento así que simplemente debes recordar usar ese path.
const axios = require('axios')
Esta línea en realidad no cambia en nada. Sin embargo es importante tener presente que ahora la paquetería de node_modules no esta siendo accedida en nuestra misma función, sino en la layer. AWS es lo suficientemente listo para dar acceso a nuestra función a la misma.
Con esto has terminado de implementar tu primera Lambda Layer. Como puedes ver no es complicado y ganas bastantes beneficios al no tener que mantener código duplicado en tus funciones,