Blog de Amazon Web Services (AWS)
Arquitecturas orientadas a eventos en AWS
Diogo Minhava e Israel López, Arquitectos de Soluciones para Startups en AWS
El concepto de arquitectura orientada a eventos, también conocida como EDA por sus siglas en inglés (Event-Driven Architecture), lleva años entre nosotros. Pero ha sido el auge de las arquitecturas basadas en microservicios, junto con la aparición de servicios serverless, lo que ha hecho que haya ido ganando más tracción en la comunidad. Cada vez más Startups optan por usar estos patrones a la hora de diseñar sus aplicaciones. El concepto es suficientemente amplio como para abarcar desde un simple flujo de trabajo para un caso de uso concreto, hasta el diseño de cómo van a interactuar todos los componentes de nuestra arquitectura.
En el siguiente artículo vamos a repasar brevemente en que consisten este tipo de arquitecturas y veremos, mediante un par de ejemplos, como desarrollar este tipo de soluciones utilizando los servicios en la nube de AWS.
¿Qué son las arquitecturas orientadas a eventos?
Empecemos por definir qué entendemos por evento. Un evento es una señal que se envía ante un cambio de estado en alguno de los componentes de la aplicación. Típicamente, este cambio es recogido en un mensaje con una serie de atributos, el cual nos permitirá pasar esta información al resto de componentes de nuestra arquitectura interesados en dicho cambio. Aunque tenemos total libertad a la hora de definir nuestros eventos, es una buena práctica contar con un esquema que estandarice su consumo. Estos eventos se establecen como la vía principal de intercambio de información entre los componentes, pasando de un modelo basado en peticiones/órdenes a uno en el que los componentes reaccionan al flujo de eventos. Este cambio de paradigma permite desacoplar aún más los servicios/microservicios, obteniendo un diseño más tolerante a los fallos en cascada típicos de sistemas distribuidos y evitando los cuellos de botella que pueden producirse en las invocaciones síncronas, especialmente cuando se combinan múltiples peticiones entre los componentes.
Este patrón de arquitectura necesita de un mecanismo para garantizar la comunicación entre los componentes. En AWS podemos usar servicios serverless como Amazon Simple Notification Service (SNS), Amazon Simple Queue Service (SQS), Amazon Kinesis o Amazon EventBridge para publicar y consumir nuestros eventos, sin tener que preocuparnos por gestionar ni un solo servidor.
Patrón Publicación/Suscripción con Amazon SNS y Amazon SQS
A continuación, vamos a ver cómo aplicar estos conceptos en el módulo de gestión de usuarios de una plataforma ficticia. Como parte del registro de usuarios, es habitual encontrarnos con integraciones hacia soluciones de terceros. Muchas veces estos procesos se pueden llevar a cabo de manera asíncrona, evitando impactar en el tiempo de espera y la experiencia de los usuarios.
En nuestro ejemplo, veremos como se integran nuestro programa de fidelización y el módulo de gestión de autorizaciones relativas a GDPR. Para esto usaremos el patrón de mensajería publicación/suscripción, que nos permite propagar simultáneamente y de manera asíncrona los eventos al resto de servicios, lo que también se conoce como fanout. Además, construiremos nuestra solución basándonos en servicios serverless, como Amazon SNS para la publicación, Amazon SQS para garantizar la entrega de manera fiable y desacoplar el consumo y AWS Lambda para la parte de cómputo. Esta solución nos permite escalar automáticamente para adaptar nuestra capacidad a la demanda. Pero también podemos, gracias al uso de colas SQS como consumidores, fijar la capacidad máxima de concurrencia de nuestras funciones Lambdas para evitar sobrecargar a los destinos, adaptando así la velocidad de consumo de los mensajes a la capacidad de procesamiento del sistema final, dejando que la cola actúe a modo de buffer.
Como podemos ver en el diagrama de arquitectura, el usuario inicia la acción de darse de alta desde nuestro portal web/aplicación móvil mediante una invocación a nuestra API de creación de usuarios, que exponemos mediante el servicio API Gateway, que actúa a modo de proxy del bloque de código que se encarga de validar los datos enviados por el usuario e introducirlos en el sistema. Todo esto ocurre de manera síncrona, pero al confirmarse la inserción del usuario en nuestro repositorio se publica el evento “UsuarioCreado”. Veamos como quedaría un ejemplo del evento creado:
{
"specversion":"1.0",
"type":"com.example.usercreated",
"source":"/mycontext",
"id":"C234-1234-1234",
"time":"2020-09-15T17:31:00Z",
"datacontenttype":"application/json",
"data":{
"name":"John",
"lastname":"Doe",
"email":"john.doe@example.com",
"address":"Main St Anytown, USA",
...
}
}
A la hora de publicar este evento, el componente que procesa la petición evalúa, en función de los datos introducidos por el usuario, si quiere formar parte del programa de fidelización de nuestro producto. En caso afirmativo, incluiremos el atributo “loyalty: true” en la publicación hacia el topic de SNS que hemos creado. Con esta estrategia, sacamos la lógica sobre si el componente de creación de perfil en el sistema de fidelización tiene que actuar ante el evento, centralizándolo en la suscripción y reduciendo de este modo las ejecuciones de dicho componente y el coste derivado de la mismas.
Este modelo de pub/sub también tiene como ventaja la facilidad de incluir nuevos destinos sin impactar en la solución que ya tenemos operativa. Supongamos un escenario en el que, desde el departamento de Marketing, nos han pedido disponer también de esta información. Simplemente tendremos que incluir un nuevo suscriptor al topic y escribir el código para integrar estos datos en la solución CRM que usa nuestro departamento de Marketing. Todo esto sin tocar una sola línea del resto de componentes.
Event-sourcing con Amazon Kinesis y AWS Lambda
En el ejemplo anterior, hemos visto como se almacenaba el cambio de estado en la base de datos de usuarios para posteriormente hacer el fanout hacia el resto de componentes. Pero, ¿qué ocurre en escenarios donde no podemos permitirnos tener una consistencia eventual de los datos? Tendríamos que gestionar una transacción distribuida, pero esto nos llevaría a una arquitectura mucho más compleja. Para estos escenarios contamos con una solución mejor: el patrón Event-sourcing.
Event-sourcing es un patrón en el cual, en lugar de actualizar la base de datos de nuestros objetos de dominio directamente, almacenamos toda la secuencia de cambios sobre los mismos como eventos en un registro global y duradero. Este registro, conocido como event-store, permite una de las características más interesantes de este patrón; se puede reconstruir el estado de la aplicación en cualquier punto, simplemente reprocesando los eventos almacenados. El ejemplo clásico es el de la cuenta bancaria, donde se registran como eventos todos los cambios que se producen en el saldo. Con esto, en lugar de contar únicamente con el saldo disponible o estado final, como ocurría en el caso de ir actualizando el registro en nuestra base de datos, tenemos todo el histórico de cambios. Esto nos permite recrear cualquier estado previo, algo muy útil de cara a la depuración de código para entender los cambios en el comportamiento de la aplicación. También nos permite reproducir todos los eventos sobre un nuevo componente para actualizar su estado al punto final. Contamos con una máquina del tiempo de todo lo que ocurre en nuestros objetos de dominio.
Este patrón se usa habitualmente junto con CQRS (Command-Query Responsibility Separation) en el que, como su propio nombre indica, los servicios de consulta están separados de los servicios que se encargan de actualizar el estado o comandos. En ambos patrones se estable una segregación en base a esta responsabilidad, nuestro event-store se encarga de los comandos, actuando como Single Source of Truth a partir del cual se crean las proyecciones que se encargarán de las lecturas.
Otro gran beneficio de event-sourcing es su flexibilidad a la hora incorporar nuevos servicios y ponerlos al día respecto al resto de componentes de la aplicación. Es posible que no sepamos qué tipo de patrones de consumo necesitaremos soportar en el futuro. Este patrón nos permite reproducir el histórico de eventos sobre un nuevo servicio, obteniendo el mismo estado final que el resto de servicios sin necesidad de hacer cargas de datos adicionales. Por continuar con el ejemplo anterior de la gestión de los usuarios, donde pueden elegir estar en un programa de fidelización, es muy posible que al iniciar nuestra Startup no nos planteemos contar con programas de este tipo, sino que decidamos agregarlo a posteriori. Un programa de fidelización suele tener puntos que los clientes obtienen al realizar compras. Al haber utilizado event-sourcing, podemos crear este nuevo servicio desde cero y luego podemos reproducir todos los eventos que almacenamos (creación de usuarios, suscripción al programa de fidelidad, añadir compras realizadas) para llevar ese servicio al estado actual. Con esto, tendremos los puntos de fidelidad de nuestros clientes sin tener que consultar las bases de datos existentes para revisar todas las compras realizadas en el pasado, pudiendo bonificar a los primeros usuarios que confiaron en nuestro producto, de manera retroactiva, sin hacer un esfuerzo adicional.
Se puede desplegar una arquitectura basada en Event-sourcing de distintas maneras. A continuación, se muestra un ejemplo basado en el registro de eventos mediante el uso de Amazon Kinesis Data Streams:
En esta arquitectura, el punto de entrada para nuestra aplicación es Amazon API Gateway. Usamos una función Lambda para validar la petición y publicar el evento en un flujo de datos de Kinesis Data Streams (KDS), que mantendrá el orden de los eventos y conservará los datos hasta un máximo de 365 días. Consumimos los eventos de este flujo de datos con Amazon Kinesis Data Firehose (KDF) para archivarlos en Amazon S3 y también actualizar un índice de Amazon Elasticsearch Service (ES). Podríamos usar una función Lambda para este fin, pero Amazon KDF hace esto de manera gestionada. Esto ya recuerda al patrón CQRS; hemos separado las lecturas, que realizaremos sobre Amazon ES, de la parte de la arquitectura que se encarga de registrar los eventos, lo que nos otorga mayor libertad a la hora de construir modelos de datos óptimos para su consumo en función de los casos de uso. Por último, también podemos combinar esta solución donde hacemos polling de los eventos con la estrategia de fanout. Esto es interesante si tenemos un subconjunto de eventos que queremos propagar hacia varios consumidores. En paralelo a KDF, podemos consumir del flujo de datos con una función Lambda que evalúa qué mensajes publicar, evitando tener a todos los consumidores haciendo polling sobre el mismo stream.
Nordstrom compartió un ejemplo real de su arquitectura basada en event-sourcing: https://www.youtube.com/watch?v=O7PTtm_3Os4
Conclusión
Este tipo de arquitecturas suponen un cambio de paradigma frente a modelos más tradicionales. Gracias a la segregación de responsabilidades y al intercambio asíncrono de mensajes nos permiten agregar nuevas funcionalidades sin interferir con lo ya existente, hacer que nuestras aplicaciones sean más tolerantes a fallos y nos ayudan a ganar flexibilidad e iterar de manera más rápida.
Para empezar, puedes visitar el siguiente enlace donde encontrarás información y más ejemplos de arquitecturas orientadas a eventos: https://aws.amazon.com/event-driven-architecture/
Sobre los autores
Israel López Moriano es Arquitecto de Soluciones en AWS basado en Barcelona. Disfruta pudiendo trabajar de cerca con las Startups, ayudando a empujar la innovación desde los servicios en la nube. El tiempo que no dedica a esta la labor lo pasa corriendo detrás de su hijo pequeño y haciendo maratones (de series).
Diogo Minhava Lopes es Arquitecto de Soluciones en AWS donde ayuda las Startups de España y Portugal a aprovechar al máximo la tecnología de AWS. Fuera del trabajo, es fácil encontrarle tocando alguna de sus guitarras o perdiendo encarnizadas batallas con su GPS sobre qué ruta es la mejor.