CQRS sobre AWS con Lambdas, DynamoDB y SNS

0
2915

Índice de contenidos

1. Introducción

En este tutorial empezaremos explicando las bases del patrón de arquitectura CQRS.
Después de explicar la teoría, vamos a implementar y desplegar un pequeño entorno serverless con lambdas sobre AWS utilizando eventos para la sincronización entre las distintas partes del sistema.

En el tutorial «Arquitectura Serverless con Lambdas sobre AWS» vimos como funcionan las arquitecturas «serverless» y como nos pueden facilitar el desarrollo para conseguir unos tiempos de puesta en producción muy cortos. Utilizamos el framework Serverless y las lambdas de AWS. También utilizamos su API Gateway para exponer los endpoints y su base de datos noSQL DynamoDB.

Ahora daremos un paso más utilizando el servicio de SNS
de Amazon para permitir comunicaciones entre distintas «lambdas» con un bajo acoplamiento. También
explicaremos las ventajas de una arquitectura CQRS y montaremos un pequeño ejemplo para explicarlo.

2. Entorno

El tutorial está escrito usando el siguiente entorno:

  • Hardware: Portátil MacBook Pro 15′ (2,2 GHz Intel Core i7, 16GB DDR3).
  • Sistema Operativo: Mac OS High Sierra
  • Entorno de desarrollo: Visual Studio Code
  • Software: serverless framework, aws client, nodejs

3. CQRS

CQRS son las siglas de Command Query Responsibility Segregation. Es un patrón
que trata de separar totalmente la responsabilidad de modificación de la responsabilidad de lectura.
Al contrario de un patrón CRUD donde los modelos utilizados para las modificaciones y las lecturas
son los mismos; con CQRS los modelos utilizados por las operaciones de actualización (commands) pueden
ser totalmente diferentes al modelo/s que exponen las distintas operaciones de lectura (queries).

Lo normal es crear incluso bases de datos independientes para cada parte.
La base de datos de lectura y de la de escritura pueden ser iguales o no. Así cada uno puede
tener el modelo que mejor se adapta a sus necesidades. Esto implica una gran mejora en el rendimiento de lectura pues la
base de datos es solo para realizar esa consulta, está más disponible y tiene la información en el
formato y estructura que necesita, desnormalizada de antemano. El tipo de base de datos también se puede adaptar a las necesidades
de cada «query». Así una parte del sistema podrá estar utilizando una base de datos relacional tradicional,
otras partes se adapten mejor a una NoSQL, e incluso alguna opte por un almacén en memoria tipo Redis.

Por contra el tener varias bases de datos implica tener que mantenerlas sincronizadas con la
complejidad que ello conlleva. Para ello se suele utilizar un sistema de comunicaciones mediante
eventos. De tal manera que los commands lanzan un evento para notificar el cambio solicitado.
Este evento puede ser almacenado para tener un histórico. Y en base a estos eventos distintos interesados
aplicarán los cambios que necesiten sobre el modelo del que son responsables.

Como hemos visto este patrón tiene sus ventajas y sus desventajas por lo que solo debe usarlo si
procede, pues añade gran complejidad. Es útil si los modelos a exponer son dispares y complejos
y si se quiere ofrecer un alto rendimiento en los accesos de lectura.

4. CQRS en AWS

Para montar esto sobre AWS vamos a ver como se podría implementar con Lambdas y algunas piezas más:

  • API Gateway: expone los endpoints e invoca las lambdas de tipo «command» y «query»
  • DynamoDB: base de datos de NoSQL que usaremos para persistir la información. Podremos tener dos tipos de almacenes:
    • Events: guardan un histórico de eventos (invocaciones a lambdas de tipo commands).
    • States: guardan los datos con el modelo que es de interés para la query/ies que lo utilizan.
  • Lambdas: las funciones que dividiremos en los siguientes tipos:
    • Commands: invocadas a través de la API para ejecutar cambios. Crearán un evento para que quien lo necesite actualice sus modelos.
    • Queries: invocadas a través de la API para ejecutar consultas
    • Save Event: invocadas cuando se lanza un evento en un command. Tienen una relación 1 a 1 con un command y se encargan de guardar el evento en un histórico (DynamoDB Events). Podría hacerse directamente en el command pero así se logra una respuesta más rápida.
    • Update State: invocadas cuando se lanza un evento en un command que le es de interés para su modelo. Actualizan su modelo en su base de datos (DynamoDB State) para que sea consultado por una Lambda Query. Pueden existir varias fuentes que conlleven actualizar un estado y por tanto varias lambdas de tipo Update State para el mismo almacén.
  • SNS: Amazon Simple Notification Service será nuestro servicio de subscripción y publicación de eventos. Mediante este servicio conseguiremos que las lambdas de tipo command no tengan que conocer quien necesita saber de su modificación. Estas publicaran el evento y los interesados se subscribirán a él.

Como podéis ver en el dibujo hay dos partes diferenciadas en la arquitectura. El lado de los «commands» (actualizaciones) y el lado de las «queries» (consultas).
Pero puede haber un command que conlleve varias queries, porque se lanza una modificación y hay varias optimizaciones de su modelo para distintas consultas.
Y también puede pasar al contrario, un modelo para una query se alimenta de las modificaciones de varios command porque combine la información de varias fuentes para una consulta más rápida.

5. Implementando el ejemplo

Vamos a montar un ejemplo con el framework Serverless.
Podéis ver el ejemplo completo aquí.
Algunas cosas las pasaré por alto porque ya las vimos en este otro tutorial.

El ejemplo es muy muy sencillo para que pueda entenderse rápidamente. Tendréis que poner un poquito de imaginación para ver lo que potencialmente se podría hacer con este tipo de diseño para cosas más complejas como endpoints óptimos de informes, recuentos, cálculos complejos precalculados, unión de modelos, etc.

El sistema permitirá dar de alta libros, consultarlos y consultar el número de libros para un autor concreto.

Diseño

Lo primero que hemos hecho es diseñar las distintas partes del sistema.

Imagen ejemplo CQRS sobre AWS

Lambda Commands:

  • Lambda Command CreateBook: Se activará mediante un endpoint tipo POST y permitirá dar de alta libros. Creará un evento «create-book» para que los interesados actuen en consecuencia.

SNS events:

  • SNS Evento create-book: Este evento contiene la información enviada al endpoint de alta de un libro

Lambdas Save Event:

  • Lambda Save Event create-book: Se activará cuando se lance un evento de tipo create-book. Guarda el evento tal cual en un almacén para tener un histórico.

Lambdas Update State:

  • Lambda Save State Book: Se activará cuando se lance un evento de tipo create-book. Guardará el estado actual de cada book para su consulta.
  • Lambda Save State Count Author: Se activará cuando se lance un evento de tipo create-book. Actualizará el recuento de libros del autor en cuestión.

Lambdas Query:

  • Lambda Query getBooks: Se activará mediante un endpoint tipo GET y permitirá consultar el estado actual de los libros
  • Lambda Query countAuthors: Se activará mediante un endpoint tipo GET y permitirá obtener el recuento de libros de cada autor

DynamoDB:

  • DynamoDB Events: Almacena los eventos para el histórico
  • DynamoDB Books: Almacena el estado actual de los libros
  • DynamoDB CountAuthors: Almacena el recuento actual de libros de un autor

Configuración de serverless

Y así quedarían las partes más importantes nuestro fichero serverless.yml:

	service: serverless-cqrs-example-books

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

	...

	provider:
		name: aws
		stage: dev
		region: eu-west-3
		runtime: nodejs4.3

	functions:
		commandCreateBook:
			handler: commands.createBook
			events:
				- http: 'POST /books'
		queryCountAuthor:
			handler: queries.countAuthors
			events:
				- http: 'GET /author/count'
		queryGetBooks:
			handler: queries.getBooks
			events:
				- http: 'GET /books'
		communicationSaveEventCreateBook:
			handler: communications.saveEventCreateBook
			events:
				- sns:
						topicName: create-book
						displayName: "Book create events"
		communicationSaveStateBook:
			handler: communications.saveStateBook
			events:
				- sns:
						topicName: create-book
						displayName: "Book create events"
		communicationSaveStateCountAuthor:
			handler: communications.saveSetateCountAuthor
			events:
				- sns:
						topicName: create-book
						displayName: "Book create events"
		
	resources:
		Resources:
			EventsBooksDynamoDBTable:
				Type: 'AWS::DynamoDB::Table'
				...
			StateBooksDynamoDBTable:
				Type: 'AWS::DynamoDB::Table'
				...
			StateAuthorCountDynamoDBTable:
				Type: 'AWS::DynamoDB::Table'
				...

Código node.js

Las funciones lambda las hemos dividido en 3 ficheros: commands.js, communications.js y queries.js

En commands.js solo debemos lanzar el evento. Para ello nos ayudamos de bus.js que declara la conexión con SNS. Así solo tendremos que enviarle el evento con su tipo:

	
const uuid = require('uuid');
const bus = require("./lib/bus")();

module.exports.createBook = (event, context, callback) => {
  const data = JSON.parse(event.body);
  ... //validation data input

  let generatedId = uuid.v1();
  const createBookEvent = {
    type: 'create-book',
    timestamp: new Date().getTime(),
    id: generatedId,
    payload: {
      id: generatedId,
      title: data.title,
      author: data.author
    }
  };
  
  bus.publish(createBookEvent,
    msg => ... , //Callback OK
    err => ... , //Callback KO
  );
};

En communications.js creamos las lambdas que se subscriben al evento para realizar actualizaciones en sus modelos en dynamoDB

const db = require("./lib/db")();

...

module.exports.saveEventCreateBook = (message, context, callback) => {
  const event = parseEvent(message);

  if (event.type !== "create-book") return;

  db.save(process.env.EVENTS_BOOKS_TABLE, event,
    entry => callback(null, entry),
    error => callback(error));
};

module.exports.saveStateBook = (message, context, callback) => {
  const event = parseEvent(message);

  if (event.type !== "create-book") return;

  let book = event.payload;

  db.save(process.env.STATE_BOOKS_TABLE, book, 
    entry => callback(null, entry), 
    error => callback(error));
};

module.exports.saveStateCountAuthor = (message, context, callback) => {
  const event = parseEvent(message);

  if (event.type !== "create-book") return;

  let author = event.payload.author;
  db.update(process.env.STATE_COUNT_AUTHORS_TABLE, author,
    "set totalCount = totalCount + :incr",
    {":incr": 1},
    entry => callback(entry),
    error => {
      let countAuthor = {
        id: author,
        totalCount: 1
      }
      db.save(process.env.STATE_COUNT_AUTHORS_TABLE, countAuthor, 
        entry => callback(null, entry), 
        error => callback(error)
      );
    }
  );
};

En queries.js creamos las lambdas que serán llamadas por los endpoints de consulta y simplemente obtienen los datos ya desformalizados de dynamoDB

	
const db = require("./lib/db")();

module.exports.getBooks = (event, context, callback) => {
  db.all(process.env.STATE_BOOKS_TABLE, 
    res => callback(null, {
      statusCode: 200,
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(res),
    }),
    err => callback(err)
  );
};


module.exports.countAuthors = (event, context, callback) => {
  db.all(process.env.STATE_COUNT_AUTHORS_TABLE, 
    res => callback(null, {
      statusCode: 200,
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(res),
    }),
    err => callback(err)
  );
};

Despliegue en local

Para poder desplegar y probar en local primero debemos instalar dynamoDB en local para poder conectar a la base de datos. Para ello ejecutamos dentro del directorio «books» el comando siguiente:

	serverless dynamodb install

Y ahora procedemos a arrancar todo en local con el siguiente comando:

	serverless offline start

En el fichero «curls.sh» tenéis ejemplos de peticiones a nuestra API REST en el que se lanzan peticiones «curl» a los endpoints. Deberíamos ver un resultado parecido a este:

	#Create Books:
	> curl -H "Content-Type: application/json" -d '{"title":"Conversaciones con CEOs y CIOs sobre Transformación Digital y Metodologías Ágiles","author":"Roberto Canales Mora"}' http://localhost:3000/books
		{"id":"183633f0-1b4c-11e8-acf9-4fd3234134fd","title":"Conversaciones con CEOs y CIOs sobre Transformación Digital y Metodologías Ágiles","author":"Roberto Canales Mora"}
	> curl -H "Content-Type: application/json" -d '{"title":"Informática Profesional","author":"Roberto Canales Mora"}' http://localhost:3000/books
		{"id":"18d58130-1b4c-11e8-acf9-4fd3234134fd","title":"Informática Profesional","author":"Roberto Canales Mora"}
	> curl -H "Content-Type: application/json" -d '{"title":"Planifica Tu Éxito, De Aprendiz A Empresario","author":"Roberto Canales Mora"}' http://localhost:3000/books
		{"id":"1974f580-1b4c-11e8-acf9-4fd3234134fd","title":"Planifica Tu Éxito, De Aprendiz A Empresario","author":"Roberto Canales Mora"}
	> curl -H "Content-Type: application/json" -d '{"title":"Clean Code","author":"Robert C. Martin"}' http://localhost:3000/books
		{"id":"1a1490e0-1b4c-11e8-acf9-4fd3234134fd","title":"Clean Code","author":"Robert C. Martin"}
	
	#Get Books:
	> curl -H "Content-Type: application/json" http://localhost:3000/books
		[{"title":"Clean Code","author":"Robert C. Martin","id":"1a1490e0-1b4c-11e8-acf9-4fd3234134fd"},{"title":"Informática Profesional","author":"Roberto Canales Mora","id":"18d58130-1b4c-11e8-acf9-4fd3234134fd"},{"title":"Planifica Tu Éxito, De Aprendiz A Empresario","author":"Roberto Canales Mora","id":"1974f580-1b4c-11e8-acf9-4fd3234134fd"},{"title":"Conversaciones con CEOs y CIOs sobre Transformación Digital y Metodologías Ágiles","author":"Roberto Canales Mora","id":"183633f0-1b4c-11e8-acf9-4fd3234134fd"}]
	
	#Count Authors:
	> curl -H "Content-Type: application/json" http://localhost:3000/authors/count
		[{"totalCount":1,"id":"Robert C. Martin"},{"totalCount":3,"id":"Roberto Canales Mora"}]

Despliegue en AWS

Vamos a proceder a desplegar en AWS nuestras funciones. Tenemos que tener instalado y configurado el cliente de AWS. Si no tienes una cuenta puedes crearla y usar la
capa gratuita de AWS que ofrece más que de sobra para todas las pruebas que queráis.
Podéis ver cómo instalar el cliente aquí y como configurarlo aquí.

Debemos configurar un parámetro de entorno para que las lambdas sepan el nombre de la cola sns a la que conectarse.

		aws configure
		aws ssm put-parameter --name accountId --type String --value  --region eu-west-3

Serverless basándose en la configuración que hemos definido en el fichero «books/serverless.yml» nos creará todas las piezas necesarias dentro del ecosistema de AWS con solo ejecutar:

	serverless deploy

Ya podemos realizar peticiones con el el fichero «curls.sh» si cambiamos el SERVICE_URL por el que AWS nos ha asignado. Y cuando terminemos, con el siguiente comando podemos borrar todos los elementos que serverless ha dado de alta en AWS automáticamente:

	serverless remove

6. Conclusiones

Como vemos trabajar con eventos en las lambdas de AWS es relativamente sencillo. Además el servicio SNS es muy fácil de configurar desde el framework Serverless y podemos trabajar en local para desarrollo emulándolo con el plugin «serverless-offline-sns».
Recordar que un patrón CQRS es demasiado complejo como para aplicarlo la mayoría de la veces pero quizas es útil para aplicarlo parcialmente en algunas situaciones.

7. Referencias

  • CQRS. Martin Fowler.
  • AWS. Amazon.

DEJA UNA RESPUESTA

Por favor ingrese su comentario!

He leído y acepto la política de privacidad

Por favor ingrese su nombre aquí

Información básica acerca de la protección de datos

  • Responsable:
  • Finalidad:
  • Legitimación:
  • Destinatarios:
  • Derechos:
  • Más información: Puedes ampliar información acerca de la protección de datos en el siguiente enlace:política de privacidad