Si has estado al día con mi contenido, recordarás que había escrito un artículo titulado, Utilizar AWS Lambda y API Gateway con Node.js y Couchbase NoSQL. En este artículo habíamos explorado el uso de los servicios Serverless de Amazon para crear funciones Lambda que interactúan con Couchbase, nuestra base de datos NoSQL.
Sin embargo, Lambda no es la única tecnología sin servidor, también conocida como funciones como servicio (FaaS). Por ejemplo, Apache OpenWhisk por ejemplo. Con OpenWhisk puede crear funciones de forma similar a como lo haría con Lambda, pero desplegarlas en un conjunto más diverso de ubicaciones, siendo la más popular IBM Bluemix.
Vamos a ver cómo crear funciones sin servidor utilizando OpenWhisk para comunicarnos con nuestro Servidor Couchbase base de datos.
De cara al futuro, hay algunas cosas a tener en cuenta. Necesitarás alojar Couchbase Server en algún lugar accesible por el mundo exterior. Esto significa que tu ordenador local no funcionará. Vas a necesitar Docker para que podamos compilar nuestras dependencias para trabajar con OpenWhisk. Por último, vas a necesitar una cuenta Bluemix, al menos para este ejemplo.
Instalación de las herramientas CLI de Bluemix para OpenWhisk
Como he mencionado anteriormente, OpenWhisk es un proyecto de la Fundación Apache. Sin embargo, por comodidad vamos a utilizarlo en IBM Bluemix.
Crear una cuenta para el Nube de IBM si aún no lo ha hecho.
En lugar de utilizar una herramienta framework como Serverless, vamos a utilizar la CLI de Bluemix. Descarga el CLI de IBM Cloud Functions para que podamos interactuar con OpenWhisk en IBM.
Antes de empezar a trabajar con su cuenta de IBM Cloud, debe iniciar sesión a través de la CLI. Desde la línea de comandos, ejecute lo siguiente:
1 |
bx inicio de sesión -a api.ng.bluemix.red -o tu_email@ejemplo.com -s dev |
Al descargar la CLI, se le dará el comando exacto, pero debe ser similar a lo que he presentado anteriormente.
Ahora podemos empezar a crear nuestro proyecto.
Comprender la estructura del proyecto y el proceso de creación de paquetes OpenWhisk
Si nunca has trabajado con FaaS antes, las cosas se hacen un poco diferente a la construcción de una aplicación independiente, difícilmente escalable.
Por ejemplo, cada punto final en nuestro proyecto FaaS será una función separada. Combinadas, estas funciones crean lo que se denomina un paquete. Estas funciones se escalan según sea necesario para satisfacer la demanda cambiante de su aplicación.
Dicho esto, crea lo siguiente:
1 2 3 4 5 6 7 8 9 10 11 12 |
crear --- paquete.json --- crear.js recuperar --- paquete.json --- recuperar.js actualización --- paquete.json --- actualización.js borrar --- paquete.json --- borrar.js |
El proyecto debe tener un directorio para cada función que deseemos crear. Cada función tendrá su propio directorio paquete.json archivo. Cada paquete.json se puede crear ejecutando lo siguiente dentro de cada uno de los directorios:
1 |
npm init -y |
Dentro de cada uno de los paquete.json también tendrá que definir qué archivo es el código de su función. Por ejemplo, abra crear/paquete.json y añada o cambie la siguiente línea:
1 |
"principal": "create.js", |
Al fijar el principal
estamos indicando qué archivo JavaScript contiene nuestra función.
Cuando empecemos a desplegar nuestras funciones, lo haremos para que formen parte del mismo paquete.
Diseñar una función para crear datos
Comencemos el desarrollo con la creación de datos en nuestra base de datos. Navega a la base de datos crear y ejecute el siguiente comando desde la línea de comandos:
1 |
npm instale couchbase uuid joi --guardar |
El comando anterior instalará nuestras dependencias de funciones. Usaremos el SDK de Couchbase para Node.js, la librería UUID para generar claves únicas, y la librería Joi para validar la entrada.
Volveremos a revisar la instalación de la dependencia, pero al menos nos mantendrá en marcha por ahora.
Ahora abra el proyecto crear/create.js e incluya lo siguiente:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
const Couchbase = requiere("couchbase"); const UUID = requiere("uuid"); const Joi = requiere("joi"); var cubo = null; función principal(parámetros) { si(cubo == null) { var grupo = nuevo Couchbase.Grupo("couchbase://" + parámetros.host); grupo.autentifique(parámetros.nombre de usuario, parámetros.contraseña); cubo = grupo.openBucket(parámetros.bucketName); } var esquema = Joi.objeto().llaves({ nombre: Joi.cadena().obligatorio(), apellido: Joi.cadena().obligatorio(), tipo: Joi.cadena().prohibido().por defecto("persona") }); var datos = parámetros; var respuesta = {}; devolver nuevo Promesa((resolver, rechace) => { var validación = Joi.valide(datos, esquema, { stripUnknown: verdadero }); si(validación.error) { respuesta = { statusCode: 500, cuerpo: JSON.stringify(validación.error.detalles) }; rechace(respuesta); } var id = UUID.v4(); cubo.insertar(id, validación.valor, (error, resultado) => { si(error) { respuesta = { cuerpo: JSON.stringify({ código: error.código, mensaje: error.mensaje }) }; rechace(respuesta); } datos.id = id; respuesta = { cuerpo: JSON.stringify(validación.valor) }; resolver(respuesta); }); }); } exportaciones.principal = principal; |
El código anterior es un poco demasiado, así que tenemos que averiguar lo que está pasando. Vamos a empezar con la variable que existe fuera de nuestra función:
1 |
var cubo = null; |
No es la mejor idea establecer una nueva conexión cada vez que se llama a la función. En su lugar, podemos mantener una instancia global del Couchbase Bucket abierto y mientras exista, usarlo. Sólo ten en cuenta que no siempre existirá porque OpenWhisk destruirá las funciones después de un tiempo de inactividad.
1 2 3 4 5 |
si(cubo == null) { var grupo = nuevo Couchbase.Grupo("couchbase://" + parámetros.host); grupo.autentifique(parámetros.nombre de usuario, parámetros.contraseña); cubo = grupo.openBucket(parámetros.bucketName); } |
Dentro de nuestra función comprobamos si el Bucket ya está abierto. Si el Bucket no está abierto, establecemos una conexión utilizando los parámetros pasados a la función. Cuando llegue el momento, definiremos parámetros por defecto que contengan esta información de conexión.
Como estamos creando datos, necesitamos validar que la entrada es correcta.
1 2 3 4 5 |
var esquema = Joi.objeto().llaves({ nombre: Joi.cadena().obligatorio(), apellido: Joi.cadena().obligatorio(), tipo: Joi.cadena().prohibido().por defecto("persona") }); |
Esperamos un nombre
y apellido
esté presente. También esperamos un tipo
no esté presente. Podemos validar esto con lo siguiente:
1 2 3 4 5 6 7 8 |
var validación = Joi.valide(datos, esquema, { stripUnknown: verdadero }); si(validación.error) { respuesta = { statusCode: 500, cuerpo: JSON.stringify(validación.error.detalles) }; rechace(respuesta); } |
En stripUnknown
eliminará los datos no definidos en el esquema. Necesitamos eliminar datos porque nuestra información de entrada y de conexión existirá en la misma carga útil. No queremos que la información de conexión se guarde en nuestros documentos. Si hay un error de validación, será devuelto. Si no hubo error de validación, podemos proceder a insertar los datos.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
var id = UUID.v4(); cubo.insertar(id, validación.valor, (error, resultado) => { si(error) { respuesta = { cuerpo: JSON.stringify({ código: error.código, mensaje: error.mensaje }) }; rechace(respuesta); } datos.id = id; respuesta = { cuerpo: JSON.stringify(validación.valor) }; resolver(respuesta); }); |
Podemos generar una nueva clave única y guardar los datos validados como un documento. Los propios datos se devolverán como respuesta.
Las demás funciones seguirán esta misma estrategia, más o menos.
Diseño de una Función para Recuperar Datos con N1QL
Ahora que tenemos los datos, vamos a intentar recuperarlos de la base de datos con una invocación a una función. Navega a tu recuperar y ejecute lo siguiente desde la línea de comandos:
1 |
npm instale couchbase --guardar |
Como no vamos a crear datos, no necesitamos generar valores únicos ni validar ningún dato de usuario. Por esta razón, sólo necesitamos el SDK de Couchbase para esta función.
Abra el archivo recuperar/retrieve.js e incluya lo siguiente:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
const Couchbase = requiere("couchbase"); var cubo = null; función principal(parámetros) { si(cubo == null) { var grupo = nuevo Couchbase.Grupo("couchbase://" + parámetros.host); grupo.autentifique(parámetros.nombre de usuario, parámetros.contraseña); cubo = grupo.openBucket(parámetros.bucketName); } var respuesta = {}; var declaración = "SELECT META().id, `" + cubo.Nombre + "`.* FROM `" + cubo.Nombre + "WHERE type = 'person'"; var consulta = Couchbase.N1qlQuery.fromString(declaración); devolver nuevo Promesa((resolver, rechace) => { cubo.consulta(consulta, (error, resultado) => { si(error) { respuesta = { cuerpo: JSON.stringify({ código: error.código, mensaje: error.mensaje }) }; rechace(respuesta); } respuesta = { cuerpo: JSON.stringify(resultado) }; resolver(respuesta); }); }); } exportaciones.principal = principal; |
Vamos a saltar sobre lo que ya hemos visto en la función anterior y saltar a lo que es nuevo. Una vez que estamos conectados a un Bucket abierto, podemos crear una consulta N1QL.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
var declaración = "SELECT META().id, `" + cubo.Nombre + "`.* FROM `" + cubo.Nombre + "WHERE type = 'person'"; var consulta = Couchbase.N1qlQuery.fromString(declaración); devolver nuevo Promesa((resolver, rechace) => { cubo.consulta(consulta, (error, resultado) => { si(error) { respuesta = { cuerpo: JSON.stringify({ código: error.código, mensaje: error.mensaje }) }; rechace(respuesta); } respuesta = { cuerpo: JSON.stringify(resultado) }; resolver(respuesta); }); }); |
Esta consulta N1QL es de tipo SQL y nos permitirá recuperar todos los documentos que coincidan con ciertos criterios. Si hay algún error, lo devuelve como respuesta, en caso contrario devuelve el conjunto de resultados.
Como no estamos validando nada, esta función para recuperar datos era mucho más sencilla.
Diseño de una función para actualizar datos con mutaciones de subdocumentos
Supongamos ahora que queremos actualizar documentos dentro de la base de datos. En lugar de recuperar documentos, hacer cambios y luego guardar esos cambios, vamos a enviar los cambios directamente a la base de datos y dejar que la base de datos resuelva las cosas.
Navegue hasta la sección actualización y ejecute lo siguiente desde la línea de comandos:
1 |
npm instale couchbase joi --guardar |
Como estamos aceptando datos de usuarios, queremos validar esos datos. No estamos creando datos, por lo que no necesitamos generar claves únicas.
Abra el archivo actualizar/update.js e incluya lo siguiente:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 |
const Couchbase = requiere("couchbase"); const Joi = requiere("joi"); var cubo = null; función principal(parámetros) { si(cubo == null) { var grupo = nuevo Couchbase.Grupo("couchbase://" + parámetros.host); grupo.autentifique(parámetros.nombre de usuario, parámetros.contraseña); cubo = grupo.openBucket(parámetros.bucketName); } var esquema = Joi.objeto().llaves({ id: Joi.cadena().obligatorio(), nombre: Joi.cadena().opcional(), apellido: Joi.cadena().opcional() }); var datos = parámetros; var respuesta = {}; devolver nuevo Promesa((resolver, rechace) => { var validación = Joi.valide(datos, esquema, { stripUnknown: verdadero }); si(validación.error) { respuesta = { statusCode: 500, cuerpo: JSON.stringify(validación.error.detalles) }; rechace(respuesta); } var constructor = cubo.mutateIn(validación.valor.id); si(validación.valor.nombre) { constructor.sustituir("nombre", validación.valor.nombre); } si(validación.valor.apellido) { constructor.sustituir("apellido", validación.valor.apellido); } constructor.ejecutar((error, resultado) => { si(error) { respuesta = { statusCode: 500, cuerpo: JSON.stringify({ código: error.código, mensaje: error.mensaje }) }; rechace(respuesta); } respuesta = { statusCode: 200, cuerpo: JSON.stringify(validación.valor) }; resolver(respuesta); }); }); } exportaciones.principal = principal; |
¿Le resulta familiar el código anterior? Debería, porque seguimos la misma estrategia.
Nuestra lógica de validación es ligeramente diferente en este ejemplo:
1 2 3 4 5 |
var esquema = Joi.objeto().llaves({ id: Joi.cadena().obligatorio(), nombre: Joi.cadena().opcional(), apellido: Joi.cadena().opcional() }); |
Queremos editar un documento en particular por lo que se requiere una clave. No sabemos lo que el usuario quiere actualizar así que establecemos las propiedades como opcionales.
Para hacer actualizaciones vamos a hacer operaciones de subdocumento en nuestros documentos. Para ello, podemos utilizar un constructor de mutación.
1 2 3 4 5 6 7 |
var constructor = cubo.mutateIn(validación.valor.id); si(validación.valor.nombre) { constructor.sustituir("nombre", validación.valor.nombre); } si(validación.valor.apellido) { constructor.sustituir("apellido", validación.valor.apellido); } |
Proporcionamos un documento para alterar y las rutas en las que existen las propiedades. Las rutas pueden ser mucho más complejas que los ejemplos utilizados aquí.
Con el conjunto de mutaciones definido, podemos ejecutarlas contra la base de datos.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
constructor.ejecutar((error, resultado) => { si(error) { respuesta = { statusCode: 500, cuerpo: JSON.stringify({ código: error.código, mensaje: error.mensaje }) }; rechace(respuesta); } respuesta = { statusCode: 200, cuerpo: JSON.stringify(validación.valor) }; resolver(respuesta); }); |
Dependiendo del resultado, se devolverá una respuesta de la invocación de la función.
Diseñar una función para eliminar datos
Estamos en la última función de un paquete de operaciones CRUD. Ha llegado el momento de eliminar datos de la base de datos.
Navegue hasta el borrar y ejecute el siguiente comando:
1 |
npm instale couchbase joi --guardar |
Estaremos aceptando claves de documentos para ser borrados por lo que necesitaremos validar la entrada. Del mismo modo también necesitamos el SDK de Couchbase para trabajar con la base de datos.
Abra el archivo delete/delete.js e incluya el siguiente código JavaScript:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
const Couchbase = requiere("couchbase"); const Joi = requiere("joi"); var cubo = null; función principal(parámetros) { si(cubo == null) { var grupo = nuevo Couchbase.Grupo("couchbase://" + parámetros.host); grupo.autentifique(parámetros.nombre de usuario, parámetros.contraseña); cubo = grupo.openBucket(parámetros.bucketName); } var esquema = Joi.objeto().llaves({ id: Joi.cadena().obligatorio() }); var datos = parámetros; var respuesta = {}; devolver nuevo Promesa((resolver, rechace) => { var validación = Joi.valide(datos, esquema, { stripUnknown: verdadero }); si(validación.error) { respuesta = { statusCode: 500, cuerpo: JSON.stringify(validación.error.detalles) }; rechace(respuesta); } cubo.eliminar(validación.valor.id, (error, resultado) => { si(error) { respuesta = { cuerpo: JSON.stringify({ código: error.código, mensaje: error.mensaje }) }; rechace(respuesta); } respuesta = { cuerpo: JSON.stringify(validación.valor) }; resolver(respuesta); }); }); } exportaciones.principal = principal; |
Probablemente estás viendo el panorama general ahora en lo que respecta a la creación de funciones con OpenWhisk y Couchbase, por lo que no vamos a caminar a través de la función anterior para eliminar documentos.
Empaquetado y despliegue de las funciones en OpenWhisk con Docker
Tenemos un conjunto de funciones listas, pero no podemos empaquetarlas y desplegarlas en Bluemix. Si lo hiciéramos, obtendríamos un montón de errores. Bluemix utiliza un sabor especial de Linux con una arquitectura determinada. He descargado las dependencias en mi Mac que no es compatible.
¿Recuerdas el artículo que escribí hace un tiempo titulado, Implementación de dependencias de Node.js nativo en AWS Lambda? Necesitamos hacer algo similar para OpenWhisk con Docker.
Con Docker instalado y listo para funcionar, ejecute lo siguiente desde la CLI:
1 2 |
docker tire de openwhisk/nodojs6acción docker ejecute -it -v /Usuarios/nraboy/Escritorio/couchbase-openwhisk:/proyecto openwhisk/nodojs6acción /papelera/bash |
Los comandos anteriores descargarán una imagen Docker OpenWhisk apropiada para Node.js. Luego desplegaremos un contenedor con esa imagen en modo terminal interactivo. Este contenedor también tendrá un volumen mapeado. Estoy mapeando mi directorio local del proyecto a un directorio dentro del contenedor.
Después de que el comando se haya ejecutado y el contenedor se haya desplegado, deberías estar en el shell dentro del contenedor.
Para cada función, ejecute lo siguiente:
1 2 |
cd /proyecto/crear npm instale |
Recuerda, instalar las dependencias desde nuestra máquina anfitriona no es suficiente. Necesitamos compilar las dependencias para Bluemix. Docker compilará estas dependencias y como el directorio está mapeado, podemos utilizarlas desde la máquina anfitriona.
Una vez instalados los paquetes de cada función, podemos empaquetarlos y desplegarlos.
Desde la máquina anfitriona, cree un archivo ZIP de cada una de las funciones. El archivo debe contener paquete.json el archivo JavaScript y el archivo node_modules directorio.
Si estás en un Mac o en un ordenador con ZIP CLI, ejecuta lo siguiente:
1 2 |
cd crear zip -r crear.zip * |
Cuando tengas un ZIP de cada función, se pueden desplegar ejecutando lo siguiente:
1 |
bx wsk acción crear couchbase/borrar --amable nodejs:por defecto borrar.zip -p host ec2-45-236-32-140.calcular-1.amazonaws.es -p nombre de usuario demo -p contraseña bluemix -p bucketName ejemplo |
He introducido algunas cosas nuevas en el comando anterior.
En primer lugar, vamos a crear un paquete llamado couchbase y en este paquete tenemos un borrar basada en la función borrar.zip archivo. También estoy pasando algunos parámetros por defecto. Estos parámetros serán nuestra información de conexión. Como esta información es sensible, no los estamos pasando al invocar la función, sino al crear la función.
Ejecute una variación del comando anterior para cada una de sus funciones.
Para ejecutar su función, intente ejecutar algo como lo siguiente:
1 |
bx wsk acción invoque couchbase/crear -p nombre Nic -p apellido Raboy --bloqueo |
El comando anterior debe pasar algunos parámetros para pasar la validación. La función se invoca de forma bloqueante, y si tiene éxito, nuestros datos se guardarán en la base de datos y se devolverán en la respuesta.
Conclusión
Acabas de ver cómo crear un paquete de funciones sin servidor para OpenWhisk que se comunican con la base de datos NoSQL, Couchbase. OpenWhisk puede utilizarse como alternativa a AWS Lambda, pero no son las únicas opciones disponibles. Independientemente de lo que elijas, las funciones como servicio (FaaS) son soluciones muy escalables para aplicaciones masivas.
¿Quieres ver otro ejemplo de OpenWhisk? Echa un vistazo a un tutorial que escribí titulado, Convierte una API RESTful de Node.js a Serverless con OpenWhisk.
Hola 👋 Nick gran post
Hoy he aprendido algo nuevo sobre el paquete joi 👍🏼
Algunos consejos
Puedes hacer que tus acciones sean una acción web y tengan una url pública cuando creas -web true
También puede omitir JSON.stringify y establecer el cuerpo a un objeto que se stringify para usted.
Para obtener la url pública de la acción web puedes ejecutar wsk action get couchbase/delete -url
Puede obtener más información sobre el uso de acciones web en la documentación aquí https://console.bluemix.net/docs/openwhisk/openwhisk_webactions.html
Gracias por los comentarios.
[...] Usar OpenWhisk para FaaS con Node.js y Couchbase NoSQL [...]